From f2116d9b2ea83b853a511377f14dc935c2e9b84e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 3 Jun 2020 07:24:53 +0100 Subject: [PATCH 001/305] Tutorial: add detail on readline method. --- v3/docs/DRIVERS.md | 2 +- v3/docs/TUTORIAL.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 53eb724..7d17c23 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -129,7 +129,7 @@ Constructor arguments: 1. `pin` Mandatory. The initialised Pin instance. 2. `suppress` Default `False`. See - [4.2.1](./DRIVERS.md#421-the-suppress-constructor-argument). + [section 4.1.1](./DRIVERS.md#411-the-suppress-constructor-argument). Methods: diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index f890460..9614c56 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1709,7 +1709,8 @@ data is available. The methods should return as fast as possible with as much data as is available. `readline()` Return as many characters as are available up to and including any -newline character. Required if you intend to use `StreamReader.readline()` +newline character. Required if you intend to use `StreamReader.readline()`. +It should return a maximum of one line. `read(n)` Return as many characters as are available but no more than `n`. Required to use `StreamReader.read()` or `StreamReader.readexactly()` From 32877440424889eb5e0e16cabdc93d235bedfda8 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 5 Jun 2020 18:27:22 +0100 Subject: [PATCH 002/305] V3 tutorial: document Delay_ms class. --- v3/docs/TUTORIAL.md | 77 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 9614c56..3ff08de 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -29,7 +29,8 @@ REPL. 3.5 [Queue](./TUTORIAL.md#35-queue) 3.6 [Message](./TUTORIAL.md#36-message) 3.7 [Barrier](./TUTORIAL.md#37-barrier) - 3.8 [Synchronising to hardware](./TUTORIAL.md#38-synchronising-to-hardware) + 3.8 [Delay_ms](./TUTORIAL.md#38-delay_ms-class) Software retriggerable delay. + 3.9 [Synchronising to hardware](./TUTORIAL.md#39-synchronising-to-hardware) Debouncing switches and pushbuttons. Taming ADC's. 4. [Designing classes for asyncio](./TUTORIAL.md#4-designing-classes-for-asyncio) 4.1 [Awaitable classes](./TUTORIAL.md#41-awaitable-classes) @@ -491,7 +492,7 @@ following classes which are non-standard, are also in that directory: Calls a user callback if not cancelled or regularly retriggered. A further set of primitives for synchronising hardware are detailed in -[section 3.8](./TUTORIAL.md#38-synchronising-to-hardware). +[section 3.9](./TUTORIAL.md#38-synchronising-to-hardware). To install the primitives, copy the `primitives` directory and contents to the target. A primitive is loaded by issuing (for example): @@ -972,7 +973,77 @@ callback runs. On its completion the tasks resume. ###### [Contents](./TUTORIAL.md#contents) -## 3.8 Synchronising to hardware +## 3.8 Delay_ms class + +This implements the software equivalent of a retriggerable monostable or a +watchdog timer. It has an internal boolean `running` state. When instantiated +the `Delay_ms` instance does nothing, with `running` `False` until triggered. +Then `running` becomes `True` and a timer is initiated. This can be prevented +from timing out by triggering it again (with a new timeout duration). So long +as it is triggered before the time specified in the preceeding trigger it will +never time out. + +If it does time out the `running` state will revert to `False`. This can be +interrogated by the object's `running()` method. In addition a **function** can +be specified to the constructor. This will execute when a timeout occurs; where +the **function** is a coroutine it will be converted to a `Task` and run +asynchronously. + +Constructor arguments (defaults in brackets): + + 1. `func` The **function** to call on timeout (default `None`). + 2. `args` A tuple of arguments for the **function** (default `()`). + 3. `can_alloc` Boolean, default `True`. See below. + 4. `duration` Integer, default 1000ms. The default timer period where no value + is passed to the `trigger` method. + +Methods: + + 1. `trigger` optional argument `duration=0`. A timeout will occur after + `duration` ms unless retriggered. If no arg is passed the period will be that + of the `duration` passed to the constructor. See Class variable below. + 2. `stop` No argument. Cancels the timeout, setting the `running` status + `False`. The timer can be restarted by issuing `trigger` again. + 3. `running` No argument. Returns the running status of the object. + 4. `__call__` Alias for running. + +Class variable: + + 1. `verbose=False` If `True` a warning will be printed if a running timer is + retriggered with a time value shorter than the time currently outstanding. + Such an operation has no effect owing to the design of `uasyncio`. + +If the `trigger` method is to be called from an interrupt service routine the +`can_alloc` constructor arg should be `False`. This causes the delay object +to use a slightly less efficient mode which avoids RAM allocation when +`trigger` runs. + +In this example a 3 second timer starts when the button is pressed. If it is +pressed repeatedly the timeout will not be triggered. If it is not pressed for +3 seconds the timeout triggers and the LED lights. + +```python +from pyb import LED +from machine import Pin +import uasyncio as asyncio +from primitives.pushbutton import Pushbutton +from primitives.delay_ms import Delay_ms + +async def my_app(): + await asyncio.sleep(60) # Run for 1 minute + +pin = Pin('X1', Pin.IN, Pin.PULL_UP) # Pushbutton to gnd +red = LED(1) +pb = Pushbutton(pin) +d = Delay_ms(lambda led: led.on(), (red,)) +pb.press_func(d.trigger, (3000,)) # Note how function and args are passed +try: + asyncio.run(my_app()) # Run main application code +finally: + asyncio.new_event_loop() # Clear retained state +``` + +## 3.9 Synchronising to hardware The following hardware-related classes are documented [here](./DRIVERS.md): * `Switch` A debounced switch which can trigger open and close user callbacks. From e5d57771b0d6ac59e9a9b24d9045e648651d7033 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 8 Jun 2020 07:48:28 +0100 Subject: [PATCH 003/305] Rewrite Delay_ms class for uasyncio V3. Add delay_test --- v3/primitives/__init__.py | 5 +- v3/primitives/delay_ms.py | 92 ++++++++-------- v3/primitives/tests/delay_test.py | 175 ++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+), 48 deletions(-) create mode 100644 v3/primitives/tests/delay_test.py diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index fb4bd3c..35d8264 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -14,6 +14,5 @@ async def _g(): def launch(func, tup_args): res = func(*tup_args) if isinstance(res, type_coro): - loop = asyncio.get_event_loop() - loop.create_task(res) - + res = asyncio.create_task(res) + return res diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py index 03df40a..34dc917 100644 --- a/v3/primitives/delay_ms.py +++ b/v3/primitives/delay_ms.py @@ -2,64 +2,66 @@ # Copyright (c) 2018-2020 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file +# Rewritten for uasyncio V3. Allows stop time to be brought forwards. import uasyncio as asyncio -import utime as time +from utime import ticks_add, ticks_diff, ticks_ms +from micropython import schedule from . import launch # Usage: # from primitives.delay_ms import Delay_ms class Delay_ms: verbose = False + # can_alloc retained to avoid breaking code. Now unsed. def __init__(self, func=None, args=(), can_alloc=True, duration=1000): - self.func = func - self.args = args - self.can_alloc = can_alloc - self.duration = duration # Default duration - self._tstop = None # Killer not running - self._running = False # Timer not running - if not can_alloc: - asyncio.create_task(self._run()) - - async def _run(self): - while True: - if not self._running: # timer not running - await asyncio.sleep_ms(0) - else: - await self._killer() + self._func = func + self._args = args + self._duration = duration # Default duration + self._tstop = None # Stop time (ms). None signifies not running. + self._tsave = None # Temporary storage for stop time + self._ktask = None # timer task + self._do_trig = self._trig # Avoid allocation in .trigger def stop(self): - self._running = False - # If uasyncio is ever fixed we should cancel .killer + if self._ktask is not None: + self._ktask.cancel() def trigger(self, duration=0): # Update end time - self._running = True - if duration <= 0: - duration = self.duration - tn = time.ticks_add(time.ticks_ms(), duration) # new end time - self.verbose and self._tstop is not None and self._tstop > tn \ - and print("Warning: can't reduce Delay_ms time.") - # Start killer if can allocate and killer is not running - sk = self.can_alloc and self._tstop is None - # The following indicates ._killer is running: it will be - # started either here or in ._run - self._tstop = tn - if sk: # ._killer stops the delay when its period has elapsed - asyncio.create_task(self._killer()) + now = ticks_ms() + if duration <= 0: # Use default set by constructor + duration = self._duration + is_running = self() + tstop = self._tstop # Current stop time + # Retriggering normally just updates ._tstop for ._timer + self._tstop = ticks_add(now, duration) + # Identify special case where we are bringing the end time forward + can = is_running and duration < ticks_diff(tstop, now) + if not is_running or can: + schedule(self._do_trig, can) + + def _trig(self, can): + if can: + self._ktask.cancel() + self._ktask = asyncio.create_task(self._timer(can)) + + def __call__(self): # Current running status + return self._tstop is not None - def running(self): - return self._running + running = __call__ - __call__ = running + async def _timer(self, restart): + if restart: # Restore cached end time + self._tstop = self._tsave + try: + twait = ticks_diff(self._tstop, ticks_ms()) + while twait > 0: # Must loop here: might be retriggered + await asyncio.sleep_ms(twait) + twait = ticks_diff(self._tstop, ticks_ms()) + if self._func is not None: + launch(self._func, self._args) # Timed out: execute callback + finally: + self._tsave = self._tstop # Save in case we restart. + self._tstop = None # timer is stopped - async def _killer(self): - twait = time.ticks_diff(self._tstop, time.ticks_ms()) - while twait > 0: # Must loop here: might be retriggered - await asyncio.sleep_ms(twait) - if self._tstop is None: - break # Return if stop() called during wait - twait = time.ticks_diff(self._tstop, time.ticks_ms()) - if self._running and self.func is not None: - launch(self.func, self.args) # Timed out: execute callback - self._tstop = None # killer not running - self._running = False # timer is stopped +# TODO launch returns the Task: make this available? diff --git a/v3/primitives/tests/delay_test.py b/v3/primitives/tests/delay_test.py new file mode 100644 index 0000000..d7d0464 --- /dev/null +++ b/v3/primitives/tests/delay_test.py @@ -0,0 +1,175 @@ +# delay_test.py Tests for Delay_ms class + +# Copyright (c) 2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +import micropython +from primitives.delay_ms import Delay_ms + +micropython.alloc_emergency_exception_buf(100) + +def printexp(exp, runtime=0): + print('Expected output:') + print('\x1b[32m') + print(exp) + print('\x1b[39m') + if runtime: + print('Running (runtime = {}s):'.format(runtime)) + else: + print('Running (runtime < 1s):') + +async def ctor_test(): # Constructor arg + s = ''' + Trigger 5 sec delay + Retrigger 5 sec delay + Callback should run + cb callback + Done + ''' + printexp(s, 12) + def cb(v): + print('cb', v) + + d = Delay_ms(cb, ('callback',), duration=5000) + + print('Trigger 5 sec delay') + d.trigger() + await asyncio.sleep(4) + print('Retrigger 5 sec delay') + d.trigger() + await asyncio.sleep(4) + print('Callback should run') + await asyncio.sleep(2) + print('Done') + +async def launch_test(): + s = ''' + Trigger 5 sec delay + Coroutine should run + Coroutine starts + Coroutine ends + Done + ''' + printexp(s, 7) + async def cb(v): + print(v, 'starts') + await asyncio.sleep(1) + print(v, 'ends') + + d = Delay_ms(cb, ('coroutine',)) + + print('Trigger 5 sec delay') + d.trigger(5000) # Test extending time + await asyncio.sleep(4) + print('Coroutine should run') + await asyncio.sleep(3) + print('Done') + + +async def reduce_test(): # Test reducing a running delay + s = ''' + Trigger 5 sec delay + Callback should run + cb callback + Callback should run + Done + ''' + printexp(s, 11) + def cb(v): + print('cb', v) + + d = Delay_ms(cb, ('callback',)) + + print('Trigger 5 sec delay') + d.trigger(5000) # Test extending time + await asyncio.sleep(4) + print('Callback should run') + await asyncio.sleep(2) + d.trigger(10000) + await asyncio.sleep(1) + d.trigger(3000) + await asyncio.sleep(2) + print('Callback should run') + await asyncio.sleep(2) + print('Done') + + +async def stop_test(): # Test the .stop and .running methods + s = ''' + Trigger 5 sec delay + Running + Callback should run + cb callback + Callback should not run + Done + ''' + printexp(s, 12) + def cb(v): + print('cb', v) + + d = Delay_ms(cb, ('callback',)) + + print('Trigger 5 sec delay') + d.trigger(5000) # Test extending time + await asyncio.sleep(4) + if d(): + print('Running') + print('Callback should run') + await asyncio.sleep(2) + d.trigger(3000) + await asyncio.sleep(1) + d.stop() + await asyncio.sleep(1) + if d(): + print('Running') + print('Callback should not run') + await asyncio.sleep(4) + print('Done') + + +async def isr_test(): # Test trigger from hard ISR + from pyb import Timer + s = ''' + Timer holds off cb for 5 secs + cb should now run + cb callback + Done + ''' + printexp(s, 6) + def cb(v): + print('cb', v) + + d = Delay_ms(cb, ('callback',)) + + def timer_cb(_): + d.trigger(200) + tim = Timer(1, freq=10, callback=timer_cb) + + print('Timer holds off cb for 5 secs') + await asyncio.sleep(5) + tim.deinit() + print('cb should now run') + await asyncio.sleep(1) + print('Done') + +av = ''' +Run a test by issuing +delay_test.test(n) +where n is a test number. Avaliable tests: +\x1b[32m +0 Test triggering from a hard ISR (Pyboard only) +1 Test the .stop method +2 Test reducing the duration of a running timer +3 Test delay defined by constructor arg +4 Test triggering a Task +\x1b[39m +''' +print(av) + +tests = (isr_test, stop_test, reduce_test, ctor_test, launch_test) +def test(n=0): + try: + asyncio.run(tests[n]()) + finally: + asyncio.new_event_loop() From a4a1f8c6271feb1f36bf333f81eb28200ced692e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 8 Jun 2020 08:57:51 +0100 Subject: [PATCH 004/305] V3 tutorial: document new Delay_ms class. --- v3/docs/TUTORIAL.md | 45 +++++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 3ff08de..efe5d72 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -993,7 +993,7 @@ Constructor arguments (defaults in brackets): 1. `func` The **function** to call on timeout (default `None`). 2. `args` A tuple of arguments for the **function** (default `()`). - 3. `can_alloc` Boolean, default `True`. See below. + 3. `can_alloc` Unused arg, retained to avoid breaking code. 4. `duration` Integer, default 1000ms. The default timer period where no value is passed to the `trigger` method. @@ -1001,44 +1001,37 @@ Methods: 1. `trigger` optional argument `duration=0`. A timeout will occur after `duration` ms unless retriggered. If no arg is passed the period will be that - of the `duration` passed to the constructor. See Class variable below. + of the `duration` passed to the constructor. The method can be called from a + hard or soft ISR. It is now valid for `duration` to be less than the current + time outstanding. 2. `stop` No argument. Cancels the timeout, setting the `running` status `False`. The timer can be restarted by issuing `trigger` again. 3. `running` No argument. Returns the running status of the object. 4. `__call__` Alias for running. -Class variable: - - 1. `verbose=False` If `True` a warning will be printed if a running timer is - retriggered with a time value shorter than the time currently outstanding. - Such an operation has no effect owing to the design of `uasyncio`. - -If the `trigger` method is to be called from an interrupt service routine the -`can_alloc` constructor arg should be `False`. This causes the delay object -to use a slightly less efficient mode which avoids RAM allocation when -`trigger` runs. - -In this example a 3 second timer starts when the button is pressed. If it is -pressed repeatedly the timeout will not be triggered. If it is not pressed for -3 seconds the timeout triggers and the LED lights. +In this example a `Delay_ms` instance is created with the default duration of +1s. It is repeatedly triggered for 5 secs, preventing the callback from +running. One second after the triggering ceases, the callback runs. ```python -from pyb import LED -from machine import Pin import uasyncio as asyncio -from primitives.pushbutton import Pushbutton from primitives.delay_ms import Delay_ms async def my_app(): - await asyncio.sleep(60) # Run for 1 minute + print('Holding off callback') + for _ in range(10): # Hold off for 5 secs + await asyncio.sleep_ms(500) + d.trigger() + print('Callback will run in 1s') + await asyncio.sleep(2) + print('Done') + +def callback(v): + print(v) -pin = Pin('X1', Pin.IN, Pin.PULL_UP) # Pushbutton to gnd -red = LED(1) -pb = Pushbutton(pin) -d = Delay_ms(lambda led: led.on(), (red,)) -pb.press_func(d.trigger, (3000,)) # Note how function and args are passed +d = Delay_ms(callback, ('Callback running',)) try: - asyncio.run(my_app()) # Run main application code + asyncio.run(my_app()) finally: asyncio.new_event_loop() # Clear retained state ``` From 089a1c7098958534bb62b08e9514d9637f5364d0 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 9 Jun 2020 12:40:15 +0100 Subject: [PATCH 005/305] primitives/delay_ms Enable access to value returned from callable. --- v3/docs/TUTORIAL.md | 15 +++-- v3/primitives/delay_ms.py | 14 +++-- v3/primitives/tests/delay_test.py | 91 +++++++++++++++++++------------ 3 files changed, 73 insertions(+), 47 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index efe5d72..c02b6f7 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -984,15 +984,15 @@ as it is triggered before the time specified in the preceeding trigger it will never time out. If it does time out the `running` state will revert to `False`. This can be -interrogated by the object's `running()` method. In addition a **function** can -be specified to the constructor. This will execute when a timeout occurs; where -the **function** is a coroutine it will be converted to a `Task` and run -asynchronously. +interrogated by the object's `running()` method. In addition a `callable` can +be specified to the constructor. A `callable` can be a callback or a coroutine. +A callback will execute when a timeout occurs; where the `callable` is a +coroutine it will be converted to a `Task` and run asynchronously. Constructor arguments (defaults in brackets): - 1. `func` The **function** to call on timeout (default `None`). - 2. `args` A tuple of arguments for the **function** (default `()`). + 1. `func` The `callable` to call on timeout (default `None`). + 2. `args` A tuple of arguments for the `callable` (default `()`). 3. `can_alloc` Unused arg, retained to avoid breaking code. 4. `duration` Integer, default 1000ms. The default timer period where no value is passed to the `trigger` method. @@ -1008,6 +1008,9 @@ Methods: `False`. The timer can be restarted by issuing `trigger` again. 3. `running` No argument. Returns the running status of the object. 4. `__call__` Alias for running. + 5. `rvalue` No argument. If a timeout has occurred and a callback has run, + returns the return value of the callback. If a coroutine was passed, returns + the `Task` instance. This allows the `Task` to be cancelled or awaited. In this example a `Delay_ms` instance is created with the default duration of 1s. It is repeatedly triggered for 5 secs, preventing the callback from diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py index 34dc917..7424335 100644 --- a/v3/primitives/delay_ms.py +++ b/v3/primitives/delay_ms.py @@ -12,8 +12,7 @@ # from primitives.delay_ms import Delay_ms class Delay_ms: - verbose = False - # can_alloc retained to avoid breaking code. Now unsed. + verbose = False # verbose and can_alloc retained to avoid breaking code. def __init__(self, func=None, args=(), can_alloc=True, duration=1000): self._func = func self._args = args @@ -21,6 +20,7 @@ def __init__(self, func=None, args=(), can_alloc=True, duration=1000): self._tstop = None # Stop time (ms). None signifies not running. self._tsave = None # Temporary storage for stop time self._ktask = None # timer task + self._retrn = None # Return value of launched callable self._do_trig = self._trig # Avoid allocation in .trigger def stop(self): @@ -31,6 +31,7 @@ def trigger(self, duration=0): # Update end time now = ticks_ms() if duration <= 0: # Use default set by constructor duration = self._duration + self._retrn = None is_running = self() tstop = self._tstop # Current stop time # Retriggering normally just updates ._tstop for ._timer @@ -50,6 +51,9 @@ def __call__(self): # Current running status running = __call__ + def rvalue(self): + return self._retrn + async def _timer(self, restart): if restart: # Restore cached end time self._tstop = self._tsave @@ -58,10 +62,8 @@ async def _timer(self, restart): while twait > 0: # Must loop here: might be retriggered await asyncio.sleep_ms(twait) twait = ticks_diff(self._tstop, ticks_ms()) - if self._func is not None: - launch(self._func, self._args) # Timed out: execute callback + if self._func is not None: # Timed out: execute callback + self._retrn = launch(self._func, self._args) finally: self._tsave = self._tstop # Save in case we restart. self._tstop = None # timer is stopped - -# TODO launch returns the Task: make this available? diff --git a/v3/primitives/tests/delay_test.py b/v3/primitives/tests/delay_test.py index d7d0464..b007f01 100644 --- a/v3/primitives/tests/delay_test.py +++ b/v3/primitives/tests/delay_test.py @@ -21,12 +21,12 @@ def printexp(exp, runtime=0): async def ctor_test(): # Constructor arg s = ''' - Trigger 5 sec delay - Retrigger 5 sec delay - Callback should run - cb callback - Done - ''' +Trigger 5 sec delay +Retrigger 5 sec delay +Callback should run +cb callback +Done +''' printexp(s, 12) def cb(v): print('cb', v) @@ -45,36 +45,54 @@ def cb(v): async def launch_test(): s = ''' - Trigger 5 sec delay - Coroutine should run - Coroutine starts - Coroutine ends - Done - ''' - printexp(s, 7) - async def cb(v): +Trigger 5 sec delay +Coroutine should run: run to completion. +Coroutine starts +Coroutine ends +Coroutine should run: test cancellation. +Coroutine starts +Coroutine should run: test awaiting. +Coroutine starts +Coroutine ends +Done +''' + printexp(s, 20) + async def cb(v, ms): print(v, 'starts') - await asyncio.sleep(1) + await asyncio.sleep_ms(ms) print(v, 'ends') - d = Delay_ms(cb, ('coroutine',)) + d = Delay_ms(cb, ('coroutine', 1000)) print('Trigger 5 sec delay') d.trigger(5000) # Test extending time await asyncio.sleep(4) - print('Coroutine should run') + print('Coroutine should run: run to completion.') await asyncio.sleep(3) + d = Delay_ms(cb, ('coroutine', 3000)) + d.trigger(5000) + await asyncio.sleep(4) + print('Coroutine should run: test cancellation.') + await asyncio.sleep(2) + coro = d.rvalue() + coro.cancel() + d.trigger(5000) + await asyncio.sleep(4) + print('Coroutine should run: test awaiting.') + await asyncio.sleep(2) + coro = d.rvalue() + await coro print('Done') async def reduce_test(): # Test reducing a running delay s = ''' - Trigger 5 sec delay - Callback should run - cb callback - Callback should run - Done - ''' +Trigger 5 sec delay +Callback should run +cb callback +Callback should run +Done +''' printexp(s, 11) def cb(v): print('cb', v) @@ -97,16 +115,18 @@ def cb(v): async def stop_test(): # Test the .stop and .running methods s = ''' - Trigger 5 sec delay - Running - Callback should run - cb callback - Callback should not run - Done +Trigger 5 sec delay +Running +Callback should run +cb callback +Callback returned 42 +Callback should not run +Done ''' printexp(s, 12) def cb(v): print('cb', v) + return 42 d = Delay_ms(cb, ('callback',)) @@ -117,6 +137,7 @@ def cb(v): print('Running') print('Callback should run') await asyncio.sleep(2) + print('Callback returned', d.rvalue()) d.trigger(3000) await asyncio.sleep(1) d.stop() @@ -131,11 +152,11 @@ def cb(v): async def isr_test(): # Test trigger from hard ISR from pyb import Timer s = ''' - Timer holds off cb for 5 secs - cb should now run - cb callback - Done - ''' +Timer holds off cb for 5 secs +cb should now run +cb callback +Done +''' printexp(s, 6) def cb(v): print('cb', v) @@ -159,7 +180,7 @@ def timer_cb(_): where n is a test number. Avaliable tests: \x1b[32m 0 Test triggering from a hard ISR (Pyboard only) -1 Test the .stop method +1 Test the .stop method and callback return value. 2 Test reducing the duration of a running timer 3 Test delay defined by constructor arg 4 Test triggering a Task From 3a6b718cbb695ecd1c432060b121c1095c553e06 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 12 Jun 2020 06:54:14 +0100 Subject: [PATCH 006/305] V3 Tutorial: improve Barrier docs. --- v3/docs/TUTORIAL.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index c02b6f7..2d5d171 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -896,13 +896,13 @@ tested in slow time by the task. ## 3.7 Barrier -I implemented this unofficial primitive before `uasyncio` had support for -`gather`. It is based on a Microsoft primitive. In most cases `gather` is to be -preferred as its implementation is more efficient. +This is an unofficial primitive and has no analog in CPython asyncio. It is +based on a Microsoft primitive. While similar in purpose to `gather` there +are differences described below. -It two uses. Firstly it can cause a task to pause until one or more other tasks +It two uses. Firstly it can allow a task to pause until one or more other tasks have terminated. For example an application might want to shut down various -peripherals before issuing a sleep period. The task wanting to sleep initiates +peripherals before starting a sleep period. The task wanting to sleep initiates several shut down tasks and waits until they have triggered the barrier to indicate completion. @@ -912,6 +912,17 @@ producer has data available and the consumer is ready to use it. At that point in time the `Barrier` can optionally run a callback before releasing the barrier to allow all waiting coros to continue. +The key difference between `Barrier` and `gather` is symmetry: `gather` is +asymmetrical. One task owns the `gather` and awaits completion of a set of +tasks. By contrast `Barrier` can be used symmetrically with member tasks +pausing until all have reached the barrier. This makes it suited for use in +the looping constructs common in firmware applications. + +`gather` provides ready access to return values. The `Barrier` class cannot +because passing the barrier does not imply return. + +Currently `gather` is more efficient; I plan to fix this. + Constructor. Mandatory arg: * `participants` The number of coros which will use the barrier. @@ -925,9 +936,9 @@ Public synchronous methods: * `trigger` No args. The barrier records that the coro has passed the critical point. Returns "immediately". -The callback can be a function or a coro. In most applications a function will -be used as this can be guaranteed to run to completion beore the barrier is -released. +The callback can be a function or a coro. Typically a function will be used; it +must run to completion beore the barrier is released. A coro will be promoted +to a `Task` and run asynchronously. Participant coros issue `await my_barrier` whereupon execution pauses until all other participants are also waiting on it. At this point any callback will run From 9e11d1fd0310c65cc2ced800c19af1abb489c74a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 12 Jun 2020 14:34:05 +0100 Subject: [PATCH 007/305] V3 Barrier primitive: access callback return value, improve docs. --- v3/docs/TUTORIAL.md | 11 ++++-- v3/primitives/barrier.py | 18 +++++----- v3/primitives/tests/asyntest.py | 62 ++++++++++++++++++++++++++++----- 3 files changed, 73 insertions(+), 18 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 2d5d171..c3362f2 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -927,7 +927,7 @@ Constructor. Mandatory arg: * `participants` The number of coros which will use the barrier. Optional args: - * `func` Callback to run. Default `None`. + * `func` Callback or coroutine to run. Default `None`. * `args` Tuple of args for the callback. Default `()`. Public synchronous methods: @@ -935,10 +935,17 @@ Public synchronous methods: barrier, or if at least one non-waiting coro has not triggered it. * `trigger` No args. The barrier records that the coro has passed the critical point. Returns "immediately". + * `result` No args. If a callback was provided, returns the return value from + the callback. If a coro, returns the `Task` instance. See below. The callback can be a function or a coro. Typically a function will be used; it must run to completion beore the barrier is released. A coro will be promoted -to a `Task` and run asynchronously. +to a `Task` and run asynchronously. The `Task` may be retrieved (e.g. for +cancellation) using the `result` method. + +If a coro waits on a barrier, it should issue an `await` prior to accessing the +`result` method. To guarantee that the callback has run it is necessary to wait +until all participant coros have passed the barrier. Participant coros issue `await my_barrier` whereupon execution pauses until all other participants are also waiting on it. At this point any callback will run diff --git a/v3/primitives/barrier.py b/v3/primitives/barrier.py index 6f126e8..000a229 100644 --- a/v3/primitives/barrier.py +++ b/v3/primitives/barrier.py @@ -27,29 +27,31 @@ def __init__(self, participants, func=None, args=()): self._func = func self._args = args self._reset(True) + self._res = None def __await__(self): - self._update() - if self._at_limit(): # All other threads are also at limit - if self._func is not None: - launch(self._func, self._args) - self._reset(not self._down) # Toggle direction to release others + if self.trigger(): return direction = self._down - while True: # Wait until last waiting thread changes the direction + while True: # Wait until last waiting task changes the direction if direction != self._down: return await asyncio.sleep_ms(0) __iter__ = __await__ + def result(self): + return self._res + def trigger(self): self._update() - if self._at_limit(): # All other threads are also at limit + if self._at_limit(): # All other tasks are also at limit if self._func is not None: - launch(self._func, self._args) + self._res = launch(self._func, self._args) self._reset(not self._down) # Toggle direction to release others + return True + return False def _reset(self, down): self._down = down diff --git a/v3/primitives/tests/asyntest.py b/v3/primitives/tests/asyntest.py index a40d0ff..c827a97 100644 --- a/v3/primitives/tests/asyntest.py +++ b/v3/primitives/tests/asyntest.py @@ -24,11 +24,12 @@ def print_tests(): test(0) Print this list. test(1) Test message acknowledge. test(2) Test Messge and Lock objects. -test(3) Test the Barrier class. -test(4) Test Semaphore -test(5) Test BoundedSemaphore. -test(6) Test the Condition class. -test(7) Test the Queue class. +test(3) Test the Barrier class with callback. +test(4) Test the Barrier class with coroutine. +test(5) Test Semaphore +test(6) Test BoundedSemaphore. +test(7) Test the Condition class. +test(8) Test the Queue class. ''' print('\x1b[32m') print(st) @@ -186,6 +187,49 @@ def barrier_test(): asyncio.create_task(report(barrier)) asyncio.run(killer(2)) +# ************ Barrier test 1 ************ + +async def my_coro(text): + try: + await asyncio.sleep_ms(0) + while True: + await asyncio.sleep(1) + print(text) + except asyncio.CancelledError: + print('my_coro was cancelled.') + +async def report1(barrier, x): + await asyncio.sleep(x) + print('report instance', x, 'waiting') + await barrier + print('report instance', x, 'done') + +async def bart(): + barrier = Barrier(4, my_coro, ('my_coro running',)) + for x in range(3): + asyncio.create_task(report1(barrier, x)) + await barrier + # Must yield before reading result(). Here we wait long enough for + await asyncio.sleep_ms(1500) # coro to print + barrier.result().cancel() + await asyncio.sleep(2) + +def barrier_test1(): + printexp('''Running (runtime = 5s): +report instance 0 waiting +report instance 1 waiting +report instance 2 waiting +report instance 2 done +report instance 1 done +report instance 0 done +my_coro running +my_coro was cancelled. + +Exact report instance done sequence may vary, but 3 instances should report +done before my_coro runs. +''', 5) + asyncio.run(bart()) + # ************ Semaphore test ************ async def run_sema(n, sema, barrier): @@ -373,12 +417,14 @@ def test(n): elif n == 3: barrier_test() # Test the Barrier class. elif n == 4: - semaphore_test(False) # Test Semaphore + barrier_test1() # Test the Barrier class. elif n == 5: - semaphore_test(True) # Test BoundedSemaphore. + semaphore_test(False) # Test Semaphore elif n == 6: - condition_test() # Test the Condition class. + semaphore_test(True) # Test BoundedSemaphore. elif n == 7: + condition_test() # Test the Condition class. + elif n == 8: queue_test() # Test the Queue class. except KeyboardInterrupt: print('Interrupted') From 53c368d298f145794314499fcd69236dba38d938 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 12 Jun 2020 14:56:55 +0100 Subject: [PATCH 008/305] V3 Barrier primitive: access callback return value, improve docs. --- v3/docs/TUTORIAL.md | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index c3362f2..91a6a6f 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -896,32 +896,34 @@ tested in slow time by the task. ## 3.7 Barrier -This is an unofficial primitive and has no analog in CPython asyncio. It is -based on a Microsoft primitive. While similar in purpose to `gather` there +This is an unofficial primitive and has no counterpart in CPython asyncio. It +is based on a Microsoft primitive. While similar in purpose to `gather` there are differences described below. -It two uses. Firstly it can allow a task to pause until one or more other tasks -have terminated. For example an application might want to shut down various -peripherals before starting a sleep period. The task wanting to sleep initiates -several shut down tasks and waits until they have triggered the barrier to -indicate completion. - -Secondly it enables multiple coros to rendezvous at a particular point. For -example producer and consumer coros can synchronise at a point where the -producer has data available and the consumer is ready to use it. At that point -in time the `Barrier` can optionally run a callback before releasing the +Its principal purpose is to cause multiple coros to rendezvous at a particular +point. For example producer and consumer coros can synchronise at a point where +the producer has data available and the consumer is ready to use it. At that +point in time the `Barrier` can optionally run a callback before releasing the barrier to allow all waiting coros to continue. +Secondly it can allow a task to pause until one or more other tasks have +terminated or passed a particular point. For example an application might want +to shut down various peripherals before starting a sleep period. The task +wanting to sleep initiates several shut down tasks and waits until they have +triggered the barrier to indicate completion. This use case may be better +served by `gather`. + The key difference between `Barrier` and `gather` is symmetry: `gather` is asymmetrical. One task owns the `gather` and awaits completion of a set of tasks. By contrast `Barrier` can be used symmetrically with member tasks pausing until all have reached the barrier. This makes it suited for use in -the looping constructs common in firmware applications. +the `while True:` constructs common in firmware applications. Use of `gather` +would imply instantiating a set of tasks on every pass of the loop. -`gather` provides ready access to return values. The `Barrier` class cannot -because passing the barrier does not imply return. +`gather` provides access to return values; irrelevant to `Barrier` because +passing a barrier does not imply return. -Currently `gather` is more efficient; I plan to fix this. +Currently `gather` is more efficient. Constructor. Mandatory arg: From 2ac972ddef3d5afe5c7daa4e48de84eeb2a2aa2c Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 13 Jun 2020 07:45:27 +0100 Subject: [PATCH 009/305] V3 primitives replace polling with Event. --- v3/docs/TUTORIAL.md | 15 +++++++-------- v3/primitives/barrier.py | 23 +++++++++-------------- v3/primitives/queue.py | 14 ++++++++++---- v3/primitives/semaphore.py | 12 +++++++++--- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 91a6a6f..bbe8d65 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -500,8 +500,8 @@ target. A primitive is loaded by issuing (for example): from primitives.semaphore import Semaphore, BoundedSemaphore from primitives.queue import Queue ``` -When `uasyncio` acquires an official version (which will be more efficient) the -invocation lines alone should be changed: +When `uasyncio` acquires official versions of the CPython primitives the +invocation lines alone should be changed. e.g. : ```python from uasyncio import Semaphore, BoundedSemaphore from uasyncio import Queue @@ -848,7 +848,7 @@ asyncio.run(queue_go(4)) ## 3.6 Message -This is an unofficial primitive and has no analog in CPython asyncio. +This is an unofficial primitive and has no counterpart in CPython asyncio. This is a minor adaptation of the `Event` class. It provides the following: * `.set()` has an optional data payload. @@ -910,8 +910,8 @@ Secondly it can allow a task to pause until one or more other tasks have terminated or passed a particular point. For example an application might want to shut down various peripherals before starting a sleep period. The task wanting to sleep initiates several shut down tasks and waits until they have -triggered the barrier to indicate completion. This use case may be better -served by `gather`. +triggered the barrier to indicate completion. This use case may also be served +by `gather`. The key difference between `Barrier` and `gather` is symmetry: `gather` is asymmetrical. One task owns the `gather` and awaits completion of a set of @@ -921,9 +921,8 @@ the `while True:` constructs common in firmware applications. Use of `gather` would imply instantiating a set of tasks on every pass of the loop. `gather` provides access to return values; irrelevant to `Barrier` because -passing a barrier does not imply return. - -Currently `gather` is more efficient. +passing a barrier does not imply return. `Barrier` now has an efficient +implementation using `Event` to suspend waiting tasks. Constructor. Mandatory arg: diff --git a/v3/primitives/barrier.py b/v3/primitives/barrier.py index 000a229..70b3b81 100644 --- a/v3/primitives/barrier.py +++ b/v3/primitives/barrier.py @@ -2,6 +2,8 @@ # Copyright (c) 2018-2020 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file +# Now uses Event rather than polling. + try: import uasyncio as asyncio except ImportError: @@ -14,13 +16,6 @@ # At that point the callback is executed. Then the barrier is 'opened' and # execution of all participants resumes. -# The nowait arg is to support task cancellation. It enables usage where one or -# more coros can register that they have reached the barrier without waiting -# for it. Any coros waiting normally on the barrier will pause until all -# non-waiting coros have passed the barrier and all waiting ones have reached -# it. The use of nowait promotes efficiency by enabling tasks which have been -# cancelled to leave the task queue as soon as possible. - class Barrier(): def __init__(self, participants, func=None, args=()): self._participants = participants @@ -28,6 +23,7 @@ def __init__(self, participants, func=None, args=()): self._args = args self._reset(True) self._res = None + self._evt = asyncio.Event() def __await__(self): if self.trigger(): @@ -37,7 +33,8 @@ def __await__(self): while True: # Wait until last waiting task changes the direction if direction != self._down: return - await asyncio.sleep_ms(0) + await self._evt.wait() + self._evt.clear() __iter__ = __await__ @@ -45,7 +42,10 @@ def result(self): return self._res def trigger(self): - self._update() + self._count += -1 if self._down else 1 + if self._count < 0 or self._count > self._participants: + raise ValueError('Too many tasks accessing Barrier') + self._evt.set() if self._at_limit(): # All other tasks are also at limit if self._func is not None: self._res = launch(self._func, self._args) @@ -67,8 +67,3 @@ def busy(self): def _at_limit(self): # Has count reached up or down limit? limit = 0 if self._down else self._participants return self._count == limit - - def _update(self): - self._count += -1 if self._down else 1 - if self._count < 0 or self._count > self._participants: - raise ValueError('Too many tasks accessing Barrier') diff --git a/v3/primitives/queue.py b/v3/primitives/queue.py index a4e124b..123c778 100644 --- a/v3/primitives/queue.py +++ b/v3/primitives/queue.py @@ -23,14 +23,18 @@ class Queue: def __init__(self, maxsize=0): self.maxsize = maxsize self._queue = [] + self._evput = asyncio.Event() # Triggered by put, tested by get + self._evget = asyncio.Event() # Triggered by get, tested by put def _get(self): + self._evget.set() return self._queue.pop(0) async def get(self): # Usage: item = await queue.get() - while self.empty(): + if self.empty(): # Queue is empty, put the calling Task on the waiting queue - await asyncio.sleep_ms(0) + await self._evput.wait() + self._evput.clear() return self._get() def get_nowait(self): # Remove and return an item from the queue. @@ -40,12 +44,14 @@ def get_nowait(self): # Remove and return an item from the queue. return self._get() def _put(self, val): + self._evput.set() self._queue.append(val) async def put(self, val): # Usage: await queue.put(item) - while self.qsize() >= self.maxsize and self.maxsize: + if self.qsize() >= self.maxsize and self.maxsize: # Queue full - await asyncio.sleep_ms(0) + await self._evget.wait() + self._evget.clear() # Task(s) waiting to get from queue, schedule first Task self._put(val) diff --git a/v3/primitives/semaphore.py b/v3/primitives/semaphore.py index ccb1170..19b82e4 100644 --- a/v3/primitives/semaphore.py +++ b/v3/primitives/semaphore.py @@ -13,6 +13,7 @@ class Semaphore(): def __init__(self, value=1): self._count = value + self._event = asyncio.Event() async def __aenter__(self): await self.acquire() @@ -23,11 +24,16 @@ async def __aexit__(self, *args): await asyncio.sleep(0) async def acquire(self): - while self._count == 0: - await asyncio.sleep_ms(0) + self._event.clear() + while self._count == 0: # Multiple tasks may be waiting for + await self._event.wait() # a release + self._event.clear() + # When we yield, another task may succeed. In this case + await asyncio.sleep(0) # the loop repeats self._count -= 1 def release(self): + self._event.set() self._count += 1 class BoundedSemaphore(Semaphore): @@ -37,6 +43,6 @@ def __init__(self, value=1): def release(self): if self._count < self._initial_value: - self._count += 1 + super().release() else: raise ValueError('Semaphore released more than acquired') From 482465b3e1e310e0d481e19b163719c6bb85e913 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 13 Jun 2020 08:08:28 +0100 Subject: [PATCH 010/305] V3 primitives replace polling with Event. --- v3/README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/v3/README.md b/v3/README.md index 78e6e26..2b939c1 100644 --- a/v3/README.md +++ b/v3/README.md @@ -1,8 +1,9 @@ # 1. Guide to uasyncio V3 The new release of `uasyncio` is pre-installed in current daily firmware -builds. This complete rewrite of `uasyncio` supports CPython 3.8 syntax. A -design aim is that it should be be a compatible subset of `asyncio`. +builds and will be found in release builds starting with V1.13. This complete +rewrite of `uasyncio` supports CPython 3.8 syntax. A design aim is that it +should be be a compatible subset of `asyncio`. These notes and the tutorial should be read in conjunction with [the official docs](http://docs.micropython.org/en/latest/library/uasyncio.html) @@ -121,15 +122,15 @@ The CPython `asyncio` library supports these synchronisation primitives: * `Condition`. In this repository. * `Queue`. In this repository. -I am hoping that the above will be replaced by more efficient official built-in -versions. To date those listed as "already incorporated" have been and should -be used. +The above unofficial primitives are CPython compatible. Using future official +versions will require a change to the import statement only. ### 3.2.2 Synchronisation primitives (old asyn.py) Applications using `asyn.py` should no longer import that module. Equivalent functionality may now be found in the `primitives` directory: this is -implemented as a Python package enabling RAM savings. +implemented as a Python package enabling RAM savings. The new versions are also +more efficient, replacing polling with the new `Event` class. These features in `asyn.py` were workrounds for bugs in V2 and should not be used with V3: From 5573c353f0b61f9b0b57b67072bf6fbd1e9d6f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Mendon=C3=A7a=20Ferreira?= Date: Mon, 15 Jun 2020 19:49:25 -0300 Subject: [PATCH 011/305] Use correct variable --- roundrobin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roundrobin.py b/roundrobin.py index 5aefae1..0bb8b0d 100644 --- a/roundrobin.py +++ b/roundrobin.py @@ -25,7 +25,7 @@ async def foo(n): async def main(delay): - print('Testing for {} seconds'.format(period)) + print('Testing for {} seconds'.format(delay)) await asyncio.sleep(delay) From f0971bbbe6e9b19939fe09a5cda94f66a6c32b02 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 16 Jun 2020 03:38:30 +0100 Subject: [PATCH 012/305] Fix V3 roundrobin.py demo. --- v3/as_demos/roundrobin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/as_demos/roundrobin.py b/v3/as_demos/roundrobin.py index 35a79cd..5a82bb8 100644 --- a/v3/as_demos/roundrobin.py +++ b/v3/as_demos/roundrobin.py @@ -26,7 +26,7 @@ async def foo(n): async def main(delay): for n in range(1, 4): asyncio.create_task(foo(n)) - print('Testing for {} seconds'.format(period)) + print('Testing for {:d} seconds'.format(delay)) await asyncio.sleep(delay) From a342eb03c416134db8aa28d6bc8dcd4eaccdf8ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Mendon=C3=A7a=20Ferreira?= Date: Tue, 16 Jun 2020 19:22:27 -0300 Subject: [PATCH 013/305] Fixed section 1 links, added a few more. Added Primitives, Demo programs and Device drivers to the Contents list. Fixed wrong links to demo programs. Added one new Demo program and two Device drivers links. --- v3/docs/TUTORIAL.md | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index bbe8d65..fd492d1 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -13,6 +13,9 @@ REPL. 0.1 [Installing uasyncio on bare metal](./TUTORIAL.md#01-installing-uasyncio-on-bare-metal) 1. [Cooperative scheduling](./TUTORIAL.md#1-cooperative-scheduling) 1.1 [Modules](./TUTORIAL.md#11-modules) + 1.1.1 [Primitives](./TUTORIAL.md#111-primitives) + 1.1.2 [Demo programs](./TUTORIAL.md#112-demo-programs) + 1.1.3 [Device drivers](./TUTORIAL.md#113-device-drivers) 2. [uasyncio](./TUTORIAL.md#2-uasyncio) 2.1 [Program structure](./TUTORIAL.md#21-program-structure) 2.2 [Coroutines and Tasks](./TUTORIAL.md#22-coroutines-and-tasks) @@ -124,7 +127,7 @@ pitfalls associated with truly asynchronous threads of execution. ## 1.1 Modules -### Primitives +### 1.1.1 Primitives The directory `primitives` contains a Python package containing the following: * Synchronisation primitives: "micro" versions of CPython's classes. @@ -137,7 +140,7 @@ The directory `primitives` contains a Python package containing the following: To install this Python package copy the `primitives` directory tree and its contents to your hardware's filesystem. -### Demo Programs +### 1.1.2 Demo programs The directory `as_demos` contains various demo programs implemented as a Python package. Copy the directory and its contents to the target hardware. @@ -146,28 +149,29 @@ The first two are the most immediately rewarding as they produce visible results by accessing Pyboard hardware. With all demos, issue ctrl-d between runs to soft reset the hardware. - 1. [aledflash.py](./as_demos/aledflash.py) Flashes three Pyboard LEDs + 1. [aledflash.py](../as_demos/aledflash.py) Flashes three Pyboard LEDs asynchronously for 10s. Requires any Pyboard. - 2. [apoll.py](./as_demos/apoll.py) A device driver for the Pyboard + 2. [apoll.py](../as_demos/apoll.py) A device driver for the Pyboard accelerometer. Demonstrates the use of a task to poll a device. Runs for 20s. Requires a Pyboard V1.x. - 3. [roundrobin.py](./as_demos/roundrobin.py) Demo of round-robin scheduling. + 3. [roundrobin.py](../as_demos/roundrobin.py) Demo of round-robin scheduling. Also a benchmark of scheduling performance. Runs for 5s on any target. - 4. [auart.py](./as_demos/auart.py) Demo of streaming I/O via a Pyboard UART. + 4. [auart.py](../as_demos/auart.py) Demo of streaming I/O via a Pyboard UART. Requires a link between X1 and X2. - 5. [auart_hd.py](./as_demos/auart_hd.py) Use of the Pyboard UART to communicate + 5. [auart_hd.py](../as_demos/auart_hd.py) Use of the Pyboard UART to communicate with a device using a half-duplex protocol e.g. devices such as those using the 'AT' modem command set. Link X1-X4, X2-X3. - 6. [gather.py](./as_demos/gether.py) Use of `gather`. Any target. - 7. [iorw.py](./as_demos/iorw.py) Demo of a read/write device driver using the + 6. [gather.py](../as_demos/gather.py) Use of `gather`. Any target. + 7. [iorw.py](../as_demos/iorw.py) Demo of a read/write device driver using the stream I/O mechanism. Requires a Pyboard. + 8. [rate.py](../as_demos/rate.py) Benchmark for uasyncio. Any target. Demos are run using this pattern: ```python import as_demos.aledflash ``` -### Device drivers +### 1.1.3 Device drivers These are installed by copying the `as_drivers` directory and contents to the target. They have their own documentation as follows: @@ -177,8 +181,13 @@ target. They have their own documentation as follows: altitude and time/date information. 2. [HTU21D](./HTU21D.md) An I2C temperature and humidity sensor. A task periodically queries the sensor maintaining constantly available values. - 3. [NEC IR](./NEC_IR) A decoder for NEC IR remote controls. A callback occurs + 3. [NEC IR](./NEC_IR.md) A decoder for NEC IR remote controls. A callback occurs whenever a valid signal is received. + 4. [HD44780](./hd44780.md) Driver for common character based LCD displays + based on the Hitachi HD44780 controller + 5. [I2C](./I2C.md) Use Pyboard I2C slave mode to implement a UART-like + asynchronous stream interface. Uses: communication with ESP8266, + or (with coding) interface a Pyboard to I2C masters. ###### [Contents](./TUTORIAL.md#contents) From 43d1e4f241a2278da2318a4b5cb7b6754ff7ce90 Mon Sep 17 00:00:00 2001 From: Andy Hobbs <863877+andydhobbs@users.noreply.github.com> Date: Sun, 28 Jun 2020 15:53:22 +0100 Subject: [PATCH 014/305] Allow for buttons default state to be passing in the constructor Using the state of a switch at initialisation can lead to spurious results. Added a default state to the class constructor to allow for a defined default state --- v3/primitives/pushbutton.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index 2c6b551..abe438c 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -15,7 +15,7 @@ class Pushbutton: debounce_ms = 50 long_press_ms = 1000 double_click_ms = 400 - def __init__(self, pin, suppress=False): + def __init__(self, pin, suppress=False, sense=None): self.pin = pin # Initialise for input self._supp = suppress self._dblpend = False # Doubleclick waiting for 2nd click @@ -26,7 +26,7 @@ def __init__(self, pin, suppress=False): self._lf = False self._ld = False # Delay_ms instance for long press self._dd = False # Ditto for doubleclick - self.sense = pin.value() # Convert from electrical to logical value + self.sense = pin.value() if sense is None else sense # Convert from electrical to logical value self.state = self.rawstate() # Initial state asyncio.create_task(self.buttoncheck()) # Thread runs forever From 0af77fbff07e692bdd37135fdbbfa8b04021b87c Mon Sep 17 00:00:00 2001 From: Andy Hobbs <863877+andydhobbs@users.noreply.github.com> Date: Sun, 28 Jun 2020 15:53:22 +0100 Subject: [PATCH 015/305] Allow for buttons default state to be passing in the constructor Using the state of a switch at initialisation can lead to spurious results. Added a default state to the class constructor to allow for a defined default state --- v3/docs/DRIVERS.md | 15 +++++++++++++-- v3/primitives/pushbutton.py | 4 ++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 7d17c23..3264c1c 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -112,8 +112,9 @@ on press, release, double-click or long press events. This can support normally open or normally closed switches, connected to `gnd` (with a pullup) or to `3V3` (with a pull-down). The `Pin` object should be -initialised appropriately. The assumption is that on instantiation the button -is not pressed. +initialised appropriately. The default state of the switch can be passed in the +optional "sense" parameter on the constructor, otherwise the assumption is that +on instantiation the button is not pressed. The Pushbutton class uses logical rather than physical state: a button's state is considered `True` if pressed, otherwise `False` regardless of its physical @@ -130,6 +131,8 @@ Constructor arguments: 1. `pin` Mandatory. The initialised Pin instance. 2. `suppress` Default `False`. See [section 4.1.1](./DRIVERS.md#411-the-suppress-constructor-argument). + 3. `sense` Default `None`. See + [section 4.1.1](./DRIVERS.md#412-the-sense-constructor-argument). Methods: @@ -198,6 +201,14 @@ set, `release_func` will be launched as follows: 4. If `double_func` exists and a double click occurs, `release_func` will not be launched. +### 4.1.2 The sense constructor argument + +When the pin value changes, the new value is compared with `sense` to determine +if the button is closed or open. This is to allow the designer to specify if +the `closed` state of the button is active `high` or active `low`. + +This parameter will default to the current value of `pin` for convienence. + # 5. primitives.aadc diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index 2c6b551..abe438c 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -15,7 +15,7 @@ class Pushbutton: debounce_ms = 50 long_press_ms = 1000 double_click_ms = 400 - def __init__(self, pin, suppress=False): + def __init__(self, pin, suppress=False, sense=None): self.pin = pin # Initialise for input self._supp = suppress self._dblpend = False # Doubleclick waiting for 2nd click @@ -26,7 +26,7 @@ def __init__(self, pin, suppress=False): self._lf = False self._ld = False # Delay_ms instance for long press self._dd = False # Ditto for doubleclick - self.sense = pin.value() # Convert from electrical to logical value + self.sense = pin.value() if sense is None else sense # Convert from electrical to logical value self.state = self.rawstate() # Initial state asyncio.create_task(self.buttoncheck()) # Thread runs forever From c7c76b2f0b36a9f58a81d49a2f49f1a8e4bf44d3 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 30 Jun 2020 10:13:23 +0100 Subject: [PATCH 016/305] Improve V3 porting guide. --- README.md | 4 ++++ v3/README.md | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/README.md b/README.md index ef04c99..1ac319a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ ensure your firmware build is official MicroPython V1.12 and follow the `uasyncio` installation instructions in [the V2 tutorial](./TUTORIAL.md). For V3, install the latest daily build which includes `uasyncio`. +I strongly recommend V3 unless you need the `fast_io` variant of V2. When V3 +acquires this ability (it is planned) and appears in a release build I expect +to obsolete all V2 material in this repo. + Resources for V3 and an updated tutorial may be found in the v3 directory. ### [Go to V3 docs](./v3/README.md) diff --git a/v3/README.md b/v3/README.md index 2b939c1..24eb4f3 100644 --- a/v3/README.md +++ b/v3/README.md @@ -103,6 +103,32 @@ MicroPython and CPython 3.8. This is discussed Classes based on `uio.IOBase` will need changes to the `write` method. See [tutorial](./docs/TUTORIAL.md#64-writing-streaming-device-drivers). +### 3.1.3 Early task creation + +It is [bad practice](https://github.com/micropython/micropython/issues/6174) +to create tasks before issuing `asyncio.run()`. CPython 3.8 throws if you do. +Such code can be ported by wrapping functions that create tasks in a +coroutine as below. + +There is a subtlety affecting code that creates tasks early: +`loop.run_forever()` did just that, never returning and scheduling all created +tasks. By contrast `asyncio.run(coro())` terminates when the coro does. Typical +firmware applications run forever so the coroutine started by `.run()` must +`await` a continuously running task. This may imply exposing an asynchronous +method which runs forever: + +```python +async def main(): + obj = MyObject() # Constructor creates tasks + await obj.run_forever() # Never terminates + +def run(): # Entry point + try: + asyncio.run(main()) + finally: + asyncio.new_event_loop() +``` + ## 3.2 Modules from this repository Modules `asyn.py` and `aswitch.py` are deprecated for V3 applications. See From c62ba242ae5d3a8efe4d6a2ad3c52635d6dfc9f3 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 2 Jul 2020 09:46:27 +0100 Subject: [PATCH 017/305] V3 tutorial: note re premature task creation. --- v3/docs/TUTORIAL.md | 61 ++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index fd492d1..69ffe66 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -13,22 +13,23 @@ REPL. 0.1 [Installing uasyncio on bare metal](./TUTORIAL.md#01-installing-uasyncio-on-bare-metal) 1. [Cooperative scheduling](./TUTORIAL.md#1-cooperative-scheduling) 1.1 [Modules](./TUTORIAL.md#11-modules) - 1.1.1 [Primitives](./TUTORIAL.md#111-primitives) - 1.1.2 [Demo programs](./TUTORIAL.md#112-demo-programs) - 1.1.3 [Device drivers](./TUTORIAL.md#113-device-drivers) +      1.1.1 [Primitives](./TUTORIAL.md#111-primitives) +      1.1.2 [Demo programs](./TUTORIAL.md#112-demo-programs) +      1.1.3 [Device drivers](./TUTORIAL.md#113-device-drivers) 2. [uasyncio](./TUTORIAL.md#2-uasyncio) 2.1 [Program structure](./TUTORIAL.md#21-program-structure) 2.2 [Coroutines and Tasks](./TUTORIAL.md#22-coroutines-and-tasks) - 2.2.1 [Queueing a task for scheduling](./TUTORIAL.md#221-queueing-a-task-for-scheduling) - 2.2.2 [Running a callback function](./TUTORIAL.md#222-running-a-callback-function) - 2.2.3 [Notes](./TUTORIAL.md#223-notes) Coros as bound methods. Returning values. +      2.2.1 [Queueing a task for scheduling](./TUTORIAL.md#221-queueing-a-task-for-scheduling) +      2.2.2 [Running a callback function](./TUTORIAL.md#222-running-a-callback-function) +      2.2.3 [Notes](./TUTORIAL.md#223-notes) Coros as bound methods. Returning values. +      2.2.4 [A typical firmware app](./TUTORIAL.md#224-a-typical-firmware-app) Avoiding a minor error 2.3 [Delays](./TUTORIAL.md#23-delays) 3. [Synchronisation](./TUTORIAL.md#3-synchronisation) 3.1 [Lock](./TUTORIAL.md#31-lock) 3.2 [Event](./TUTORIAL.md#32-event) 3.3 [gather](./TUTORIAL.md#33-gather) 3.4 [Semaphore](./TUTORIAL.md#34-semaphore) - 3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore) +      3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore) 3.5 [Queue](./TUTORIAL.md#35-queue) 3.6 [Message](./TUTORIAL.md#36-message) 3.7 [Barrier](./TUTORIAL.md#37-barrier) @@ -37,23 +38,23 @@ REPL. Debouncing switches and pushbuttons. Taming ADC's. 4. [Designing classes for asyncio](./TUTORIAL.md#4-designing-classes-for-asyncio) 4.1 [Awaitable classes](./TUTORIAL.md#41-awaitable-classes) - 4.1.1 [Use in context managers](./TUTORIAL.md#411-use-in-context-managers) - 4.1.2 [Portable code](./TUTORIAL.md#412-portable-code) +      4.1.1 [Use in context managers](./TUTORIAL.md#411-use-in-context-managers) +      4.1.2 [Portable code](./TUTORIAL.md#412-portable-code) 4.2 [Asynchronous iterators](./TUTORIAL.md#42-asynchronous-iterators) 4.3 [Asynchronous context managers](./TUTORIAL.md#43-asynchronous-context-managers) 5. [Exceptions timeouts and cancellation](./TUTORIAL.md#5-exceptions-timeouts-and-cancellation) 5.1 [Exceptions](./TUTORIAL.md#51-exceptions) - 5.1.1 [Global exception handler](./TUTORIAL.md#511-global-exception-handler) - 5.1.2 [Keyboard interrupts](./TUTORIAL.md#512-keyboard-interrupts) +      5.1.1 [Global exception handler](./TUTORIAL.md#511-global-exception-handler) +      5.1.2 [Keyboard interrupts](./TUTORIAL.md#512-keyboard-interrupts) 5.2 [Cancellation and Timeouts](./TUTORIAL.md#52-cancellation-and-timeouts) - 5.2.1 [Task cancellation](./TUTORIAL.md#521-task-cancellation) - 5.2.2 [Tasks with timeouts](./TUTORIAL.md#522-tasks-with-timeouts) - 5.2.3 [Cancelling running tasks](./TUTORIAL.md#523-cancelling-running-tasks) A "gotcha". +      5.2.1 [Task cancellation](./TUTORIAL.md#521-task-cancellation) +      5.2.2 [Tasks with timeouts](./TUTORIAL.md#522-tasks-with-timeouts) +      5.2.3 [Cancelling running tasks](./TUTORIAL.md#523-cancelling-running-tasks) A "gotcha". 6. [Interfacing hardware](./TUTORIAL.md#6-interfacing-hardware) 6.1 [Timing issues](./TUTORIAL.md#61-timing-issues) 6.2 [Polling hardware with a task](./TUTORIAL.md#62-polling-hardware-with-a-task) 6.3 [Using the stream mechanism](./TUTORIAL.md#63-using-the-stream-mechanism) - 6.3.1 [A UART driver example](./TUTORIAL.md#631-a-uart-driver-example) +      6.3.1 [A UART driver example](./TUTORIAL.md#631-a-uart-driver-example) 6.4 [Writing streaming device drivers](./TUTORIAL.md#64-writing-streaming-device-drivers) 6.5 [A complete example: aremote.py](./TUTORIAL.md#65-a-complete-example-aremotepy) A driver for an IR remote control receiver. @@ -66,7 +67,7 @@ REPL. 7.4 [Testing](./TUTORIAL.md#74-testing) 7.5 [A common error](./TUTORIAL.md#75-a-common-error) This can be hard to find. 7.6 [Socket programming](./TUTORIAL.md#76-socket-programming) - 7.6.1 [WiFi issues](./TUTORIAL.md#761-wifi-issues) +      7.6.1 [WiFi issues](./TUTORIAL.md#761-wifi-issues) 7.7 [CPython compatibility and the event loop](./TUTORIAL.md#77-cpython-compatibility-and-the-event-loop) Compatibility with CPython 3.5+ 7.8 [Race conditions](./TUTORIAL.md#78-race-conditions) 8. [Notes for beginners](./TUTORIAL.md#8-notes-for-beginners) @@ -439,6 +440,32 @@ asyncio.run(main()) ###### [Contents](./TUTORIAL.md#contents) +### 2.2.4 A typical firmware app + +It is bad practice to create a task prior to issuing `asyncio.run()`. CPython +will throw an exception in this case. MicroPython +[does not](https://github.com/micropython/micropython/issues/6174) but it's +wise to avoid doing this. + +Most firmware applications run forever. For this to occur, the coro passed to +`asyncio.run()` must not terminate. This suggests the following application +structure: +```python +import uasyncio as asyncio +from my_app import MyClass + +async def main(): + my_class = MyClass() # Constructor might create tasks + asyncio.create_task(my_class.foo()) # Or you might do this + await my_class.run_forever() # Non-terminating method +try: + asyncio.run(main()) +finally: + asyncio.new_event_loop() # Clear retained state +``` + +###### [Contents](./TUTORIAL.md#contents) + ## 2.3 Delays Where a delay is required in a task there are two options. For longer delays and @@ -1049,6 +1076,7 @@ import uasyncio as asyncio from primitives.delay_ms import Delay_ms async def my_app(): + d = Delay_ms(callback, ('Callback running',)) print('Holding off callback') for _ in range(10): # Hold off for 5 secs await asyncio.sleep_ms(500) @@ -1060,7 +1088,6 @@ async def my_app(): def callback(v): print(v) -d = Delay_ms(callback, ('Callback running',)) try: asyncio.run(my_app()) finally: From 9b453dcbc9e16417308fc6f0333aed2e83310293 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 5 Jul 2020 15:00:05 +0100 Subject: [PATCH 018/305] Add primitives/set_global_exception. Tutorial updates. --- v3/docs/DRIVERS.md | 31 +++++++++++++++++ v3/docs/TUTORIAL.md | 70 +++++++++++++++++++++------------------ v3/primitives/__init__.py | 13 ++++++++ 3 files changed, 81 insertions(+), 33 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 7d17c23..59976e0 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -271,3 +271,34 @@ The `AADC` class uses the `uasyncio` stream I/O mechanism. This is not the most obvious design. It was chosen because the plan for `uasyncio` is that it will include an option for prioritising I/O. I wanted this class to be able to use this for applications requiring rapid response. + +# 6. Additional functions + +These comprise `launch` and `set_global_exception` imported as follows: +```python +from primitives import launch, set_global_exception +``` + +`launch` enables a function to accept a coro or a callback interchangeably. It +accepts the callable plus a tuple of args. If a callback is passed, `launch` +runs it and returns the callback's return value. If a coro is passed, it is +converted to a `task` and run asynchronously. The return value is the `task` +instance. A usage example is in `primitives/switch.py`. + +`set_global_exception` is a convenience funtion to enable a global exception +handler. This simplifies debugging. The function takes no args. It is called as +follows: + +```python +import uasyncio as asyncio +from primitives import set_global_exception + +async def main(): + set_global_exception() + # Main body of application code omitted + +try: + asyncio.run(main()) +finally: + asyncio.new_event_loop() # Clear retained state +``` diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 69ffe66..e3aa4d5 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -342,32 +342,13 @@ asyncio.run(main()) CPython the `run` call does not terminate. * `await` Arg: the task or coro to run. If a coro is passed it must be specified with function call syntax. Starts the task ASAP. The awaiting task - blocks until the awaited one has run to completion. + blocks until the awaited one has run to completion. As described + [in section 2.2](./TUTORIAL.md#22-coroutines-and-tasks) it is possible to + `await` a task which has already been started. In this instance the `await` is + on the `task` object (function call syntax is not used). The above are compatible with CPython 3.8 or above. -It is possible to `await` a task which has already been started: -```python -import uasyncio as asyncio -async def bar(x): - count = 0 - for _ in range(5): - count += 1 - print('Instance: {} count: {}'.format(x, count)) - await asyncio.sleep(1) # Pause 1s - -async def main(): - my_task = asyncio.create_task(bar(1)) - print('Task is running') - await asyncio.sleep(2) # Do something else - print('Awaiting task') - await my_task # If the task has already finished, this returns immediately - return 10 - -a = asyncio.run(main()) -print(a) -``` - ###### [Contents](./TUTORIAL.md#contents) ### 2.2.2 Running a callback function @@ -416,10 +397,11 @@ retrieve the returned data issue: result = await my_task() ``` -It is possible to await completion of multiple asynchronously running tasks, -accessing the return value of each. This is done by `uasyncio.gather` which -launches a number of tasks and pauses until the last terminates. It returns a -list containing the data returned by each task: +It is possible to await completion of a set of multiple asynchronously running +tasks, accessing the return value of each. This is done by +[uasyncio.gather](./TUTORIAL.md#33-gather) which launches the tasks and pauses +until the last terminates. It returns a list containing the data returned by +each task: ```python import uasyncio as asyncio @@ -442,19 +424,41 @@ asyncio.run(main()) ### 2.2.4 A typical firmware app +Most firmware applications run forever. This requires the coro passed to +`asyncio.run()` to `await` a non-terminating coro. + +To ease debugging, and for CPython compatibility, some "boilerplate" code is +suggested in the sample below. + +By default an exception in a task will not stop the application as a whole from +running. This can make debugging difficult. The fix shown below is discussed +[in 5.1.1](./TUTORIAL.md#511-global-exception-handler). + It is bad practice to create a task prior to issuing `asyncio.run()`. CPython will throw an exception in this case. MicroPython [does not](https://github.com/micropython/micropython/issues/6174) but it's wise to avoid doing this. -Most firmware applications run forever. For this to occur, the coro passed to -`asyncio.run()` must not terminate. This suggests the following application -structure: +Lastly, `uasyncio` retains state. This means that, by default, you need to +reboot between runs of an application. This can be fixed with the +`new_event_loop` method discussed +[in 7.2](./TUTORIAL.md#72-uasyncio-retains-state). + +These considerations suggest the following application structure: ```python import uasyncio as asyncio from my_app import MyClass +def set_global_exception(): + def handle_exception(loop, context): + import sys + sys.print_exception(context["exception"]) + sys.exit() + loop = asyncio.get_event_loop() + loop.set_exception_handler(handle_exception) + async def main(): + set_global_exception() # Debug aid my_class = MyClass() # Constructor might create tasks asyncio.create_task(my_class.foo()) # Or you might do this await my_class.run_forever() # Non-terminating method @@ -528,7 +532,7 @@ following classes which are non-standard, are also in that directory: Calls a user callback if not cancelled or regularly retriggered. A further set of primitives for synchronising hardware are detailed in -[section 3.9](./TUTORIAL.md#38-synchronising-to-hardware). +[section 3.9](./TUTORIAL.md#39-synchronising-to-hardware). To install the primitives, copy the `primitives` directory and contents to the target. A primitive is loaded by issuing (for example): @@ -888,8 +892,8 @@ This is an unofficial primitive and has no counterpart in CPython asyncio. This is a minor adaptation of the `Event` class. It provides the following: * `.set()` has an optional data payload. - * `.set()` is capable of being called from an interrupt service routine - a - feature not yet available in the more efficient official `Event`. + * `.set()` is capable of being called from a hard or soft interrupt service + routine - a feature not yet available in the more efficient official `Event`. * It is an awaitable class. The `.set()` method can accept an optional data value of any type. A task diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index 35d8264..0274fc2 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -1,3 +1,8 @@ +# __init__.py Common functions for uasyncio primitives + +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + try: import uasyncio as asyncio except ImportError: @@ -16,3 +21,11 @@ def launch(func, tup_args): if isinstance(res, type_coro): res = asyncio.create_task(res) return res + +def set_global_exception(): + def _handle_exception(loop, context): + import sys + sys.print_exception(context["exception"]) + sys.exit() + loop = asyncio.get_event_loop() + loop.set_exception_handler(_handle_exception) From 2ebb26e5a9aab8d732c79c1f4768f5e6a49683e6 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 7 Jul 2020 17:57:25 +0100 Subject: [PATCH 019/305] v3 DRIVERS.md add TOC --- v3/docs/DRIVERS.md | 84 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 59976e0..9ed69f5 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -1,4 +1,4 @@ -# 1. Introduction +# 0. Introduction Drivers for switches and pushbuttons are provided, plus a retriggerable delay class. The switch and button drivers support debouncing. The switch driver @@ -11,6 +11,22 @@ events. The asynchronous ADC supports pausing a task until the value read from an ADC goes outside defined bounds. +# 1. Contents + + 1. [Contents](./DRIVERS.md#1-contents) + 2. [Installation and usage](./DRIVERS.md#2-installation-and-usage) + 3. [Interfacing switches](./DRIVERS.md#3-interfacing-switches) Switch debouncer with callbacks. + 3.1 [Switch class](./DRIVERS.md#31-switch-class) + 4. [Interfacing pushbuttons](./DRIVERS.md#4-interfacing-pushbuttons) Extends Switch for long and double click events + 4.1 [Pushbutton class](./DRIVERS.md#41-pushbutton-class) +      4.1.1 [The suppress constructor argument](./DRIVERS.md#411-the-suppress-constructor-argument) + 5. [ADC monitoring](./DRIVERS.md#5-ADC monitoring) Pause until an ADC goes out of bounds + 5.1 [AADC class](./DRIVERS.md#51--aadc-class) + 5.2 [Design note](./DRIVERS.md#52-design-note) + 6. [Additional functions](./DRIVERS.md#6-additional-functions) + 6.1 [launch](./DRIVERS.md#61-launch) Run a coro or callback interchangeably + 6.2 [set_global_exception](,.DRIVERS.md#62-set_global_exception) Simplify debugging with a global exception handler + ###### [Tutorial](./TUTORIAL.md#contents) # 2. Installation and usage @@ -38,11 +54,13 @@ from primitives.tests.adctest import test test() ``` -# 3. primitives.switch +###### [Contents](./DRIVERS.md#1-contents) + +# 3. Interfacing switches -This module provides the `Switch` class. This supports debouncing a normally -open switch connected between a pin and ground. Can run callbacks or schedule -coros on contact closure and/or opening. +The `primitives.switch` module provides the `Switch` class. This supports +debouncing a normally open switch connected between a pin and ground. Can run +callbacks or schedule coros on contact closure and/or opening. In the following text the term `callable` implies a Python `callable`: namely a function, bound method, coroutine or bound coroutine. The term implies that any @@ -102,11 +120,14 @@ sw.close_func(pulse, (red, 1000)) # Note how coro and args are passed asyncio.run(my_app()) # Run main application code ``` -# 4. primitives.pushbutton +###### [Contents](./DRIVERS.md#1-contents) + +# 4. Interfacing pushbuttons -The `Pushbutton` class is generalisation of `Switch` to support normally open -or normally closed switches connected to ground or 3V3. Can run a `callable` on -on press, release, double-click or long press events. +The `primitives.pushbutton` module provides the `Pushbutton` class. This is a +generalisation of `Switch` to support normally open or normally closed switches +connected to ground or 3V3. Can run a `callable` on on press, release, +double-click or long press events. ## 4.1 Pushbutton class @@ -172,8 +193,10 @@ pb.press_func(toggle, (red,)) # Note how function and args are passed asyncio.run(my_app()) # Run main application code ``` -An alternative Pushbutton class with lower RAM usage is available -[here](https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py). +An alternative, compatible `Pushbutton` implementation is available +[here](https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py): +this implementation avoids the use of the `Delay_ms` class to minimise the +number of coroutines. ### 4.1.1 The suppress constructor argument @@ -198,15 +221,16 @@ set, `release_func` will be launched as follows: 4. If `double_func` exists and a double click occurs, `release_func` will not be launched. +###### [Contents](./DRIVERS.md#1-contents) -# 5. primitives.aadc +# 5. ADC monitoring -The `AADC` (asynchronous ADC) class provides for coroutines which pause until -the value returned by an ADC goes outside predefined bounds. The bounds can be -absolute or relative to the current value. The data from ADC's is usually -noisy. Relative bounds provide a simple (if crude) means of eliminating this. -Absolute bounds can be used to raise an alarm, or log data, if the value goes -out of range. Typical usage: +The `primitives.aadc` module provides the `AADC` (asynchronous ADC) class. This +provides for coroutines which pause until the value returned by an ADC goes +outside predefined bounds. Bounds may be absolute or relative to the current +value. Data from ADC's is usually noisy. Relative bounds provide a simple (if +crude) means of eliminating this. Absolute bounds can be used to raise an alarm +or log data, if the value goes out of range. Typical usage: ```python import uasyncio as asyncio from machine import ADC @@ -272,21 +296,30 @@ obvious design. It was chosen because the plan for `uasyncio` is that it will include an option for prioritising I/O. I wanted this class to be able to use this for applications requiring rapid response. +###### [Contents](./DRIVERS.md#1-contents) + # 6. Additional functions -These comprise `launch` and `set_global_exception` imported as follows: +## 6.1 Launch + +Importe as follows: ```python -from primitives import launch, set_global_exception +from primitives import launch ``` - `launch` enables a function to accept a coro or a callback interchangeably. It accepts the callable plus a tuple of args. If a callback is passed, `launch` runs it and returns the callback's return value. If a coro is passed, it is converted to a `task` and run asynchronously. The return value is the `task` instance. A usage example is in `primitives/switch.py`. +## 6.2 set_global_exception + +Import as follows: +```python +from primitives import set_global_exception +``` `set_global_exception` is a convenience funtion to enable a global exception -handler. This simplifies debugging. The function takes no args. It is called as +handler to simplify debugging. The function takes no args. It is called as follows: ```python @@ -302,3 +335,10 @@ try: finally: asyncio.new_event_loop() # Clear retained state ``` +This is explained in the tutorial. In essence if an exception occurs in a task, +the default behaviour is for the task to stop but for the rest of the code to +continue to run. This means that the failure can be missed and the sequence of +events can be hard to deduce. A global handler ensures that the entire +application stops allowing the traceback and other debug prints to be studied. + +###### [Contents](./DRIVERS.md#1-contents) From fed96b4e1d74b03835950320718f3b58ce5a3c39 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 7 Jul 2020 18:01:50 +0100 Subject: [PATCH 020/305] v3 DRIVERS.md add TOC --- v3/docs/DRIVERS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 9ed69f5..6cfdfbd 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -20,12 +20,12 @@ goes outside defined bounds. 4. [Interfacing pushbuttons](./DRIVERS.md#4-interfacing-pushbuttons) Extends Switch for long and double click events 4.1 [Pushbutton class](./DRIVERS.md#41-pushbutton-class)      4.1.1 [The suppress constructor argument](./DRIVERS.md#411-the-suppress-constructor-argument) - 5. [ADC monitoring](./DRIVERS.md#5-ADC monitoring) Pause until an ADC goes out of bounds - 5.1 [AADC class](./DRIVERS.md#51--aadc-class) + 5. [ADC monitoring](./DRIVERS.md#5-adc-monitoring) Pause until an ADC goes out of bounds + 5.1 [AADC class](./DRIVERS.md#51-aadc-class) 5.2 [Design note](./DRIVERS.md#52-design-note) 6. [Additional functions](./DRIVERS.md#6-additional-functions) 6.1 [launch](./DRIVERS.md#61-launch) Run a coro or callback interchangeably - 6.2 [set_global_exception](,.DRIVERS.md#62-set_global_exception) Simplify debugging with a global exception handler + 6.2 [set_global_exception](./DRIVERS.md#62-set_global_exception) Simplify debugging with a global exception handler ###### [Tutorial](./TUTORIAL.md#contents) From 1738720548307e59ca5fcdec9e54c87de6b77656 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 14 Jul 2020 17:58:17 +0100 Subject: [PATCH 021/305] V3 tutorial: add link to web programming video. --- v3/docs/TUTORIAL.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index e3aa4d5..efac063 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2477,6 +2477,11 @@ The initial reaction of beginners to the idea of cooperative multi-tasking is often one of disappointment. Surely pre-emptive is better? Why should I have to explicitly yield control when the Python virtual machine can do it for me? +My background is in hardware interfacing: I am not a web developer. I found +[this video](https://www.youtube.com/watch?v=kdzL3r-yJZY) to be an interesting +beginner-level introduction to asynchronous web programming which discusses the +relative merits of cooperative and pre-emptive scheduling in that environment. + When it comes to embedded systems the cooperative model has two advantages. Firstly, it is lightweight. It is possible to have large numbers of tasks because unlike descheduled threads, paused tasks contain little state. From 63e23aef2a8f9c3f6ee1bd5234596dc5b2f3b794 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 15 Jul 2020 07:48:58 +0100 Subject: [PATCH 022/305] V3 DRIVERS.md fox TOC and link. --- v3/docs/DRIVERS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 3f84bdf..f291007 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -20,6 +20,7 @@ goes outside defined bounds. 4. [Interfacing pushbuttons](./DRIVERS.md#4-interfacing-pushbuttons) Extends Switch for long and double click events 4.1 [Pushbutton class](./DRIVERS.md#41-pushbutton-class)      4.1.1 [The suppress constructor argument](./DRIVERS.md#411-the-suppress-constructor-argument) +      4.1.2 [The sense constructor argument](./DRIVERS.md#412-the-sense-constructor-argument) 5. [ADC monitoring](./DRIVERS.md#5-adc-monitoring) Pause until an ADC goes out of bounds 5.1 [AADC class](./DRIVERS.md#51-aadc-class) 5.2 [Design note](./DRIVERS.md#52-design-note) @@ -196,7 +197,7 @@ pb.press_func(toggle, (red,)) # Note how function and args are passed asyncio.run(my_app()) # Run main application code ``` -An alternative, compatible `Pushbutton` implementation is available +An alternative `Pushbutton` implementation is available [here](https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py): this implementation avoids the use of the `Delay_ms` class to minimise the number of coroutines. From 2262ff6d7eac9ff25b97d978004efcf74598d630 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 15 Jul 2020 07:51:27 +0100 Subject: [PATCH 023/305] V3 DRIVERS.md fix TOC and link. --- v3/docs/DRIVERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index f291007..b21785c 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -154,7 +154,7 @@ Constructor arguments: 2. `suppress` Default `False`. See [section 4.1.1](./DRIVERS.md#411-the-suppress-constructor-argument). 3. `sense` Default `None`. See - [section 4.1.1](./DRIVERS.md#412-the-sense-constructor-argument). + [section 4.1.2](./DRIVERS.md#412-the-sense-constructor-argument). Methods: From 7784a9fc650aa777bc7cff8c8d7bc3a1bc6da917 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 15 Jul 2020 08:31:17 +0100 Subject: [PATCH 024/305] Initial commit of sched module --- v3/README.md | 6 + v3/as_drivers/sched/__init__.py | 0 v3/as_drivers/sched/asynctest.py | 40 +++ v3/as_drivers/sched/cron.py | 113 ++++++++ v3/as_drivers/sched/crontest.py | 72 +++++ v3/as_drivers/sched/primitives/__init__.py | 31 ++ v3/as_drivers/sched/sched.py | 21 ++ v3/as_drivers/sched/synctest.py | 41 +++ v3/docs/SCHEDULE.md | 317 +++++++++++++++++++++ 9 files changed, 641 insertions(+) create mode 100644 v3/as_drivers/sched/__init__.py create mode 100644 v3/as_drivers/sched/asynctest.py create mode 100644 v3/as_drivers/sched/cron.py create mode 100644 v3/as_drivers/sched/crontest.py create mode 100644 v3/as_drivers/sched/primitives/__init__.py create mode 100644 v3/as_drivers/sched/sched.py create mode 100644 v3/as_drivers/sched/synctest.py create mode 100644 v3/docs/SCHEDULE.md diff --git a/v3/README.md b/v3/README.md index 24eb4f3..f11aafe 100644 --- a/v3/README.md +++ b/v3/README.md @@ -25,6 +25,12 @@ Documented in the tutorial. Comprises: * Classes for interfacing switches and pushbuttons. * A software retriggerable monostable timer class, similar to a watchdog. +### A scheduler + +This [lightweight scheduler](./docs/SCHEDULE.md) enables tasks to be scheduled +at future times. These can be assigned in a flexible way: a task might run at +4.10am on Monday and Friday if there's no "r" in the month. + ### Asynchronous device drivers These device drivers are intended as examples of asynchronous code which are diff --git a/v3/as_drivers/sched/__init__.py b/v3/as_drivers/sched/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v3/as_drivers/sched/asynctest.py b/v3/as_drivers/sched/asynctest.py new file mode 100644 index 0000000..43720b1 --- /dev/null +++ b/v3/as_drivers/sched/asynctest.py @@ -0,0 +1,40 @@ +# asynctest.py Demo of asynchronous code scheduling tasks with cron + +# Copyright (c) 2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +from sched.sched import schedule +from sched.cron import cron +from time import localtime + +def foo(txt): # Demonstrate callback + yr, mo, md, h, m, s, wd = localtime()[:7] + fst = 'Callback {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}' + print(fst.format(txt, h, m, s, md, mo, yr)) + +async def bar(txt): # Demonstrate coro launch + yr, mo, md, h, m, s, wd = localtime()[:7] + fst = 'Coroutine {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}' + print(fst.format(txt, h, m, s, md, mo, yr)) + await asyncio.sleep(0) + +async def main(): + print('Asynchronous test running...') + cron4 = cron(hrs=None, mins=range(0, 60, 4)) + asyncio.create_task(schedule(cron4, foo, ('every 4 mins',))) + + cron5 = cron(hrs=None, mins=range(0, 60, 5)) + asyncio.create_task(schedule(cron5, foo, ('every 5 mins',))) + + cron3 = cron(hrs=None, mins=range(0, 60, 3)) # Launch a coroutine + asyncio.create_task(schedule(cron3, bar, ('every 3 mins',))) + + cron2 = cron(hrs=None, mins=range(0, 60, 2)) + asyncio.create_task(schedule(cron2, foo, ('one shot',), True)) + await asyncio.sleep(900) # Quit after 15 minutes + +try: + asyncio.run(main()) +finally: + _ = asyncio.new_event_loop() diff --git a/v3/as_drivers/sched/cron.py b/v3/as_drivers/sched/cron.py new file mode 100644 index 0000000..e0ddeae --- /dev/null +++ b/v3/as_drivers/sched/cron.py @@ -0,0 +1,113 @@ +# cron.py + +# Copyright (c) 2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +from time import mktime, localtime +# Validation +_valid = ((0, 59, 'secs'), (0, 59, 'mins'), (0, 23, 'hrs'), + (1, 31, 'mday'), (1, 12, 'month'), (0, 6, 'wday')) +_mdays = {2:28, 4:30, 6:30, 9:30, 11:30} +# A call to the inner function takes 270-520μs on Pyboard depending on args +def cron(*, secs=0, mins=0, hrs=3, mday=None, month=None, wday=None): + # Given an arg and current value, return offset between arg and cv + # If arg is iterable return offset of next arg +ve for future -ve for past (add modulo) + def do_arg(a, cv): # Arg, current value + if a is None: + return 0 + elif isinstance(a, int): + return a - cv + try: + return min(x for x in a if x >= cv) - cv + except ValueError: # wrap-round + return min(a) - cv # -ve + except TypeError: + raise ValueError('Invalid argument type', type(a)) + + if secs is None: # Special validation for seconds + raise ValueError('Invalid None value for secs') + if not isinstance(secs, int) and len(secs) > 1: # It's an iterable + ss = sorted(secs) + if min((a[1] - a[0] for a in zip(ss, ss[1:]))) < 2: + raise ValueError("Can't have consecutive seconds.", last, x) + args = (secs, mins, hrs, mday, month, wday) # Validation for all args + valid = iter(_valid) + vestr = 'Argument {} out of range' + vmstr = 'Invalid no. of days for month' + for arg in args: # Check for illegal arg values + lower, upper, errtxt = next(valid) + if isinstance(arg, int): + if not lower <= arg <= upper: + raise ValueError(vestr.format(errtxt)) + elif arg is not None: # Must be an iterable + if any(v for v in arg if not lower <= v <= upper): + raise ValueError(vestr.format(errtxt)) + if mday is not None and month is not None: # Check mday against month + max_md = mday if isinstance(mday, int) else max(mday) + if isinstance(month, int): + if max_md > _mdays.get(month, 31): + raise ValueError(vmstr) + elif sum((m for m in month if max_md > _mdays.get(m, 31))): + raise ValueError(vmstr) + if mday is not None and wday is not None and do_arg(mday, 23) > 0: + raise ValueError('mday must be <= 22 if wday also specified.') + + def inner(tnow): + tev = tnow # Time of next event: work forward from time now + yr, mo, md, h, m, s, wd = localtime(tev)[:7] + init_mo = mo # Month now + toff = do_arg(secs, s) + tev += toff if toff >= 0 else 60 + toff + + yr, mo, md, h, m, s, wd = localtime(tev)[:7] + toff = do_arg(mins, m) + tev += 60 * (toff if toff >= 0 else 60 + toff) + + yr, mo, md, h, m, s, wd = localtime(tev)[:7] + toff = do_arg(hrs, h) + tev += 3600 * (toff if toff >= 0 else 24 + toff) + + yr, mo, md, h, m, s, wd = localtime(tev)[:7] + toff = do_arg(month, mo) + mo += toff + md = md if mo == init_mo else 1 + if toff < 0: + yr += 1 + tev = mktime((yr, mo, md, h, m, s, wd, 0)) + yr, mo, md, h, m, s, wd = localtime(tev)[:7] + if mday is not None: + if mo == init_mo: # Month has not rolled over or been changed + toff = do_arg(mday, md) # see if mday causes rollover + md += toff + if toff < 0: + toff = do_arg(month, mo + 1) # Get next valid month + mo += toff + 1 # Offset is relative to next month + if toff < 0: + yr += 1 + else: # Month has rolled over: day is absolute + md = do_arg(mday, 0) + + if wday is not None: + if mo == init_mo: + toff = do_arg(wday, wd) + md += toff % 7 # mktime handles md > 31 but month may increment + tev = mktime((yr, mo, md, h, m, s, wd, 0)) + cur_mo = mo + _, mo = localtime(tev)[:2] # get month + if mo != cur_mo: + toff = do_arg(month, mo) # Get next valid month + mo += toff # Offset is relative to new, incremented month + if toff < 0: + yr += 1 + tev = mktime((yr, mo, 1, h, m, s, wd, 0)) # 1st of new month + yr, mo, md, h, m, s, wd = localtime(tev)[:7] # get day of week + toff = do_arg(wday, wd) + md += toff % 7 + else: + md = 1 if mday is None else md + tev = mktime((yr, mo, md, h, m, s, wd, 0)) # 1st of new month + yr, mo, md, h, m, s, wd = localtime(tev)[:7] # get day of week + md += (do_arg(wday, 0) - wd) % 7 + + return mktime((yr, mo, md, h, m, s, wd, 0)) - tnow + return inner diff --git a/v3/as_drivers/sched/crontest.py b/v3/as_drivers/sched/crontest.py new file mode 100644 index 0000000..7638614 --- /dev/null +++ b/v3/as_drivers/sched/crontest.py @@ -0,0 +1,72 @@ +# crontest.py + +# Copyright (c) 2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +from time import time, ticks_diff, ticks_us, localtime +from sched.cron import cron +import sys + +maxruntime = 0 +fail = 0 +def result(t, msg): + global fail + if t != next(iexp): + print('FAIL', msg, t) + fail += 1 + return + print('PASS', msg, t) + +def test(*, secs=0, mins=0, hrs=3, mday=None, month=None, wday=None, tsource=None): + global maxruntime + ts = int(time() if tsource is None else tsource) # int() for Unix build + cg = cron(secs=secs, mins=mins, hrs=hrs, mday=mday, month=month, wday=wday) + start = ticks_us() + t = cg(ts) # Time relative to ts + delta = ticks_diff(ticks_us(), start) + maxruntime = max(maxruntime, delta) + print('Runtime = {}μs'.format(delta)) + tev = t + ts # Absolute time of 1st event + yr, mo, md, h, m, s, wd = localtime(tev)[:7] + print('{:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}'.format(h, m, s, md, mo, yr)) + return t # Relative time + +now = 1596074400 if sys.platform == 'linux' else 649393200 # 3am Thursday (day 3) 30 July 2020 +iexp = iter([79500, 79500, 86700, 10680, 13564800, 17712000, + 12781800, 11217915, 5443200, 21600, 17193600, + 18403200, 5353140, 13392000, 18662400]) +# Expect 01:05:00 on 31/07/2020 +result(test(wday=4, hrs=(1,2), mins=5, tsource=now), 'wday and time both cause 1 day increment.') +# 01:05:00 on 31/07/2020 +result(test(hrs=(1,2), mins=5, tsource=now), 'time causes 1 day increment.') +# 03:05:00 on 31/07/2020 +result(test(wday=4, mins=5, tsource=now), 'wday causes 1 day increment.') +# 05:58:00 on 30/07/2020 +result(test(hrs=(5, 23), mins=58, tsource=now), 'time increment no day change.') +# 03:00:00 on 03/01/2021 +result(test(month=1, wday=6, tsource=now), 'month and year rollover, 1st Sunday') +# 03:00:00 on 20/02/2021 +result(test(month=2, mday=20, tsource=now), 'month and year rollover, mday->20 Feb') +# 01:30:00 on 25/12/2020 +result(test(month=12, mday=25, hrs=1, mins=30, tsource=now), 'Forward to Christmas day, hrs backwards') +# 23:05:15 on 06/12/2020 +result(test(month=12, wday=6, hrs=23, mins=5, secs=15, tsource=now), '1st Sunday in Dec 2020') +# 03:00:00 on 01/10/2020 +result(test(month=10, tsource=now), 'Current time on 1st Oct 2020') +# 09:00:00 on 30/07/2020 +result(test(month=7, hrs=9, tsource=now), 'Explicitly specify current month') +# 03:00:00 on 14/02/2021 +result(test(month=2, mday=8, wday=6, tsource=now), 'Second Sunday in February 2021') +# 03:00:00 on 28/02/2021 +result(test(month=2, mday=22, wday=6, tsource=now), 'Fourth Sunday in February 2021') # last day of month +# 01:59:00 on 01/10/2020 +result(test(month=(7, 10), hrs=1, mins=59, tsource=now + 24*3600), 'Time causes month rollover to next legal month') +# 03:00:00 on 01/01/2021 +result(test(month=(7, 1), mday=1, tsource=now), 'mday causes month rollover to next year') +# 03:00:00 on 03/03/2021 +result(test(month=(7, 3), wday=(2, 6), tsource=now), 'wday causes month rollover to next year') +print('Max runtime {}μs'.format(maxruntime)) +if fail: + print(fail, 'FAILURES OCCURRED') +else: + print('ALL TESTS PASSED') diff --git a/v3/as_drivers/sched/primitives/__init__.py b/v3/as_drivers/sched/primitives/__init__.py new file mode 100644 index 0000000..0274fc2 --- /dev/null +++ b/v3/as_drivers/sched/primitives/__init__.py @@ -0,0 +1,31 @@ +# __init__.py Common functions for uasyncio primitives + +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +try: + import uasyncio as asyncio +except ImportError: + import asyncio + + +async def _g(): + pass +type_coro = type(_g()) + +# If a callback is passed, run it and return. +# If a coro is passed initiate it and return. +# coros are passed by name i.e. not using function call syntax. +def launch(func, tup_args): + res = func(*tup_args) + if isinstance(res, type_coro): + res = asyncio.create_task(res) + return res + +def set_global_exception(): + def _handle_exception(loop, context): + import sys + sys.print_exception(context["exception"]) + sys.exit() + loop = asyncio.get_event_loop() + loop.set_exception_handler(_handle_exception) diff --git a/v3/as_drivers/sched/sched.py b/v3/as_drivers/sched/sched.py new file mode 100644 index 0000000..4cfe3c2 --- /dev/null +++ b/v3/as_drivers/sched/sched.py @@ -0,0 +1,21 @@ +# sched.py + +# Copyright (c) 2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +from sched.primitives import launch +from time import time + +async def schedule(fcron, routine, args=(), run_once=False): + maxt = 1000 # uasyncio can't handle arbitrarily long delays + done = False + while not done: + tw = fcron(int(time())) # Time to wait (s) + while tw > 0: # While there is still time to wait + tw = min(tw, maxt) + await asyncio.sleep(tw) + tw -= maxt + launch(routine, args) + done = run_once + await asyncio.sleep_ms(1200) # ensure we're into next second diff --git a/v3/as_drivers/sched/synctest.py b/v3/as_drivers/sched/synctest.py new file mode 100644 index 0000000..c4499b6 --- /dev/null +++ b/v3/as_drivers/sched/synctest.py @@ -0,0 +1,41 @@ +# synctest.py Demo of synchronous code scheduling tasks with cron + +# Copyright (c) 2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +from .cron import cron +from time import localtime, sleep, time + +def foo(txt): + yr, mo, md, h, m, s, wd = localtime()[:7] + fst = "{} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}" + print(fst.format(txt, h, m, s, md, mo, yr)) + +def main(): + print('Synchronous test running...') + tasks = [] # Entries: cron, callback, args, one_shot + cron4 = cron(hrs=None, mins=range(0, 60, 4)) + tasks.append([cron4, foo, ('every 4 mins',), False, False]) + cron5 = cron(hrs=None, mins=range(0, 60, 5)) + tasks.append([cron5, foo, ('every 5 mins',), False, False]) + cron3 = cron(hrs=None, mins=range(0, 60, 3)) + tasks.append([cron3, foo, ('every 3 mins',), False, False]) + cron2 = cron(hrs=None, mins=range(0, 60, 2)) + tasks.append([cron2, foo, ('one shot',), True, False]) + to_run = [] + while True: + now = int(time()) # Ensure constant: get once per iteration. + tasks.sort(key=lambda x:x[0](now)) + to_run.clear() # Pending tasks + deltat = tasks[0][0](now) # Time to pending task(s) + for task in (t for t in tasks if t[0](now) == deltat): # Tasks with same delta t + to_run.append(task) + task[4] = True # Has been scheduled + # Remove on-shot tasks which have been scheduled + tasks = [t for t in tasks if not (t[3] and t[4])] + sleep(deltat) + for tsk in to_run: + tsk[1](*tsk[2]) + sleep(1.2) # Ensure seconds have rolled over + +main() diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md new file mode 100644 index 0000000..36c0f38 --- /dev/null +++ b/v3/docs/SCHEDULE.md @@ -0,0 +1,317 @@ + 1. [Scheduling tasks](./SCHEDULE.md#1-scheduling-tasks) + 2. [Overview](./SCHEDULE.md#2-overview) + 3. [Installation](./SCHEDULE.md#3-installation) + 4. [The cron object](./SCHEDULE.md#4-the-cron-object) + 4.1 [Time specifiers](./SCHEDULE.md#41-time-specifiers).. + 4.2 [The time to an event](./SCHEDULE.md#42-the-time-to-an-event) + 4.3 [How it works](./SCHEDULE.md#43-how-it-works) + 4.4 [Calendar behaviour](./SCHEDULE.md#44-calendar-behaviour) + 4.5 [Limitations](./SCHEDULE.md#45-limitations) + 4.6 [The Unix build](./SCHEDULE.md#46-the-unix-build) + 5. [The schedule function](./SCHEDULE.md#5-the schedule-function) + 6. [Use in synchronous code](./SCHEDULE.md#6-use-in-synchronous-code) + +###### [Tutorial](./TUTORIAL.md#contents) +###### [Main V3 README](../README.md) + +# 1. Scheduling tasks + +A common requirement is to schedule tasks to occur at specific times in the +future. This module facilitates this. The module can accept wildcard values +enabling tasks to be scheduled in a flexible manner. For example you might want +a callback to run at 3.10 am on every month which has an "r" in the name. + +It is partly inspired by the Unix cron table, also by the +[Python schedule](https://github.com/dbader/schedule) module. Compared to the +latter it is less capable but is small, fast and designed for microcontroller +use. Repetitive and one-shot events may be created. + +It is ideally suited for use with `uasyncio` and basic use requires minimal +`uasyncio` knowledge. Users intending only to schedule callbacks can simply +adapt the example code. It can be used in synchronous code and an example is +provided. + +It is cross-platform and has been tested on Pyboard, Pyboard D, ESP8266, ESP32 +and the Unix build (the latter is subject to a minor local time issue). + +# 2. Overview + +There are two components, the `cron` object (in `sched/cron.py`) and the +`schedule` function (in `sched/sched.py`). The user creates `cron` instances, +passing arguments specifying time intervals. The `cron` instance may be run at +any time and will return the time in seconds to the next scheduled event. + +The `schedule` function is an optional component for use with `uasyncio`. The +function takes a `cron` instance and a callback and causes that callback to run +at the times specified by the `cron`. A coroutine may be substituted for the +callback - at the specified times it will be promoted to a `Task` and run. + +# 3. Installation + +Copy the `sched` directory and contents to the target's filesystem. It requires +`uasyncio` V3 which is included in daily firmware builds. It will be in release +builds after V1.12. + +To install to an SD card using [rshell](https://github.com/dhylands/rshell) +move to the parent directory of `sched` and issue: +``` +> rsync sched /sd/sched +``` +Adapt the destination as appropriate for your hardware. + +# 4. The cron object + +This is a closure. It accepts a time specification for future events. Each call +returns the number of seconds to wait for the next event to occur. + +It takes the following keyword-only args. A flexible set of data types are +accepted. These are known as `Time specifiers` and described below. Valid +numbers are shown as inclusive ranges. + 1. `secs=0` Seconds (0..59). + 2. `mins=0` Minutes (0..59). + 3. `hrs=3` Hours (0..23). + 4. `mday=None` Day of month (1..31). + 5. `month=None` Months (1..12). + 6. `wday=None` Weekday (0..6 Mon..Sun). + +## 4.1 Time specifiers + +The args may be of the following types. + 1. `None` This is a wildcard matching any value. Do not use for `secs`. + 2. An integer. + 3. An object supporting the Python iterator protocol and iterating over + integers. For example `hrs=(3, 17)` will cause events to occur at 3am and 5pm, + `wday=range(0, 5)` specifies weekdays. Tuples, lists, ranges or sets may be + passed. + +Legal ranges are listed above. Basic validation is done when a `cron` is +instantiated. + +Note the implications of the `None` wildcard. Setting `mins=None` will schedule +the event to occur on every minute (equivalent to `*` in a Unix cron table). +Setting `secs=None` or consecutive seconds values will cause a `ValueError` - +events must be at least two seconds apart. + +Default values schedule an event every day at 03.00.00. + +## 4.2 The time to an event + +When the `cron` instance is run, it must be passed a time value (normally the +time now as returned by `time.time()`). The instance returns the number of +seconds to the first event matching the specifier. + +```python +from sched.cron import cron +cron1 = cron(hrs=None, mins=range(0, 60, 15)) # Every 15 minutes of every day +cron2 = cron(mday=25, month=12, hrs=9) # 9am every Christmas day +cron3 = cron(wday=(0, 4)) # 3am every Monday and Friday +now = int(time.time()) # Unix build returns a float here +tnext = min(cron1(now), cron2(now), cron3(now)) # Seconds until 1st event +``` + +## 4.3 How it works + +When a cron instance is run it seeks a future time and date relative to the +passed time value. This will be the soonest matching the specifier. A `cron` +instance is a conventional function and does not store state. Repeated calls +will return the same value if passed the same time value (`now` in the above +example). + +## 4.4 Calendar behaviour + +Specifying a day in the month which exceeds the length of a specified month +(e.g. `month=(2, 6, 7), mday=30`) will produce a `ValueError`. February is +assumed to have 28 days. + +### 4.4.1 Behaviour of mday and wday values + +The following describes how to schedule something for (say) the second Sunday +in a month, plus limitations of doing this. + +If a month is specified which differs from the current month, the day in the +month defaults to the 1st. This can be overridden with `mday` and `wday`, so +you can specify the 21st (`mday=21`) or the first Sunday in the month +(`wday=6`). If `mday` and `wday` are both specified, `mday` is applied first. +This enables the Nth instance of a day to be defined. To specify the second +Sunday in the month specify `mday=8` to skip the first week, and set `wday=6` +to specify Sunday. Unfortunately you can't specify the last (say) Tuesday in +the month. + +Specifying `wday=d` and `mday=n` where n > 22 could result in a day beyond the +end of the month. It's not obvious what constitutes rational behaviour in this +pathological corner case: a `ValueError` will result. + +### 4.4.2 Time causing month rollover + +The following describes behaviour which I consider correct. + +On the last day of the month there are circumstances where a time specifier can +cause a day rollover. Consider application start. If a `cron` is run whose time +specifier provides only times prior to the current time, its month increments +and the day changes to the 1st. This is the soonest that the event can occur at +the specified time. + +Consider the case where the next month is disallowed. In this case the month +will change to the next valid month. This code, run at 9am on 31st July, would +aim to run the event at 1.59 on 1st October. +```python +my_cron(month=(2, 7, 10), hrs=1, mins=59) # moves forward 1 day +t_wait = my_cron(time.time()) # but month may be disallowed +``` + +## 4.5 Limitations + +The `cron` code has a resolution of 1 second. It is intended for scheduling +infrequent events (`uasyncio` is recommended for doing fast scheduling). + +Specifying `secs=None` will cause a `ValueError`. The minimum interval between +scheduled events is 2 seconds. Attempts to schedule events with a shorter gap +will raise a `ValueError`. + +A `cron` call typically takes 270 to 520μs on a Pyboard, but the upper bound +depends on the complexity of the time specifiers. + +On hardware platforms the MicroPython `time` module does not handle daylight +saving time. Scheduled times are relative to system time. This does not apply +to the Unix build. + +It has been tested on ESP8266 but this platform has poor time stability so is +not well suited to long term timing applications. On my reference board timing +drifted by 1.4mins/hr, an error of 2.3%. + +## 4.6 The Unix build + +Asynchronous use requires `uasyncio` V3, so ensure this is installed on a Linux +box. + +The synchronous and asynchronous demos run under the Unix build: it should be +usable on Linux provided the daylight saving time (DST) constraints below are +met. + +A consequence of DST is that there are impossible times when clocks go forward +and duplicates when they go back. Scheduling those times will fail. A solution +is to avoid scheduling the times in your region where this occurs (01.00.00 to +02.00.00 in March and October here). + +The `crontest.py` test program produces failures under Unix. Most of these +result from the fact that the Unix `localtime` function handles daylight saving +time. On bare hardware MicroPython has no provision for DST. I do not plan to +adapt `cron.py` to account for this: its design focus is small lightweight code +to run on bare metal targets. I could adapt `crontest.py` but it would surely +fail in other countries. + +# 5. The schedule function + +This enables a callback or coroutine to be run at intervals specified by a +`cron` instance. An option for one-shot use is available. It is an asynchronous +function. Positional args: + 1. `fcron` A `cron` instance. + 2. `routine` The callable (callback or coroutine) to run. + 3. `args=()` A tuple of args for the callable. + 4. `run_once=False` If `True` the callable will be run once only. + +The `schedule` function only terminates if `run_once=True`, and then typically +after a long time. Usually `schedule` is started with `asyncio.create_task`, as +in the following example where a callback is scheduled at various times. The +code below may be run by issuing +```python +import sched.asynctest +``` +This is the demo code. +```python +import uasyncio as asyncio +from sched.sched import schedule +from sched.cron import cron +from time import localtime + +def foo(txt): # Demonstrate callback + yr, mo, md, h, m, s, wd = localtime()[:7] + fst = 'Callback {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}' + print(fst.format(txt, h, m, s, md, mo, yr)) + +async def bar(txt): # Demonstrate coro launch + yr, mo, md, h, m, s, wd = localtime()[:7] + fst = 'Coroutine {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}' + print(fst.format(txt, h, m, s, md, mo, yr)) + await asyncio.sleep(0) + +async def main(): + print('Asynchronous test running...') + cron4 = cron(hrs=None, mins=range(0, 60, 4)) + asyncio.create_task(schedule(cron4, foo, ('every 4 mins',))) + + cron5 = cron(hrs=None, mins=range(0, 60, 5)) + asyncio.create_task(schedule(cron5, foo, ('every 5 mins',))) + + cron3 = cron(hrs=None, mins=range(0, 60, 3)) # Launch a coroutine + asyncio.create_task(schedule(cron3, bar, ('every 3 mins',))) + + cron2 = cron(hrs=None, mins=range(0, 60, 2)) + asyncio.create_task(schedule(cron2, foo, ('one shot',), True)) + await asyncio.sleep(900) # Quit after 15 minutes + +try: + asyncio.run(main()) +finally: + _ = asyncio.new_event_loop() +``` + +# 6. Use in synchronous code + +It is possible to use the `cron` closure in synchronous code. This involves +writing an event loop, an example of which is illustrated below. In this +example a task list entry is a tuple with the following contents. + 1. The `cron` instance. + 2. The callback to run. + 3. A tuple of arguments for the callback. + 4. A boolean, `True` if the callback is to be run once only. + 5. A boolean, `True` if the task has been put on the pending queue. + +The code below may be found in `sched/synctest.py` and may be run by issuing +```python +import sched.synctest +``` +This is the demo code. +```python +from .cron import cron +from time import localtime, sleep, time + +def foo(txt): + yr, mo, md, h, m, s, wd = localtime()[:7] + fst = "{} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}" + print(fst.format(txt, h, m, s, md, mo, yr)) + +def main(): + print('Synchronous test running...') + tasks = [] # Entries: cron, callback, args, one_shot + cron4 = cron(hrs=None, mins=range(0, 60, 4)) + tasks.append([cron4, foo, ('every 4 mins',), False, False]) + cron5 = cron(hrs=None, mins=range(0, 60, 5)) + tasks.append([cron5, foo, ('every 5 mins',), False, False]) + cron3 = cron(hrs=None, mins=range(0, 60, 3)) + tasks.append([cron3, foo, ('every 3 mins',), False, False]) + cron2 = cron(hrs=None, mins=range(0, 60, 2)) + tasks.append([cron2, foo, ('one shot',), True, False]) + to_run = [] + while True: + now = time() # Ensure constant: get once per iteration. + tasks.sort(key=lambda x:x[0](now)) + to_run.clear() # Pending tasks + deltat = tasks[0][0](now) # Time to pending task(s) + for task in (t for t in tasks if t[0](now) == deltat): # Tasks with same delta t + to_run.append(task) + task[4] = True # Has been scheduled + # Remove on-shot tasks which have been scheduled + tasks = [t for t in tasks if not (t[3] and t[4])] + sleep(deltat) + for tsk in to_run: + tsk[1](*tsk[2]) + sleep(2) # Ensure seconds have rolled over + +main() +``` + +In my opinion the asynchronous version is cleaner and easier to understand. It +is also more versatile because the advanced features of `uasyncio` are +available to the application. The above code is incompatible with `uasyncio` +because of the blocking calls to `time.sleep`. From 4bc1cb55d03e59a508ff055c27286d29fe63b934 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 15 Jul 2020 08:53:16 +0100 Subject: [PATCH 025/305] v3/docs/SCHEDULE.md fix links, add section 7. --- v3/docs/SCHEDULE.md | 46 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index 36c0f38..bb94651 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -2,17 +2,18 @@ 2. [Overview](./SCHEDULE.md#2-overview) 3. [Installation](./SCHEDULE.md#3-installation) 4. [The cron object](./SCHEDULE.md#4-the-cron-object) - 4.1 [Time specifiers](./SCHEDULE.md#41-time-specifiers).. + 4.1 [Time specifiers](./SCHEDULE.md#41-time-specifiers) 4.2 [The time to an event](./SCHEDULE.md#42-the-time-to-an-event) 4.3 [How it works](./SCHEDULE.md#43-how-it-works) 4.4 [Calendar behaviour](./SCHEDULE.md#44-calendar-behaviour) 4.5 [Limitations](./SCHEDULE.md#45-limitations) 4.6 [The Unix build](./SCHEDULE.md#46-the-unix-build) - 5. [The schedule function](./SCHEDULE.md#5-the schedule-function) - 6. [Use in synchronous code](./SCHEDULE.md#6-use-in-synchronous-code) + 5. [The schedule function](./SCHEDULE.md#5-the-schedule-function) The primary interface for uasyncio + 6. [Use in synchronous code](./SCHEDULE.md#6-use-in-synchronous-code) If you really must + 7. [Hardware timing limitations](./SCHEDULE,md#7-hardware-timing-limitations) -###### [Tutorial](./TUTORIAL.md#contents) -###### [Main V3 README](../README.md) +##### [Tutorial](./TUTORIAL.md#contents) +##### [Main V3 README](../README.md) # 1. Scheduling tasks @@ -46,6 +47,8 @@ function takes a `cron` instance and a callback and causes that callback to run at the times specified by the `cron`. A coroutine may be substituted for the callback - at the specified times it will be promoted to a `Task` and run. +##### [Top](./SCHEDULE.md#1-scheduling-tasks) + # 3. Installation Copy the `sched` directory and contents to the target's filesystem. It requires @@ -74,6 +77,8 @@ numbers are shown as inclusive ranges. 5. `month=None` Months (1..12). 6. `wday=None` Weekday (0..6 Mon..Sun). +##### [Top](./SCHEDULE.md#1-scheduling-tasks) + ## 4.1 Time specifiers The args may be of the following types. @@ -109,6 +114,8 @@ now = int(time.time()) # Unix build returns a float here tnext = min(cron1(now), cron2(now), cron3(now)) # Seconds until 1st event ``` +##### [Top](./SCHEDULE.md#1-scheduling-tasks) + ## 4.3 How it works When a cron instance is run it seeks a future time and date relative to the @@ -159,6 +166,8 @@ my_cron(month=(2, 7, 10), hrs=1, mins=59) # moves forward 1 day t_wait = my_cron(time.time()) # but month may be disallowed ``` +##### [Top](./SCHEDULE.md#1-scheduling-tasks) + ## 4.5 Limitations The `cron` code has a resolution of 1 second. It is intended for scheduling @@ -175,10 +184,6 @@ On hardware platforms the MicroPython `time` module does not handle daylight saving time. Scheduled times are relative to system time. This does not apply to the Unix build. -It has been tested on ESP8266 but this platform has poor time stability so is -not well suited to long term timing applications. On my reference board timing -drifted by 1.4mins/hr, an error of 2.3%. - ## 4.6 The Unix build Asynchronous use requires `uasyncio` V3, so ensure this is installed on a Linux @@ -200,6 +205,8 @@ adapt `cron.py` to account for this: its design focus is small lightweight code to run on bare metal targets. I could adapt `crontest.py` but it would surely fail in other countries. +##### [Top](./SCHEDULE.md#1-scheduling-tasks) + # 5. The schedule function This enables a callback or coroutine to be run at intervals specified by a @@ -256,6 +263,8 @@ finally: _ = asyncio.new_event_loop() ``` +##### [Top](./SCHEDULE.md#1-scheduling-tasks) + # 6. Use in synchronous code It is possible to use the `cron` closure in synchronous code. This involves @@ -315,3 +324,22 @@ In my opinion the asynchronous version is cleaner and easier to understand. It is also more versatile because the advanced features of `uasyncio` are available to the application. The above code is incompatible with `uasyncio` because of the blocking calls to `time.sleep`. + +##### [Top](./SCHEDULE.md#1-scheduling-tasks) + +# 7. Hardware timing limitations + +The code has been tested on Pyboard 1.x, Pyboard D, ESP32 and ESP8266. All +except ESP8266 have good timing performance. Pyboards can be calibrated to +timepiece precision using a cheap DS3231 and +[this utility](https://github.com/peterhinch/micropython-samples/tree/master/DS3231). + +The ESP8266 has poor time stability so is not well suited to long term timing +applications. On my reference board timing drifted by 1.4mins/hr, an error of +2.3%. + +Boards with internet connectivity can periodically synchronise to an NTP server +but this carries a risk of sudden jumps in the system time which may disrupt +`uasyncio` and the scheduler. + +##### [Top](./SCHEDULE.md#1-scheduling-tasks) From cadb6355c33b2c00617fed4af5adb1006688fe32 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 15 Jul 2020 08:54:34 +0100 Subject: [PATCH 026/305] v3/docs/SCHEDULE.md fix links, add section 7. --- v3/docs/SCHEDULE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index bb94651..eb098b7 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -10,7 +10,7 @@ 4.6 [The Unix build](./SCHEDULE.md#46-the-unix-build) 5. [The schedule function](./SCHEDULE.md#5-the-schedule-function) The primary interface for uasyncio 6. [Use in synchronous code](./SCHEDULE.md#6-use-in-synchronous-code) If you really must - 7. [Hardware timing limitations](./SCHEDULE,md#7-hardware-timing-limitations) + 7. [Hardware timing limitations](./SCHEDULE.md#7-hardware-timing-limitations) ##### [Tutorial](./TUTORIAL.md#contents) ##### [Main V3 README](../README.md) From 028525a7e550b8566dfb0fc6e1d189c36a3fd4c2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 15 Jul 2020 09:02:59 +0100 Subject: [PATCH 027/305] v3/docs/SCHEDULE.md fix links. --- v3/docs/SCHEDULE.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index eb098b7..50c6135 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -1,3 +1,5 @@ +# 0. Contents + 1. [Scheduling tasks](./SCHEDULE.md#1-scheduling-tasks) 2. [Overview](./SCHEDULE.md#2-overview) 3. [Installation](./SCHEDULE.md#3-installation) @@ -47,7 +49,7 @@ function takes a `cron` instance and a callback and causes that callback to run at the times specified by the `cron`. A coroutine may be substituted for the callback - at the specified times it will be promoted to a `Task` and run. -##### [Top](./SCHEDULE.md#1-scheduling-tasks) +##### [Top](./SCHEDULE.md#0-contents) # 3. Installation @@ -77,7 +79,7 @@ numbers are shown as inclusive ranges. 5. `month=None` Months (1..12). 6. `wday=None` Weekday (0..6 Mon..Sun). -##### [Top](./SCHEDULE.md#1-scheduling-tasks) +##### [Top](./SCHEDULE.md#0-contents) ## 4.1 Time specifiers @@ -114,7 +116,7 @@ now = int(time.time()) # Unix build returns a float here tnext = min(cron1(now), cron2(now), cron3(now)) # Seconds until 1st event ``` -##### [Top](./SCHEDULE.md#1-scheduling-tasks) +##### [Top](./SCHEDULE.md#0-contents) ## 4.3 How it works @@ -166,7 +168,7 @@ my_cron(month=(2, 7, 10), hrs=1, mins=59) # moves forward 1 day t_wait = my_cron(time.time()) # but month may be disallowed ``` -##### [Top](./SCHEDULE.md#1-scheduling-tasks) +##### [Top](./SCHEDULE.md#0-contents) ## 4.5 Limitations @@ -205,7 +207,7 @@ adapt `cron.py` to account for this: its design focus is small lightweight code to run on bare metal targets. I could adapt `crontest.py` but it would surely fail in other countries. -##### [Top](./SCHEDULE.md#1-scheduling-tasks) +##### [Top](./SCHEDULE.md#0-contents) # 5. The schedule function @@ -263,7 +265,7 @@ finally: _ = asyncio.new_event_loop() ``` -##### [Top](./SCHEDULE.md#1-scheduling-tasks) +##### [Top](./SCHEDULE.md#0-contents) # 6. Use in synchronous code @@ -325,7 +327,7 @@ is also more versatile because the advanced features of `uasyncio` are available to the application. The above code is incompatible with `uasyncio` because of the blocking calls to `time.sleep`. -##### [Top](./SCHEDULE.md#1-scheduling-tasks) +##### [Top](./SCHEDULE.md#0-contents) # 7. Hardware timing limitations @@ -342,4 +344,4 @@ Boards with internet connectivity can periodically synchronise to an NTP server but this carries a risk of sudden jumps in the system time which may disrupt `uasyncio` and the scheduler. -##### [Top](./SCHEDULE.md#1-scheduling-tasks) +##### [Top](./SCHEDULE.md#0-contents) From 66f03edc020e5f9ac91937bb9b760f1be34cddee Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 15 Jul 2020 10:18:35 +0100 Subject: [PATCH 028/305] v3/docs/SCHEDULE.md Various improvements. --- v3/docs/SCHEDULE.md | 89 ++++++++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index 50c6135..b040823 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -3,7 +3,7 @@ 1. [Scheduling tasks](./SCHEDULE.md#1-scheduling-tasks) 2. [Overview](./SCHEDULE.md#2-overview) 3. [Installation](./SCHEDULE.md#3-installation) - 4. [The cron object](./SCHEDULE.md#4-the-cron-object) + 4. [The cron object](./SCHEDULE.md#4-the-cron-object) How to specify times and dates 4.1 [Time specifiers](./SCHEDULE.md#41-time-specifiers) 4.2 [The time to an event](./SCHEDULE.md#42-the-time-to-an-event) 4.3 [How it works](./SCHEDULE.md#43-how-it-works) @@ -11,8 +11,8 @@ 4.5 [Limitations](./SCHEDULE.md#45-limitations) 4.6 [The Unix build](./SCHEDULE.md#46-the-unix-build) 5. [The schedule function](./SCHEDULE.md#5-the-schedule-function) The primary interface for uasyncio - 6. [Use in synchronous code](./SCHEDULE.md#6-use-in-synchronous-code) If you really must - 7. [Hardware timing limitations](./SCHEDULE.md#7-hardware-timing-limitations) + 6. [Hardware timing limitations](./SCHEDULE.md#6-hardware-timing-limitations) + 7. [Use in synchronous code](./SCHEDULE.md#7-use-in-synchronous-code) If you really must ##### [Tutorial](./TUTORIAL.md#contents) ##### [Main V3 README](../README.md) @@ -64,10 +64,24 @@ move to the parent directory of `sched` and issue: ``` Adapt the destination as appropriate for your hardware. +The following files are installed in the `sched` directory. + 1. `cron.py` Computes time to next event. + 2. `sched.py` The `uasyncio` `schedule` function: schedule a callback or coro. + 3. `primitives/__init__.py` Necessary for `sched.py`. + 4. `asynctest.py` Demo of asynchronous scheduling. + 5. `synctest.py` Synchronous scheduling demo. For `uasyncio` phobics only. + 6. `crontest.py` A test for `cron.py` code. + 7. `__init__.py` Empty file for Python package. + +The `crontest` script is only of interest to those wishing to adapt `cron.py`. +To run error-free a bare metal target should be used for the reason discussed +[here](./SCHEDULE.md#46-the-unix-build). + # 4. The cron object This is a closure. It accepts a time specification for future events. Each call -returns the number of seconds to wait for the next event to occur. +when passed the current time returns the number of seconds to wait for the next +event to occur. It takes the following keyword-only args. A flexible set of data types are accepted. These are known as `Time specifiers` and described below. Valid @@ -148,7 +162,7 @@ the month. Specifying `wday=d` and `mday=n` where n > 22 could result in a day beyond the end of the month. It's not obvious what constitutes rational behaviour in this -pathological corner case: a `ValueError` will result. +pathological corner case. Validation will throw a `ValueError` in this case. ### 4.4.2 Time causing month rollover @@ -165,7 +179,7 @@ will change to the next valid month. This code, run at 9am on 31st July, would aim to run the event at 1.59 on 1st October. ```python my_cron(month=(2, 7, 10), hrs=1, mins=59) # moves forward 1 day -t_wait = my_cron(time.time()) # but month may be disallowed +t_wait = my_cron(time.time()) # Next month is disallowed so jumps to October ``` ##### [Top](./SCHEDULE.md#0-contents) @@ -184,14 +198,14 @@ depends on the complexity of the time specifiers. On hardware platforms the MicroPython `time` module does not handle daylight saving time. Scheduled times are relative to system time. This does not apply -to the Unix build. +to the Unix build where daylight saving needs to be considered. ## 4.6 The Unix build -Asynchronous use requires `uasyncio` V3, so ensure this is installed on a Linux -box. +Asynchronous use requires `uasyncio` V3, so ensure this is installed on the +Linux target. -The synchronous and asynchronous demos run under the Unix build: it should be +The synchronous and asynchronous demos run under the Unix build. The module is usable on Linux provided the daylight saving time (DST) constraints below are met. @@ -200,12 +214,10 @@ and duplicates when they go back. Scheduling those times will fail. A solution is to avoid scheduling the times in your region where this occurs (01.00.00 to 02.00.00 in March and October here). -The `crontest.py` test program produces failures under Unix. Most of these -result from the fact that the Unix `localtime` function handles daylight saving -time. On bare hardware MicroPython has no provision for DST. I do not plan to -adapt `cron.py` to account for this: its design focus is small lightweight code -to run on bare metal targets. I could adapt `crontest.py` but it would surely -fail in other countries. +The `crontest.py` test program produces failures under Unix. These result from +the fact that the Unix `localtime` function handles daylight saving time. The +purpose of `crontest.py` is to check `cron` code. It should be run on bare +metal targets. ##### [Top](./SCHEDULE.md#0-contents) @@ -267,11 +279,29 @@ finally: ##### [Top](./SCHEDULE.md#0-contents) -# 6. Use in synchronous code +# 6. Hardware timing limitations + +The code has been tested on Pyboard 1.x, Pyboard D, ESP32 and ESP8266. All +except ESP8266 have good timing performance. Pyboards can be calibrated to +timepiece precision using a cheap DS3231 and +[this utility](https://github.com/peterhinch/micropython-samples/tree/master/DS3231). + +The ESP8266 has poor time stability so is not well suited to long term timing +applications. On my reference board timing drifted by 1.4mins/hr, an error of +2.3%. + +Boards with internet connectivity can periodically synchronise to an NTP server +but this carries a risk of sudden jumps in the system time which may disrupt +`uasyncio` and the scheduler. + +##### [Top](./SCHEDULE.md#0-contents) + +# 7. Use in synchronous code It is possible to use the `cron` closure in synchronous code. This involves -writing an event loop, an example of which is illustrated below. In this -example a task list entry is a tuple with the following contents. +the mildly masochistic task of writing an event loop, an example of which is +illustrated below. In this example a task list entry is a tuple with the +following contents. 1. The `cron` instance. 2. The callback to run. 3. A tuple of arguments for the callback. @@ -312,7 +342,7 @@ def main(): for task in (t for t in tasks if t[0](now) == deltat): # Tasks with same delta t to_run.append(task) task[4] = True # Has been scheduled - # Remove on-shot tasks which have been scheduled + # Remove one-shot tasks which have been scheduled tasks = [t for t in tasks if not (t[3] and t[4])] sleep(deltat) for tsk in to_run: @@ -325,23 +355,6 @@ main() In my opinion the asynchronous version is cleaner and easier to understand. It is also more versatile because the advanced features of `uasyncio` are available to the application. The above code is incompatible with `uasyncio` -because of the blocking calls to `time.sleep`. - -##### [Top](./SCHEDULE.md#0-contents) - -# 7. Hardware timing limitations - -The code has been tested on Pyboard 1.x, Pyboard D, ESP32 and ESP8266. All -except ESP8266 have good timing performance. Pyboards can be calibrated to -timepiece precision using a cheap DS3231 and -[this utility](https://github.com/peterhinch/micropython-samples/tree/master/DS3231). - -The ESP8266 has poor time stability so is not well suited to long term timing -applications. On my reference board timing drifted by 1.4mins/hr, an error of -2.3%. - -Boards with internet connectivity can periodically synchronise to an NTP server -but this carries a risk of sudden jumps in the system time which may disrupt -`uasyncio` and the scheduler. +because of the blocking calls to `time.sleep()`. ##### [Top](./SCHEDULE.md#0-contents) From c6890ae743f5ef7e8f45102a2c7ae31eb2dd9a10 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 16 Jul 2020 07:51:06 +0100 Subject: [PATCH 029/305] v3/docs/DRIVERS.md Improve description of Pushbutton sense arg. --- v3/docs/DRIVERS.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index b21785c..8f44336 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -153,7 +153,7 @@ Constructor arguments: 1. `pin` Mandatory. The initialised Pin instance. 2. `suppress` Default `False`. See [section 4.1.1](./DRIVERS.md#411-the-suppress-constructor-argument). - 3. `sense` Default `None`. See + 3. `sense` Default `None`. Option to define electrical connection. See [section 4.1.2](./DRIVERS.md#412-the-sense-constructor-argument). Methods: @@ -227,12 +227,21 @@ set, `release_func` will be launched as follows: ### 4.1.2 The sense constructor argument +In most applications it can be assumed that, at power-up, pushbuttons are not +pressed. The default `None` value uses this assumption to assign the `False` +(not pressed) state at power up. It therefore works with normally open or +normally closed buttons wired to either supply rail. This without programmer +intervention. + +In certain use cases this assumption does not hold, and `sense` must explicitly +be specified. This defines the logical state at power-up regardless of whether, +at that time, the button is pressed. Hence `sense=0` defines a button connected +in such a way that when it is not pressed, the voltage on the pin is 0. + When the pin value changes, the new value is compared with `sense` to determine if the button is closed or open. This is to allow the designer to specify if the `closed` state of the button is active `high` or active `low`. -This parameter will default to the current value of `pin` for convienence. - ###### [Contents](./DRIVERS.md#1-contents) # 5. ADC monitoring From 5ea87464c6c57e7203c6733b01421194796b9d4e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 16 Jul 2020 12:52:04 +0100 Subject: [PATCH 030/305] v3/as_drivers/sched Improve schedule() arg pattern. --- v3/as_drivers/sched/asynctest.py | 14 +- v3/as_drivers/sched/sched.py | 12 +- v3/docs/SCHEDULE.md | 238 +++++++++++++++++-------------- 3 files changed, 139 insertions(+), 125 deletions(-) diff --git a/v3/as_drivers/sched/asynctest.py b/v3/as_drivers/sched/asynctest.py index 43720b1..d3b7712 100644 --- a/v3/as_drivers/sched/asynctest.py +++ b/v3/as_drivers/sched/asynctest.py @@ -5,7 +5,6 @@ import uasyncio as asyncio from sched.sched import schedule -from sched.cron import cron from time import localtime def foo(txt): # Demonstrate callback @@ -21,17 +20,14 @@ async def bar(txt): # Demonstrate coro launch async def main(): print('Asynchronous test running...') - cron4 = cron(hrs=None, mins=range(0, 60, 4)) - asyncio.create_task(schedule(cron4, foo, ('every 4 mins',))) + asyncio.create_task(schedule(foo, 'every 4 mins', hrs=None, mins=range(0, 60, 4))) - cron5 = cron(hrs=None, mins=range(0, 60, 5)) - asyncio.create_task(schedule(cron5, foo, ('every 5 mins',))) + asyncio.create_task(schedule(foo, 'every 5 mins', hrs=None, mins=range(0, 60, 5))) - cron3 = cron(hrs=None, mins=range(0, 60, 3)) # Launch a coroutine - asyncio.create_task(schedule(cron3, bar, ('every 3 mins',))) + # Launch a coroutine + asyncio.create_task(schedule(bar, 'every 3 mins', hrs=None, mins=range(0, 60, 3))) - cron2 = cron(hrs=None, mins=range(0, 60, 2)) - asyncio.create_task(schedule(cron2, foo, ('one shot',), True)) + asyncio.create_task(schedule(foo, 'one shot', hrs=None, mins=range(0, 60, 2), times=1)) await asyncio.sleep(900) # Quit after 15 minutes try: diff --git a/v3/as_drivers/sched/sched.py b/v3/as_drivers/sched/sched.py index 4cfe3c2..24857fe 100644 --- a/v3/as_drivers/sched/sched.py +++ b/v3/as_drivers/sched/sched.py @@ -6,16 +6,18 @@ import uasyncio as asyncio from sched.primitives import launch from time import time +from sched.cron import cron -async def schedule(fcron, routine, args=(), run_once=False): +async def schedule(func, *args, times=None, **kwargs): + fcron = cron(**kwargs) maxt = 1000 # uasyncio can't handle arbitrarily long delays - done = False - while not done: + while times is None or times > 0: tw = fcron(int(time())) # Time to wait (s) while tw > 0: # While there is still time to wait tw = min(tw, maxt) await asyncio.sleep(tw) tw -= maxt - launch(routine, args) - done = run_once + launch(func, args) + if times is not None: + times -= 1 await asyncio.sleep_ms(1200) # ensure we're into next second diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index b040823..aca223f 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -3,14 +3,16 @@ 1. [Scheduling tasks](./SCHEDULE.md#1-scheduling-tasks) 2. [Overview](./SCHEDULE.md#2-overview) 3. [Installation](./SCHEDULE.md#3-installation) - 4. [The cron object](./SCHEDULE.md#4-the-cron-object) How to specify times and dates + 4. [The schedule function](./SCHEDULE.md#4-the-schedule-function) The primary interface for uasyncio 4.1 [Time specifiers](./SCHEDULE.md#41-time-specifiers) - 4.2 [The time to an event](./SCHEDULE.md#42-the-time-to-an-event) - 4.3 [How it works](./SCHEDULE.md#43-how-it-works) - 4.4 [Calendar behaviour](./SCHEDULE.md#44-calendar-behaviour) - 4.5 [Limitations](./SCHEDULE.md#45-limitations) - 4.6 [The Unix build](./SCHEDULE.md#46-the-unix-build) - 5. [The schedule function](./SCHEDULE.md#5-the-schedule-function) The primary interface for uasyncio + 4.2 [Calendar behaviour](./SCHEDULE.md#42-calendar-behaviour) Calendars can be tricky... +      4.2.1 [Behaviour of mday and wday values](./SCHEDULE.md#421-behaviour-of-mday-and-wday-values) +      4.2.2 [Time causing month rollover](./SCHEDULE.md#422-time-causing-month-rollover) + 4.3 [Limitations](./SCHEDULE.md#43-limitations) + 4.4 [The Unix build](./SCHEDULE.md#44-the-unix-build) + 5. [The cron object](./SCHEDULE.md#5-the-cron-object) For hackers and synchronous coders + 5.1 [The time to an event](./SCHEDULE.md#51-the-time-to-an-event) + 5.2 [How it works](./SCHEDULE.md#52-how-it-works) 6. [Hardware timing limitations](./SCHEDULE.md#6-hardware-timing-limitations) 7. [Use in synchronous code](./SCHEDULE.md#7-use-in-synchronous-code) If you really must @@ -39,15 +41,18 @@ and the Unix build (the latter is subject to a minor local time issue). # 2. Overview -There are two components, the `cron` object (in `sched/cron.py`) and the -`schedule` function (in `sched/sched.py`). The user creates `cron` instances, -passing arguments specifying time intervals. The `cron` instance may be run at -any time and will return the time in seconds to the next scheduled event. +The `schedule` function (`sched/sched.py`) is the interface for use with +`uasyncio`. The function takes a callback and causes that callback to run at +specified times. A coroutine may be substituted for the callback - at the +specified times it will be promoted to a `Task` and run. -The `schedule` function is an optional component for use with `uasyncio`. The -function takes a `cron` instance and a callback and causes that callback to run -at the times specified by the `cron`. A coroutine may be substituted for the -callback - at the specified times it will be promoted to a `Task` and run. +The `schedule` function instantiates a `cron` object (in `sched/cron.py`). This +is the core of the scheduler: it is a closure created with a time specifier and +returning the time to the next scheduled event. Users of `uasyncio` do not need +to deal with `cron` instances. + +This library can also be used in synchronous code, in which case `cron` +instances must explicitly be created. ##### [Top](./SCHEDULE.md#0-contents) @@ -77,21 +82,67 @@ The `crontest` script is only of interest to those wishing to adapt `cron.py`. To run error-free a bare metal target should be used for the reason discussed [here](./SCHEDULE.md#46-the-unix-build). -# 4. The cron object +# 4. The schedule function -This is a closure. It accepts a time specification for future events. Each call -when passed the current time returns the number of seconds to wait for the next -event to occur. +This enables a callback or coroutine to be run at intervals. The callable can +be specified to run once only. `schedule` is an asynchronous function. -It takes the following keyword-only args. A flexible set of data types are -accepted. These are known as `Time specifiers` and described below. Valid -numbers are shown as inclusive ranges. +Positional args: + 1. `func` The callable (callback or coroutine) to run. + 2. Any further positional args are passed to the callable. + +Keyword-only args. Args 1..6 are +[Time specifiers](./SCHEDULE.md#41-time-specifiers): a variety of data types +may be passed, but all ultimately produce integers (or `None`). Valid numbers +are shown as inclusive ranges. 1. `secs=0` Seconds (0..59). 2. `mins=0` Minutes (0..59). 3. `hrs=3` Hours (0..23). 4. `mday=None` Day of month (1..31). 5. `month=None` Months (1..12). 6. `wday=None` Weekday (0..6 Mon..Sun). + 7. `times=None` If an integer `n` is passed the callable will be run at the + next `n` scheduled times. Hence a value of 1 specifies a one-shot event. + +The `schedule` function only terminates if `times` is not `None`, and then +typically after a long time. Consequently `schedule` is usually started with +`asyncio.create_task`, as in the following example where a callback is +scheduled at various times. The code below may be run by issuing +```python +import sched.asynctest +``` +This is the demo code. +```python +import uasyncio as asyncio +from sched.sched import schedule +from time import localtime + +def foo(txt): # Demonstrate callback + yr, mo, md, h, m, s, wd = localtime()[:7] + fst = 'Callback {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}' + print(fst.format(txt, h, m, s, md, mo, yr)) + +async def bar(txt): # Demonstrate coro launch + yr, mo, md, h, m, s, wd = localtime()[:7] + fst = 'Coroutine {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}' + print(fst.format(txt, h, m, s, md, mo, yr)) + await asyncio.sleep(0) + +async def main(): + print('Asynchronous test running...') + asyncio.create_task(schedule(foo, 'every 4 mins', hrs=None, mins=range(0, 60, 4))) + asyncio.create_task(schedule(foo, 'every 5 mins', hrs=None, mins=range(0, 60, 5))) + # Launch a coroutine + asyncio.create_task(schedule(bar, 'every 3 mins', hrs=None, mins=range(0, 60, 3))) + # Launch a one-shot task + asyncio.create_task(schedule(foo, 'one shot', hrs=None, mins=range(0, 60, 2), times=1)) + await asyncio.sleep(900) # Quit after 15 minutes + +try: + asyncio.run(main()) +finally: + _ = asyncio.new_event_loop() +``` ##### [Top](./SCHEDULE.md#0-contents) @@ -105,8 +156,8 @@ The args may be of the following types. `wday=range(0, 5)` specifies weekdays. Tuples, lists, ranges or sets may be passed. -Legal ranges are listed above. Basic validation is done when a `cron` is -instantiated. +Legal integer values are listed above. Basic validation is done as soon as +`schedule` is run. Note the implications of the `None` wildcard. Setting `mins=None` will schedule the event to occur on every minute (equivalent to `*` in a Unix cron table). @@ -115,38 +166,13 @@ events must be at least two seconds apart. Default values schedule an event every day at 03.00.00. -## 4.2 The time to an event - -When the `cron` instance is run, it must be passed a time value (normally the -time now as returned by `time.time()`). The instance returns the number of -seconds to the first event matching the specifier. - -```python -from sched.cron import cron -cron1 = cron(hrs=None, mins=range(0, 60, 15)) # Every 15 minutes of every day -cron2 = cron(mday=25, month=12, hrs=9) # 9am every Christmas day -cron3 = cron(wday=(0, 4)) # 3am every Monday and Friday -now = int(time.time()) # Unix build returns a float here -tnext = min(cron1(now), cron2(now), cron3(now)) # Seconds until 1st event -``` - -##### [Top](./SCHEDULE.md#0-contents) - -## 4.3 How it works - -When a cron instance is run it seeks a future time and date relative to the -passed time value. This will be the soonest matching the specifier. A `cron` -instance is a conventional function and does not store state. Repeated calls -will return the same value if passed the same time value (`now` in the above -example). - -## 4.4 Calendar behaviour +## 4.2 Calendar behaviour Specifying a day in the month which exceeds the length of a specified month (e.g. `month=(2, 6, 7), mday=30`) will produce a `ValueError`. February is assumed to have 28 days. -### 4.4.1 Behaviour of mday and wday values +### 4.2.1 Behaviour of mday and wday values The following describes how to schedule something for (say) the second Sunday in a month, plus limitations of doing this. @@ -164,30 +190,30 @@ Specifying `wday=d` and `mday=n` where n > 22 could result in a day beyond the end of the month. It's not obvious what constitutes rational behaviour in this pathological corner case. Validation will throw a `ValueError` in this case. -### 4.4.2 Time causing month rollover +### 4.2.2 Time causing month rollover The following describes behaviour which I consider correct. On the last day of the month there are circumstances where a time specifier can -cause a day rollover. Consider application start. If a `cron` is run whose time -specifier provides only times prior to the current time, its month increments -and the day changes to the 1st. This is the soonest that the event can occur at -the specified time. +cause a day rollover. Consider application start. If a callback is scheduled +with a time specifier offering only times prior to the current time, its month +increments and the day changes to the 1st. This is the soonest that the event +can occur at the specified time. Consider the case where the next month is disallowed. In this case the month will change to the next valid month. This code, run at 9am on 31st July, would -aim to run the event at 1.59 on 1st October. +aim to run `foo` at 1.59 on 1st October. ```python -my_cron(month=(2, 7, 10), hrs=1, mins=59) # moves forward 1 day -t_wait = my_cron(time.time()) # Next month is disallowed so jumps to October +asyncio.create_task(schedule(foo, month=(2, 7, 10), hrs=1, mins=59)) ``` ##### [Top](./SCHEDULE.md#0-contents) -## 4.5 Limitations +## 4.3 Limitations -The `cron` code has a resolution of 1 second. It is intended for scheduling -infrequent events (`uasyncio` is recommended for doing fast scheduling). +The underlying `cron` code has a resolution of 1 second. The library is +intended for scheduling infrequent events (`uasyncio` has its own approach to +fast scheduling). Specifying `secs=None` will cause a `ValueError`. The minimum interval between scheduled events is 2 seconds. Attempts to schedule events with a shorter gap @@ -200,7 +226,7 @@ On hardware platforms the MicroPython `time` module does not handle daylight saving time. Scheduled times are relative to system time. This does not apply to the Unix build where daylight saving needs to be considered. -## 4.6 The Unix build +## 4.4 The Unix build Asynchronous use requires `uasyncio` V3, so ensure this is installed on the Linux target. @@ -221,61 +247,50 @@ metal targets. ##### [Top](./SCHEDULE.md#0-contents) -# 5. The schedule function +# 5. The cron object -This enables a callback or coroutine to be run at intervals specified by a -`cron` instance. An option for one-shot use is available. It is an asynchronous -function. Positional args: - 1. `fcron` A `cron` instance. - 2. `routine` The callable (callback or coroutine) to run. - 3. `args=()` A tuple of args for the callable. - 4. `run_once=False` If `True` the callable will be run once only. +This is the core of the scheduler. Users of `uasyncio` do not need to concern +themseleves with it. It is documented for those wishing to modify the code and +for those wanting to perform scheduling in synchronous code. -The `schedule` function only terminates if `run_once=True`, and then typically -after a long time. Usually `schedule` is started with `asyncio.create_task`, as -in the following example where a callback is scheduled at various times. The -code below may be run by issuing -```python -import sched.asynctest -``` -This is the demo code. -```python -import uasyncio as asyncio -from sched.sched import schedule -from sched.cron import cron -from time import localtime +It is a closure whose creation accepts a time specification for future events. +Each subsequent call is passed the current time and returns the number of +seconds to wait for the next event to occur. -def foo(txt): # Demonstrate callback - yr, mo, md, h, m, s, wd = localtime()[:7] - fst = 'Callback {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}' - print(fst.format(txt, h, m, s, md, mo, yr)) +It takes the following keyword-only args. A flexible set of data types are +accepted namely [time specifiers](./SCHEDULE.md#41-time-specifiers). Valid +numbers are shown as inclusive ranges. + 1. `secs=0` Seconds (0..59). + 2. `mins=0` Minutes (0..59). + 3. `hrs=3` Hours (0..23). + 4. `mday=None` Day of month (1..31). + 5. `month=None` Months (1..12). + 6. `wday=None` Weekday (0..6 Mon..Sun). -async def bar(txt): # Demonstrate coro launch - yr, mo, md, h, m, s, wd = localtime()[:7] - fst = 'Coroutine {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}' - print(fst.format(txt, h, m, s, md, mo, yr)) - await asyncio.sleep(0) +## 5.1 The time to an event -async def main(): - print('Asynchronous test running...') - cron4 = cron(hrs=None, mins=range(0, 60, 4)) - asyncio.create_task(schedule(cron4, foo, ('every 4 mins',))) +When the `cron` instance is run, it must be passed a time value (normally the +time now as returned by `time.time()`). The instance returns the number of +seconds to the first event matching the specifier. - cron5 = cron(hrs=None, mins=range(0, 60, 5)) - asyncio.create_task(schedule(cron5, foo, ('every 5 mins',))) +```python +from sched.cron import cron +cron1 = cron(hrs=None, mins=range(0, 60, 15)) # Every 15 minutes of every day +cron2 = cron(mday=25, month=12, hrs=9) # 9am every Christmas day +cron3 = cron(wday=(0, 4)) # 3am every Monday and Friday +now = int(time.time()) # Unix build returns a float here +tnext = min(cron1(now), cron2(now), cron3(now)) # Seconds until 1st event +``` - cron3 = cron(hrs=None, mins=range(0, 60, 3)) # Launch a coroutine - asyncio.create_task(schedule(cron3, bar, ('every 3 mins',))) +##### [Top](./SCHEDULE.md#0-contents) - cron2 = cron(hrs=None, mins=range(0, 60, 2)) - asyncio.create_task(schedule(cron2, foo, ('one shot',), True)) - await asyncio.sleep(900) # Quit after 15 minutes +## 5.2 How it works -try: - asyncio.run(main()) -finally: - _ = asyncio.new_event_loop() -``` +When a cron instance is run it seeks a future time and date relative to the +passed time value. This will be the soonest matching the specifier. A `cron` +instance is a conventional function and does not store state. Repeated calls +will return the same value if passed the same time value (`now` in the above +example). ##### [Top](./SCHEDULE.md#0-contents) @@ -354,7 +369,8 @@ main() In my opinion the asynchronous version is cleaner and easier to understand. It is also more versatile because the advanced features of `uasyncio` are -available to the application. The above code is incompatible with `uasyncio` -because of the blocking calls to `time.sleep()`. +available to the application including cancellation of scheduled tasks. The +above code is incompatible with `uasyncio` because of the blocking calls to +`time.sleep()`. ##### [Top](./SCHEDULE.md#0-contents) From c7702d7f8c684e8bd060f1e511805bf2479c0531 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 19 Jul 2020 17:42:35 +0100 Subject: [PATCH 031/305] v3/as_drivers/sched schedule now returns callback result. --- v3/as_drivers/sched/sched.py | 3 ++- v3/docs/SCHEDULE.md | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/v3/as_drivers/sched/sched.py b/v3/as_drivers/sched/sched.py index 24857fe..bbf2a2b 100644 --- a/v3/as_drivers/sched/sched.py +++ b/v3/as_drivers/sched/sched.py @@ -17,7 +17,8 @@ async def schedule(func, *args, times=None, **kwargs): tw = min(tw, maxt) await asyncio.sleep(tw) tw -= maxt - launch(func, args) + res = launch(func, args) if times is not None: times -= 1 await asyncio.sleep_ms(1200) # ensure we're into next second + return res diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index aca223f..448f0f0 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -85,7 +85,8 @@ To run error-free a bare metal target should be used for the reason discussed # 4. The schedule function This enables a callback or coroutine to be run at intervals. The callable can -be specified to run once only. `schedule` is an asynchronous function. +be specified to run forever, once only or a fixed number of times. `schedule` +is an asynchronous function. Positional args: 1. `func` The callable (callback or coroutine) to run. @@ -104,8 +105,11 @@ are shown as inclusive ranges. 7. `times=None` If an integer `n` is passed the callable will be run at the next `n` scheduled times. Hence a value of 1 specifies a one-shot event. -The `schedule` function only terminates if `times` is not `None`, and then -typically after a long time. Consequently `schedule` is usually started with +The `schedule` function only terminates if `times` is not `None`. In this case +termination occurs after the last run of the callable and the return value is +the value returned by that run of the callable. + +Because `schedule` does not terminate promptly it is usually started with `asyncio.create_task`, as in the following example where a callback is scheduled at various times. The code below may be run by issuing ```python From fad63814d123a609a8d8fa90002c323848ee356a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 24 Jul 2020 06:44:34 +0100 Subject: [PATCH 032/305] v3 tutorial: correct asynchronous iterator __aiter__ method. --- v3/docs/TUTORIAL.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index efac063..b752a49 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1242,7 +1242,7 @@ class AsyncIterable: self.data = (1, 2, 3, 4, 5) self.index = 0 - async def __aiter__(self): + def __aiter__(self): # See note below return self async def __anext__(self): @@ -1266,6 +1266,9 @@ async def run(): print(x) asyncio.run(run()) ``` +The `__aiter__` method was formerly an asynchronous method. CPython 3.6 accepts +synchronous or asynchronous methods. CPython 3.8 and MicroPython require +synchronous code [ref](https://github.com/micropython/micropython/pull/6272). ###### [Contents](./TUTORIAL.md#contents) From 75c1b7ce5b6d0650196b0a0959a1f655c3418d80 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 3 Aug 2020 18:47:08 +0100 Subject: [PATCH 033/305] GPS.md Add note about how to run special test scripts. --- v3/docs/GPS.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/v3/docs/GPS.md b/v3/docs/GPS.md index c01d7ba..5e1bb84 100644 --- a/v3/docs/GPS.md +++ b/v3/docs/GPS.md @@ -838,13 +838,17 @@ These tests allow NMEA parsing to be verified in the absence of GPS hardware: * `astests_pyb.py` Test with synthetic data on UART. GPS hardware replaced by a loopback on UART 4. Requires a Pyboard. - * `astests.py` Test with synthetic data. Run on CPython 3.8+ or MicroPython. - Run as follows: + * `astests.py` Test with synthetic data. Run on a PC under CPython 3.8+ or + MicroPython. Run from the `v3` directory at the REPL as follows: ```python from as_drivers.as_GPS.astests import run_tests run_tests() ``` +or at the command line: +```bash +$ micropython -m as_drivers.as_GPS.astests +``` ###### [Top](./GPS.md#1-as_gps) From 4f28d9fe61b7c3bc2ae8603303a3e181cfbd6706 Mon Sep 17 00:00:00 2001 From: "David B. Adrian" Date: Wed, 5 Aug 2020 22:39:58 +0200 Subject: [PATCH 034/305] Fixes incorrect clearing of old states in queue's event objects --- v3/primitives/queue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/primitives/queue.py b/v3/primitives/queue.py index 123c778..cb4b653 100644 --- a/v3/primitives/queue.py +++ b/v3/primitives/queue.py @@ -33,8 +33,8 @@ def _get(self): async def get(self): # Usage: item = await queue.get() if self.empty(): # Queue is empty, put the calling Task on the waiting queue - await self._evput.wait() self._evput.clear() + await self._evput.wait() return self._get() def get_nowait(self): # Remove and return an item from the queue. @@ -50,8 +50,8 @@ def _put(self, val): async def put(self, val): # Usage: await queue.put(item) if self.qsize() >= self.maxsize and self.maxsize: # Queue full - await self._evget.wait() self._evget.clear() + await self._evget.wait() # Task(s) waiting to get from queue, schedule first Task self._put(val) From c7f07e1925bd6495e193cfa7b4a3ff38565359de Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 6 Aug 2020 11:37:23 +0100 Subject: [PATCH 035/305] v3/primitives/queue.py Optimisation of put_nowait. --- v3/primitives/queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/primitives/queue.py b/v3/primitives/queue.py index cb4b653..dab568c 100644 --- a/v3/primitives/queue.py +++ b/v3/primitives/queue.py @@ -56,7 +56,7 @@ async def put(self, val): # Usage: await queue.put(item) self._put(val) def put_nowait(self, val): # Put an item into the queue without blocking. - if self.qsize() >= self.maxsize and self.maxsize: + if self.maxsize and self.qsize() >= self.maxsize: raise QueueFull() self._put(val) From a16285ed6434fc3a49517fdae967e030e0881082 Mon Sep 17 00:00:00 2001 From: "David B. Adrian" Date: Thu, 6 Aug 2020 13:24:59 +0200 Subject: [PATCH 036/305] Full check optimization in v3 queue --- v3/primitives/queue.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/v3/primitives/queue.py b/v3/primitives/queue.py index dab568c..da5eeb8 100644 --- a/v3/primitives/queue.py +++ b/v3/primitives/queue.py @@ -48,7 +48,7 @@ def _put(self, val): self._queue.append(val) async def put(self, val): # Usage: await queue.put(item) - if self.qsize() >= self.maxsize and self.maxsize: + if self.full(): # Queue full self._evget.clear() await self._evget.wait() @@ -56,7 +56,7 @@ async def put(self, val): # Usage: await queue.put(item) self._put(val) def put_nowait(self, val): # Put an item into the queue without blocking. - if self.maxsize and self.qsize() >= self.maxsize: + if self.full(): raise QueueFull() self._put(val) @@ -67,10 +67,6 @@ def empty(self): # Return True if the queue is empty, False otherwise. return len(self._queue) == 0 def full(self): # Return True if there are maxsize items in the queue. - # Note: if the Queue was initialized with maxsize=0 (the default), - # then full() is never True. - - if self.maxsize <= 0: - return False - else: - return self.qsize() >= self.maxsize + # Note: if the Queue was initialized with maxsize=0 (the default) or + # any negative number, then full() is never True. + return self.maxsize > 0 and self.qsize() >= self.maxsize \ No newline at end of file From 89f00796e271e820c0ce885711aed3ba392560d4 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 6 Aug 2020 13:58:23 +0100 Subject: [PATCH 037/305] v3/primitives/queue.py Add trailing newline. --- v3/primitives/queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/primitives/queue.py b/v3/primitives/queue.py index da5eeb8..2b14687 100644 --- a/v3/primitives/queue.py +++ b/v3/primitives/queue.py @@ -69,4 +69,4 @@ def empty(self): # Return True if the queue is empty, False otherwise. def full(self): # Return True if there are maxsize items in the queue. # Note: if the Queue was initialized with maxsize=0 (the default) or # any negative number, then full() is never True. - return self.maxsize > 0 and self.qsize() >= self.maxsize \ No newline at end of file + return self.maxsize > 0 and self.qsize() >= self.maxsize From f874ac4243f708f0fab53861d4eddc2a89004775 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 12 Aug 2020 13:35:16 +0100 Subject: [PATCH 038/305] Add as_drivers/client_server alpha code. --- v3/as_drivers/client_server/heartbeat.py | 26 ++++++++++++ v3/as_drivers/client_server/uclient.py | 53 ++++++++++++++++++++++++ v3/as_drivers/client_server/userver.py | 52 +++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 v3/as_drivers/client_server/heartbeat.py create mode 100644 v3/as_drivers/client_server/uclient.py create mode 100644 v3/as_drivers/client_server/userver.py diff --git a/v3/as_drivers/client_server/heartbeat.py b/v3/as_drivers/client_server/heartbeat.py new file mode 100644 index 0000000..68a821e --- /dev/null +++ b/v3/as_drivers/client_server/heartbeat.py @@ -0,0 +1,26 @@ +# flash.py Heartbeat code for simple uasyncio-based echo server + +# Released under the MIT licence +# Copyright (c) Peter Hinch 2019 + +import uasyncio as asyncio +from sys import platform + + +async def heartbeat(tms): + if platform == 'pyboard': # V1.x or D series + from pyb import LED + led = LED(1) + elif platform == 'esp8266': + from machine import Pin + led = Pin(2, Pin.OUT, value=1) + elif platform == 'linux': + return # No LED + else: + raise OSError('Unsupported platform.') + while True: + if platform == 'pyboard': + led.toggle() + elif platform == 'esp8266': + led(not led()) + await asyncio.sleep_ms(tms) diff --git a/v3/as_drivers/client_server/uclient.py b/v3/as_drivers/client_server/uclient.py new file mode 100644 index 0000000..e6f4acf --- /dev/null +++ b/v3/as_drivers/client_server/uclient.py @@ -0,0 +1,53 @@ +# uclient.py Demo of simple uasyncio-based client for echo server + +# Released under the MIT licence +# Copyright (c) Peter Hinch 2019-2020 + +import usocket as socket +import uasyncio as asyncio +import ujson +from heartbeat import heartbeat # Optional LED flash + +server = '192.168.0.41' +port = 8123 + +async def run(): + # Optional fast heartbeat to confirm nonblocking operation + asyncio.create_task(heartbeat(100)) + sock = socket.socket() + def close(): + sock.close() + print('Server disconnect.') + try: + serv = socket.getaddrinfo(server, port)[0][-1] + sock.connect(serv) + except OSError as e: + print('Cannot connect to {} on port {}'.format(server, port)) + sock.close() + return + while True: + sreader = asyncio.StreamReader(sock) + swriter = asyncio.StreamWriter(sock, {}) + data = ['value', 1] + while True: + try: + swriter.write('{}\n'.format(ujson.dumps(data))) + await swriter.drain() + res = await sreader.readline() + except OSError: + close() + return + try: + print('Received', ujson.loads(res)) + except ValueError: + close() + return + await asyncio.sleep(2) + data[1] += 1 + +try: + asyncio.run(run()) +except KeyboardInterrupt: + print('Interrupted') # This mechanism doesn't work on Unix build. +finally: + _ = asyncio.new_event_loop() diff --git a/v3/as_drivers/client_server/userver.py b/v3/as_drivers/client_server/userver.py new file mode 100644 index 0000000..4263d2a --- /dev/null +++ b/v3/as_drivers/client_server/userver.py @@ -0,0 +1,52 @@ +# userver.py Demo of simple uasyncio-based echo server + +# Released under the MIT licence +# Copyright (c) Peter Hinch 2019 + +import usocket as socket +import uasyncio as asyncio +import uselect as select +import ujson +from heartbeat import heartbeat # Optional LED flash + +class Server: + + async def run(self, port=8123): + print('Awaiting client connection.') + self.cid = 0 + asyncio.create_task(heartbeat(100)) + self.server = await asyncio.start_server(self.run_client, '0.0.0.0', port) + while True: + await asyncio.sleep(100) + + async def run_client(self, sreader, swriter): + self.cid += 1 + print('Got connection from client', self.cid) + try: + while True: + res = await sreader.readline() + if res == b'': + raise OSError + print('Received {} from client {}'.format(ujson.loads(res.rstrip()), self.cid)) + swriter.write(res) + await swriter.drain() # Echo back + except OSError: + pass + print('Client {} disconnect.'.format(self.cid)) + await sreader.wait_closed() + print('Client {} socket closed.'.format(self.cid)) + + def close(self): + print('Closing server') + self.server.close() + await self.server.wait_closed() + print('Server closed') + +server = Server() +try: + asyncio.run(server.run()) +except KeyboardInterrupt: + print('Interrupted') # This mechanism doesn't work on Unix build. +finally: + server.close() + _ = asyncio.new_event_loop() From 1e6c2a2cbab69d4c70d67b584503a89947d7bc45 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 12 Aug 2020 14:35:27 +0100 Subject: [PATCH 039/305] v3/as_drivers/client_server/userver Add client timeout. --- v3/as_drivers/client_server/userver.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/v3/as_drivers/client_server/userver.py b/v3/as_drivers/client_server/userver.py index 4263d2a..0bebc5f 100644 --- a/v3/as_drivers/client_server/userver.py +++ b/v3/as_drivers/client_server/userver.py @@ -1,7 +1,7 @@ # userver.py Demo of simple uasyncio-based echo server # Released under the MIT licence -# Copyright (c) Peter Hinch 2019 +# Copyright (c) Peter Hinch 2019-2020 import usocket as socket import uasyncio as asyncio @@ -11,11 +11,17 @@ class Server: - async def run(self, port=8123): + def __init__(self, host='0.0.0.0', port=8123, backlog=5, timeout=20): + self.host = host + self.port = port + self.backlog = backlog + self.timeout = timeout + + async def run(self): print('Awaiting client connection.') self.cid = 0 asyncio.create_task(heartbeat(100)) - self.server = await asyncio.start_server(self.run_client, '0.0.0.0', port) + self.server = await asyncio.start_server(self.run_client, self.host, self.port, self.backlog) while True: await asyncio.sleep(100) @@ -24,7 +30,10 @@ async def run_client(self, sreader, swriter): print('Got connection from client', self.cid) try: while True: - res = await sreader.readline() + try: + res = await asyncio.wait_for(sreader.readline(), self.timeout) + except asyncio.TimeoutError: + res = b'' if res == b'': raise OSError print('Received {} from client {}'.format(ujson.loads(res.rstrip()), self.cid)) From d14adb7d8979cd410499ad67475e707c661a078c Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 22 Aug 2020 19:02:19 +0100 Subject: [PATCH 040/305] V3/docs/DRIVERS.md improve description of Pushbutton suppress ctor arg. --- v3/docs/DRIVERS.md | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 8f44336..11b7cfe 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -205,19 +205,25 @@ number of coroutines. ### 4.1.1 The suppress constructor argument When the button is pressed `press_func` runs immediately. This minimal latency -is ideal for applications such as games, but does imply that in the event of a -long press, both `press_func` and `long_func` run: `press_func` immediately and -`long_func` if the button is still pressed when the timer has elapsed. Similar -reasoning applies to the double click function. +is ideal for applications such as games. Consider a long press: `press_func` +runs initially, then `long_func`, and finally `release_func`. In the case of a +double-click `press_func` and `release_func` will run twice; `double_func` runs +once. There can be a need for a `callable` which runs if a button is pressed but -only if a doubleclick or long press function does not run. The soonest that the -absence of a long press can be detected is on button release. The absence of a -double click can only be detected when the double click timer times out without -a second press occurring. +only if a doubleclick or long press function does not run. The `suppress` arg +changes the behaviour of `release_func` to fill that role. This has timing +implications. -This `callable` is the `release_func`. If the `suppress` constructor arg is -set, `release_func` will be launched as follows: +The soonest that the absence of a long press can be detected is on button +release. Absence of a double click can only be detected when the double click +timer times out without a second press occurring. + +Note `suppress` affects the behaviour of `release_func` only. Other callbacks +including `press_func` behave normally. + +If the `suppress` constructor arg is set, `release_func` will be launched as +follows: 1. If `double_func` does not exist on rapid button release. 2. If `double_func` exists, after the expiration of the doubleclick timer. 3. If `long_func` exists and the press duration causes `long_func` to be @@ -225,6 +231,12 @@ set, `release_func` will be launched as follows: 4. If `double_func` exists and a double click occurs, `release_func` will not be launched. +In the typical case where `long_func` and `double_func` are both defined, this +ensures that only one of `long_func`, `double_func` and `release_func` run. In +the case of a single short press, `release_func` will be delayed until the +expiry of the double-click timer (because until that time a second click might +occur). + ### 4.1.2 The sense constructor argument In most applications it can be assumed that, at power-up, pushbuttons are not From d76d075c271380318373aa3948e3452911c2224b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 29 Aug 2020 15:58:46 +0100 Subject: [PATCH 041/305] v3/primitives/queue.py Fix bug with multiple competing get/put tasks. --- v3/primitives/queue.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/v3/primitives/queue.py b/v3/primitives/queue.py index 2b14687..a636bdb 100644 --- a/v3/primitives/queue.py +++ b/v3/primitives/queue.py @@ -31,8 +31,9 @@ def _get(self): return self._queue.pop(0) async def get(self): # Usage: item = await queue.get() - if self.empty(): - # Queue is empty, put the calling Task on the waiting queue + while self.empty(): # May be multiple tasks waiting on get() + # Queue is empty, suspend task until a put occurs + # 1st of N tasks gets, the rest loop again self._evput.clear() await self._evput.wait() return self._get() @@ -48,7 +49,7 @@ def _put(self, val): self._queue.append(val) async def put(self, val): # Usage: await queue.put(item) - if self.full(): + while self.full(): # Queue full self._evget.clear() await self._evget.wait() From 68f8ec15c9fc7b4d437dbf2df020c197de85c535 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 30 Aug 2020 17:50:27 +0100 Subject: [PATCH 042/305] v3/primitives/queue.py Bugfix - asyntest.py tests Queue more thoroughly. --- v3/primitives/queue.py | 8 ++-- v3/primitives/tests/asyntest.py | 78 ++++++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/v3/primitives/queue.py b/v3/primitives/queue.py index a636bdb..405c857 100644 --- a/v3/primitives/queue.py +++ b/v3/primitives/queue.py @@ -27,14 +27,14 @@ def __init__(self, maxsize=0): self._evget = asyncio.Event() # Triggered by get, tested by put def _get(self): - self._evget.set() + self._evget.set() # Schedule all tasks waiting on get + self._evget.clear() return self._queue.pop(0) async def get(self): # Usage: item = await queue.get() while self.empty(): # May be multiple tasks waiting on get() # Queue is empty, suspend task until a put occurs # 1st of N tasks gets, the rest loop again - self._evput.clear() await self._evput.wait() return self._get() @@ -45,13 +45,13 @@ def get_nowait(self): # Remove and return an item from the queue. return self._get() def _put(self, val): - self._evput.set() + self._evput.set() # Schedule tasks waiting on put + self._evput.clear() self._queue.append(val) async def put(self, val): # Usage: await queue.put(item) while self.full(): # Queue full - self._evget.clear() await self._evget.wait() # Task(s) waiting to get from queue, schedule first Task self._put(val) diff --git a/v3/primitives/tests/asyntest.py b/v3/primitives/tests/asyntest.py index c827a97..ac0278b 100644 --- a/v3/primitives/tests/asyntest.py +++ b/v3/primitives/tests/asyntest.py @@ -374,39 +374,97 @@ def condition_test(): # ************ Queue test ************ from primitives.queue import Queue -q = Queue() async def slow_process(): await asyncio.sleep(2) return 42 -async def bar(): +async def bar(q): print('Waiting for slow process.') result = await slow_process() print('Putting result onto queue') await q.put(result) # Put result on q -async def foo(): +async def foo(q): print("Running foo()") result = await q.get() print('Result was {}'.format(result)) -async def queue_go(delay): - asyncio.create_task(foo()) - asyncio.create_task(bar()) - await asyncio.sleep(delay) +async def q_put(n, q): + for x in range(8): + obj = (n, x) + await q.put(obj) + await asyncio.sleep(0) + +async def q_get(n, q): + for x in range(8): + await q.get() + await asyncio.sleep(0) + +async def putter(q): + # put some item, then sleep + for _ in range(20): + await q.put(1) + await asyncio.sleep_ms(50) + + +async def getter(q): + # checks for new items, and relies on the "blocking" of the get method + for _ in range(20): + await q.get() + +async def queue_go(): + q = Queue(10) + asyncio.create_task(foo(q)) + asyncio.create_task(bar(q)) + await asyncio.sleep(3) + for n in range(4): + asyncio.create_task(q_put(n, q)) + await asyncio.sleep(1) + assert q.qsize() == 10 + await q.get() + await asyncio.sleep(0.1) + assert q.qsize() == 10 + while not q.empty(): + await q.get() + await asyncio.sleep(0.1) + assert q.empty() + print('Competing put tasks test complete') + + for n in range(4): + asyncio.create_task(q_get(n, q)) + await asyncio.sleep(1) + x = 0 + while not q.full(): + await q.put(x) + await asyncio.sleep(0.3) + x += 1 + assert q.qsize() == 10 + print('Competing get tasks test complete') + await asyncio.gather( + putter(q), + getter(q) + ) + print('Queue tests complete') print("I've seen starships burn off the shoulder of Orion...") print("Time to die...") def queue_test(): - printexp('''Running (runtime = 3s): + printexp('''Running (runtime = 20s): Running foo() Waiting for slow process. Putting result onto queue +Result was 42 +Competing put tasks test complete +Competing get tasks test complete +Queue tests complete + + I've seen starships burn off the shoulder of Orion... Time to die... -''', 3) - asyncio.run(queue_go(3)) + +''', 20) + asyncio.run(queue_go()) def test(n): try: From 78d22a404a84bf10e41f46b3e015550f4b3ed6c3 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 1 Sep 2020 09:29:54 +0100 Subject: [PATCH 043/305] v3 Simplify Barrier class. Improve test and tutorial. --- v3/docs/TUTORIAL.md | 150 ++++++++++++++++---------------- v3/primitives/barrier.py | 46 ++++------ v3/primitives/tests/asyntest.py | 30 +++++-- 3 files changed, 114 insertions(+), 112 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index b752a49..9967fcd 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -650,49 +650,37 @@ Synchronous Methods: Asynchronous Method: * `wait` Pause until event is set. -Coros waiting on the event issue `await event.wait()` when execution pauses until -another issues `event.set()`. - -This presents a problem if `event.set()` is issued in a looping construct; the -code must wait until the event has been accessed by all waiting tasks before -setting it again. In the case where a single task is awaiting the event this -can be achieved by the receiving task clearing the event: - -```python -async def eventwait(event): - await event.wait() - # Process the data - event.clear() # Tell the caller it's ready for more -``` - -The task raising the event checks that it has been serviced: - -```python -async def foo(event): - while True: - # Acquire data from somewhere - while event.is_set(): - await asyncio.sleep(1) # Wait for task to be ready - # Data is available to the task, so alert it: - event.set() -``` - -Where multiple tasks wait on a single event synchronisation can be achieved by -means of an acknowledge event. Each task needs a separate event. - -```python -async def eventwait(event, ack_event): - await event.wait() - ack_event.set() -``` - -This is cumbersome. In most cases - even those with a single waiting task - the -Barrier class offers a simpler approach. +Tasks wait on the event by issuing `await event.wait()`; execution pauses until +another issues `event.set()`. This causes all tasks waiting on the `Event` to +be queued for execution. Note that the synchronous sequence +```python +event.set() +event.clear() +``` +will cause waiting task(s) to resume in round-robin order. + +The `Event` class is an efficient and effective way to synchronise tasks, but +firmware applications often have multiple tasks running `while True:` loops. +The number of `Event` instances required to synchronise these can multiply. +Consider the case of one producer task feeding N consumers. The producer sets +an `Event` to tell the consumer that data is ready; it then needs to wait until +all consumers have completed before triggering them again. Consider these +approaches: + 1. Each consumer sets an `Event` on completion. Producer waits until all + `Event`s are set before clearing them and setting its own `Event`. + 2. Consumers do not loop, running to completion. Producer uses `gather` to + instantiate consumer tasks and wait on their completion. + 3. `Event`s are replaced with a single [Barrier](./TUTORIAL.md#37-barrier) + instance. + +Solution 1 suffers a proliferation of `Event`s and suffers an inefficient +busy-wait where the producer waits on N events. Solution 2 is inefficient with +constant creation of tasks. Arguably the `Barrier` class is the best approach. **NOTE NOT YET SUPPORTED - see Message class** An Event can also provide a means of communication between an interrupt handler and a task. The handler services the hardware and sets an event which is tested -in slow time by the task. +in slow time by the task. See [PR6106](https://github.com/micropython/micropython/pull/6106). ###### [Contents](./TUTORIAL.md#contents) @@ -888,9 +876,9 @@ asyncio.run(queue_go(4)) ## 3.6 Message -This is an unofficial primitive and has no counterpart in CPython asyncio. +This is an unofficial primitive with no counterpart in CPython asyncio. -This is a minor adaptation of the `Event` class. It provides the following: +This is similar to the `Event` class. It provides the following: * `.set()` has an optional data payload. * `.set()` is capable of being called from a hard or soft interrupt service routine - a feature not yet available in the more efficient official `Event`. @@ -927,11 +915,14 @@ async def main(): asyncio.run(main()) ``` - A `Message` can provide a means of communication between an interrupt handler and a task. The handler services the hardware and issues `.set()` which is tested in slow time by the task. +Currently its behaviour differs from that of `Event` where multiple tasks wait +on a `Message`. This may change: it is therefore recommended to use `Message` +instances with only one receiving task. + ###### [Contents](./TUTORIAL.md#contents) ## 3.7 Barrier @@ -964,6 +955,47 @@ would imply instantiating a set of tasks on every pass of the loop. passing a barrier does not imply return. `Barrier` now has an efficient implementation using `Event` to suspend waiting tasks. +The following is a typical usage example. A data provider acquires data from +some hardware and transmits it concurrently on a number of interefaces. These +run at different speeds. The `Barrier` synchronises these loops. This can run +on a Pyboard. +```python +import uasyncio as asyncio +from primitives.barrier import Barrier +from machine import UART +import ujson + +data = None +async def provider(barrier): + global data + n = 0 + while True: + n += 1 # Get data from some source + data = ujson.dumps([n, 'the quick brown fox jumps over the lazy dog']) + print('Provider triggers senders') + await barrier # Free sender tasks + print('Provider waits for last sender to complete') + await barrier + +async def sender(barrier, swriter, n): + while True: + await barrier # Provider has got data + swriter.write(data) + await swriter.drain() + print('UART', n, 'sent', data) + await barrier # Trigger provider when last sender has completed + +async def main(): + sw1 = asyncio.StreamWriter(UART(1, 9600), {}) + sw2 = asyncio.StreamWriter(UART(2, 1200), {}) + barrier = Barrier(3) + for n, sw in enumerate((sw1, sw2)): + asyncio.create_task(sender(barrier, sw, n + 1)) + await provider(barrier) + +asyncio.run(main()) +``` + Constructor. Mandatory arg: * `participants` The number of coros which will use the barrier. @@ -972,8 +1004,8 @@ Optional args: * `args` Tuple of args for the callback. Default `()`. Public synchronous methods: - * `busy` No args. Returns `True` if at least one coro is waiting on the - barrier, or if at least one non-waiting coro has not triggered it. + * `busy` No args. Returns `True` if at least one task is waiting on the + barrier. * `trigger` No args. The barrier records that the coro has passed the critical point. Returns "immediately". * `result` No args. If a callback was provided, returns the return value from @@ -1000,36 +1032,6 @@ passed the barrier, and all waiting coros have reached it. At that point all waiting coros will resume. A non-waiting coro issues `barrier.trigger()` to indicate that is has passed the critical point. -```python -import uasyncio as asyncio -from uasyncio import Event -from primitives.barrier import Barrier - -def callback(text): - print(text) - -async def report(num, barrier, event): - for i in range(5): - # De-synchronise for demo - await asyncio.sleep_ms(num * 50) - print('{} '.format(i), end='') - await barrier - event.set() - -async def main(): - barrier = Barrier(3, callback, ('Synch',)) - event = Event() - for num in range(3): - asyncio.create_task(report(num, barrier, event)) - await event.wait() - -asyncio.run(main()) -``` - -multiple instances of `report` print their result and pause until the other -instances are also complete and waiting on `barrier`. At that point the -callback runs. On its completion the tasks resume. - ###### [Contents](./TUTORIAL.md#contents) ## 3.8 Delay_ms class diff --git a/v3/primitives/barrier.py b/v3/primitives/barrier.py index 70b3b81..445f4ed 100644 --- a/v3/primitives/barrier.py +++ b/v3/primitives/barrier.py @@ -19,22 +19,16 @@ class Barrier(): def __init__(self, participants, func=None, args=()): self._participants = participants + self._count = participants self._func = func self._args = args - self._reset(True) self._res = None self._evt = asyncio.Event() def __await__(self): if self.trigger(): - return - - direction = self._down - while True: # Wait until last waiting task changes the direction - if direction != self._down: - return - await self._evt.wait() - self._evt.clear() + return # Other tasks have already reached barrier + await self._evt.wait() # Wait until last task reaches it __iter__ = __await__ @@ -42,28 +36,18 @@ def result(self): return self._res def trigger(self): - self._count += -1 if self._down else 1 - if self._count < 0 or self._count > self._participants: + self._count -=1 + if self._count < 0: raise ValueError('Too many tasks accessing Barrier') - self._evt.set() - if self._at_limit(): # All other tasks are also at limit - if self._func is not None: - self._res = launch(self._func, self._args) - self._reset(not self._down) # Toggle direction to release others - return True - return False - - def _reset(self, down): - self._down = down - self._count = self._participants if down else 0 + if self._count > 0: + return False # At least 1 other task has not reached barrier + # All other tasks are waiting + if self._func is not None: + self._res = launch(self._func, self._args) + self._count = self._participants + self._evt.set() # Release others + self._evt.clear() + return True def busy(self): - if self._down: - done = self._count == self._participants - else: - done = self._count == 0 - return not done - - def _at_limit(self): # Has count reached up or down limit? - limit = 0 if self._down else self._participants - return self._count == limit + return self._count < self._participants diff --git a/v3/primitives/tests/asyntest.py b/v3/primitives/tests/asyntest.py index ac0278b..9d07289 100644 --- a/v3/primitives/tests/asyntest.py +++ b/v3/primitives/tests/asyntest.py @@ -23,7 +23,7 @@ def print_tests(): st = '''Available functions: test(0) Print this list. test(1) Test message acknowledge. -test(2) Test Messge and Lock objects. +test(2) Test Message and Lock objects. test(3) Test the Barrier class with callback. test(4) Test the Barrier class with coroutine. test(5) Test Semaphore @@ -175,17 +175,29 @@ async def report(barrier): print('{} '.format(i), end='') await barrier +async def do_barrier_test(): + barrier = Barrier(3, callback, ('Synch',)) + for _ in range(2): + for _ in range(3): + asyncio.create_task(report(barrier)) + await asyncio.sleep(1) + print() + await asyncio.sleep(1) + def barrier_test(): - printexp('''0 0 0 Synch + printexp('''Running (runtime = 3s): +0 0 0 Synch 1 1 1 Synch 2 2 2 Synch 3 3 3 Synch 4 4 4 Synch -''') - barrier = Barrier(3, callback, ('Synch',)) - for _ in range(3): - asyncio.create_task(report(barrier)) - asyncio.run(killer(2)) + +1 1 1 Synch +2 2 2 Synch +3 3 3 Synch +4 4 4 Synch +''', 3) + asyncio.run(do_barrier_test()) # ************ Barrier test 1 ************ @@ -208,7 +220,11 @@ async def bart(): barrier = Barrier(4, my_coro, ('my_coro running',)) for x in range(3): asyncio.create_task(report1(barrier, x)) + await asyncio.sleep(4) + assert barrier.busy() await barrier + await asyncio.sleep(0) + assert not barrier.busy() # Must yield before reading result(). Here we wait long enough for await asyncio.sleep_ms(1500) # coro to print barrier.result().cancel() From c2f7f46db7b8f3fe95d8eb04406ebd8326f68e24 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 6 Sep 2020 16:02:08 +0100 Subject: [PATCH 044/305] Move V2 resources to v2 directory. --- DRIVERS.md => v2/DRIVERS.md | 0 FASTPOLL.md => v2/FASTPOLL.md | 0 {HD44780 => v2/HD44780}/README.md | 0 {HD44780 => v2/HD44780}/alcd.py | 0 {HD44780 => v2/HD44780}/alcdtest.py | 0 PRIMITIVES.md => v2/PRIMITIVES.md | 0 v2/README.md | 166 +++++++ TUTORIAL.md => v2/TUTORIAL.md | 0 UNDER_THE_HOOD.md => v2/UNDER_THE_HOOD.md | 0 aledflash.py => v2/aledflash.py | 0 apoll.py => v2/apoll.py | 0 aqtest.py => v2/aqtest.py | 0 astests.py => v2/astests.py | 0 v2/aswitch.py | 231 +++++++++ v2/asyn.py | 470 ++++++++++++++++++ asyn_demos.py => v2/asyn_demos.py | 0 asyntest.py => v2/asyntest.py | 0 auart.py => v2/auart.py | 0 auart_hd.py => v2/auart_hd.py | 0 awaitable.py => v2/awaitable.py | 0 {benchmarks => v2/benchmarks}/call_lp.py | 0 {benchmarks => v2/benchmarks}/latency.py | 0 {benchmarks => v2/benchmarks}/overdue.py | 0 .../benchmarks}/priority_test.py | 0 {benchmarks => v2/benchmarks}/rate.py | 0 {benchmarks => v2/benchmarks}/rate_esp.py | 0 {benchmarks => v2/benchmarks}/rate_fastio.py | 0 cantest.py => v2/cantest.py | 0 chain.py => v2/chain.py | 0 check_async_code.py => v2/check_async_code.py | 0 .../client_server}/heartbeat.py | 0 .../client_server}/uclient.py | 0 .../client_server}/userver.py | 0 {fast_io => v2/fast_io}/__init__.py | 0 {fast_io => v2/fast_io}/core.py | 0 {fast_io => v2/fast_io}/fast_can_test.py | 0 {fast_io => v2/fast_io}/iorw_can.py | 0 {fast_io => v2/fast_io}/iorw_to.py | 0 {fast_io => v2/fast_io}/ms_timer.py | 0 {fast_io => v2/fast_io}/ms_timer_test.py | 0 {fast_io => v2/fast_io}/pin_cb.py | 0 {fast_io => v2/fast_io}/pin_cb_test.py | 0 {gps => v2/gps}/LICENSE | 0 {gps => v2/gps}/README.md | 0 {gps => v2/gps}/as_GPS.py | 0 {gps => v2/gps}/as_GPS_time.py | 0 {gps => v2/gps}/as_GPS_utils.py | 0 {gps => v2/gps}/as_rwGPS.py | 0 {gps => v2/gps}/as_rwGPS_time.py | 0 {gps => v2/gps}/as_tGPS.py | 0 {gps => v2/gps}/ast_pb.py | 0 {gps => v2/gps}/ast_pbrw.py | 0 {gps => v2/gps}/astests.py | 0 {gps => v2/gps}/astests_pyb.py | 0 {gps => v2/gps}/log.kml | 0 {gps => v2/gps}/log_kml.py | 0 {htu21d => v2/htu21d}/README.md | 0 {htu21d => v2/htu21d}/htu21d_mc.py | 0 {htu21d => v2/htu21d}/htu_test.py | 0 {i2c => v2/i2c}/README.md | 0 {i2c => v2/i2c}/asi2c.py | 0 {i2c => v2/i2c}/asi2c_i.py | 0 {i2c => v2/i2c}/i2c_esp.py | 0 {i2c => v2/i2c}/i2c_init.py | 0 {i2c => v2/i2c}/i2c_resp.py | 0 io.py => v2/io.py | 0 iorw.py => v2/iorw.py | 0 {lowpower => v2/lowpower}/README.md | 0 {lowpower => v2/lowpower}/current.png | Bin {lowpower => v2/lowpower}/current1.png | Bin {lowpower => v2/lowpower}/lp_uart.py | 0 {lowpower => v2/lowpower}/lpdemo.py | 0 {lowpower => v2/lowpower}/mqtt_log.py | 0 {lowpower => v2/lowpower}/rtc_time.py | 0 {lowpower => v2/lowpower}/rtc_time_cfg.py | 0 {nec_ir => v2/nec_ir}/README.md | 0 {nec_ir => v2/nec_ir}/aremote.py | 0 {nec_ir => v2/nec_ir}/art.py | 0 {nec_ir => v2/nec_ir}/art1.py | 0 roundrobin.py => v2/roundrobin.py | 0 sock_nonblock.py => v2/sock_nonblock.py | 0 {syncom_as => v2/syncom_as}/README.md | 0 {syncom_as => v2/syncom_as}/main.py | 0 {syncom_as => v2/syncom_as}/sr_init.py | 0 {syncom_as => v2/syncom_as}/sr_passive.py | 0 {syncom_as => v2/syncom_as}/syncom.py | 0 v3/docs/TUTORIAL.md | 6 +- 87 files changed, 870 insertions(+), 3 deletions(-) rename DRIVERS.md => v2/DRIVERS.md (100%) rename FASTPOLL.md => v2/FASTPOLL.md (100%) rename {HD44780 => v2/HD44780}/README.md (100%) rename {HD44780 => v2/HD44780}/alcd.py (100%) rename {HD44780 => v2/HD44780}/alcdtest.py (100%) rename PRIMITIVES.md => v2/PRIMITIVES.md (100%) create mode 100644 v2/README.md rename TUTORIAL.md => v2/TUTORIAL.md (100%) rename UNDER_THE_HOOD.md => v2/UNDER_THE_HOOD.md (100%) rename aledflash.py => v2/aledflash.py (100%) rename apoll.py => v2/apoll.py (100%) rename aqtest.py => v2/aqtest.py (100%) rename astests.py => v2/astests.py (100%) create mode 100644 v2/aswitch.py create mode 100644 v2/asyn.py rename asyn_demos.py => v2/asyn_demos.py (100%) rename asyntest.py => v2/asyntest.py (100%) rename auart.py => v2/auart.py (100%) rename auart_hd.py => v2/auart_hd.py (100%) rename awaitable.py => v2/awaitable.py (100%) rename {benchmarks => v2/benchmarks}/call_lp.py (100%) rename {benchmarks => v2/benchmarks}/latency.py (100%) rename {benchmarks => v2/benchmarks}/overdue.py (100%) rename {benchmarks => v2/benchmarks}/priority_test.py (100%) rename {benchmarks => v2/benchmarks}/rate.py (100%) rename {benchmarks => v2/benchmarks}/rate_esp.py (100%) rename {benchmarks => v2/benchmarks}/rate_fastio.py (100%) rename cantest.py => v2/cantest.py (100%) rename chain.py => v2/chain.py (100%) rename check_async_code.py => v2/check_async_code.py (100%) rename {client_server => v2/client_server}/heartbeat.py (100%) rename {client_server => v2/client_server}/uclient.py (100%) rename {client_server => v2/client_server}/userver.py (100%) rename {fast_io => v2/fast_io}/__init__.py (100%) rename {fast_io => v2/fast_io}/core.py (100%) rename {fast_io => v2/fast_io}/fast_can_test.py (100%) rename {fast_io => v2/fast_io}/iorw_can.py (100%) rename {fast_io => v2/fast_io}/iorw_to.py (100%) rename {fast_io => v2/fast_io}/ms_timer.py (100%) rename {fast_io => v2/fast_io}/ms_timer_test.py (100%) rename {fast_io => v2/fast_io}/pin_cb.py (100%) rename {fast_io => v2/fast_io}/pin_cb_test.py (100%) rename {gps => v2/gps}/LICENSE (100%) rename {gps => v2/gps}/README.md (100%) rename {gps => v2/gps}/as_GPS.py (100%) rename {gps => v2/gps}/as_GPS_time.py (100%) rename {gps => v2/gps}/as_GPS_utils.py (100%) rename {gps => v2/gps}/as_rwGPS.py (100%) rename {gps => v2/gps}/as_rwGPS_time.py (100%) rename {gps => v2/gps}/as_tGPS.py (100%) rename {gps => v2/gps}/ast_pb.py (100%) rename {gps => v2/gps}/ast_pbrw.py (100%) rename {gps => v2/gps}/astests.py (100%) rename {gps => v2/gps}/astests_pyb.py (100%) rename {gps => v2/gps}/log.kml (100%) rename {gps => v2/gps}/log_kml.py (100%) rename {htu21d => v2/htu21d}/README.md (100%) rename {htu21d => v2/htu21d}/htu21d_mc.py (100%) rename {htu21d => v2/htu21d}/htu_test.py (100%) rename {i2c => v2/i2c}/README.md (100%) rename {i2c => v2/i2c}/asi2c.py (100%) rename {i2c => v2/i2c}/asi2c_i.py (100%) rename {i2c => v2/i2c}/i2c_esp.py (100%) rename {i2c => v2/i2c}/i2c_init.py (100%) rename {i2c => v2/i2c}/i2c_resp.py (100%) rename io.py => v2/io.py (100%) rename iorw.py => v2/iorw.py (100%) rename {lowpower => v2/lowpower}/README.md (100%) rename {lowpower => v2/lowpower}/current.png (100%) rename {lowpower => v2/lowpower}/current1.png (100%) rename {lowpower => v2/lowpower}/lp_uart.py (100%) rename {lowpower => v2/lowpower}/lpdemo.py (100%) rename {lowpower => v2/lowpower}/mqtt_log.py (100%) rename {lowpower => v2/lowpower}/rtc_time.py (100%) rename {lowpower => v2/lowpower}/rtc_time_cfg.py (100%) rename {nec_ir => v2/nec_ir}/README.md (100%) rename {nec_ir => v2/nec_ir}/aremote.py (100%) rename {nec_ir => v2/nec_ir}/art.py (100%) rename {nec_ir => v2/nec_ir}/art1.py (100%) rename roundrobin.py => v2/roundrobin.py (100%) rename sock_nonblock.py => v2/sock_nonblock.py (100%) rename {syncom_as => v2/syncom_as}/README.md (100%) rename {syncom_as => v2/syncom_as}/main.py (100%) rename {syncom_as => v2/syncom_as}/sr_init.py (100%) rename {syncom_as => v2/syncom_as}/sr_passive.py (100%) rename {syncom_as => v2/syncom_as}/syncom.py (100%) diff --git a/DRIVERS.md b/v2/DRIVERS.md similarity index 100% rename from DRIVERS.md rename to v2/DRIVERS.md diff --git a/FASTPOLL.md b/v2/FASTPOLL.md similarity index 100% rename from FASTPOLL.md rename to v2/FASTPOLL.md diff --git a/HD44780/README.md b/v2/HD44780/README.md similarity index 100% rename from HD44780/README.md rename to v2/HD44780/README.md diff --git a/HD44780/alcd.py b/v2/HD44780/alcd.py similarity index 100% rename from HD44780/alcd.py rename to v2/HD44780/alcd.py diff --git a/HD44780/alcdtest.py b/v2/HD44780/alcdtest.py similarity index 100% rename from HD44780/alcdtest.py rename to v2/HD44780/alcdtest.py diff --git a/PRIMITIVES.md b/v2/PRIMITIVES.md similarity index 100% rename from PRIMITIVES.md rename to v2/PRIMITIVES.md diff --git a/v2/README.md b/v2/README.md new file mode 100644 index 0000000..163855a --- /dev/null +++ b/v2/README.md @@ -0,0 +1,166 @@ +# 1. uasyncio V2 + +This repo also contains an optional `fast_io` variant of `uasyncio` V2. This +variant offers high I/O performance and also includes workrounds for many of +the bugs in V2. (Bugs properly fixed in V3.) + +## Reasons for running V2 + +In general I recommend V3, especially for new projects. It is better in every +respect bar one: the `fast_io` variant of V2 currently offers superior I/O +performance, relative both to V2 and V3. + +The main reason for running official V2 is that many existing libraries have +not yet been ported to V3. Some will run without change, but those using more +advanced features of `uasyncio` may not. + +## 1.1 Resources + + * [A tutorial](./TUTORIAL.md) An introductory tutorial on asynchronous + programming and the use of the `uasyncio` library. + * [Asynchronous device drivers](./DRIVERS.md). A module providing drivers for + devices such as switches and pushbuttons. + * [Synchronisation primitives](./PRIMITIVES.md). Provides commonly used + synchronisation primitives plus an API for task cancellation and monitoring. + * [A driver for an IR remote control](./nec_ir/README.md) This is intended as + an example of an asynchronous device driver. It decodes signals received from + infra red remote controls using the popular NEC protocol. + * [A driver for the HTU21D](./htu21d/README.md) temperature and humidity + sensor. This is intended to be portable across platforms and is another + example of an asynchronous device driver. + * [A driver for character LCD displays](./HD44780/README.md). A simple + asynchronous interface to displays based on the Hitachi HD44780 chip. + * [A driver for GPS modules](./gps/README.md) Runs a background task to read + and decode NMEA sentences, providing constantly updated position, course, + altitude and time/date information. + * [Communication using I2C slave mode.](./i2c/README.md) Enables a Pyboard to + to communicate with another MicroPython device using stream I/O. The Pyboard + achieves bidirectional communication with targets such as an ESP8266. + * [Communication between devices](./syncom_as/README.md) Enables MicroPython + boards to communicate without using a UART. This is hardware agnostic but + slower than the I2C version. + +## 1.2 The fast_io variant + +This comprises two parts. + 1. The [fast_io](./FASTPOLL.md) version of `uasyncio` is a "drop in" + replacement for the official version 2 providing bug fixes, additional + functionality and, in certain respects, higher performance. + 2. An optional extension module enabling the [fast_io](./FASTPOLL.md) version + to run with very low power draw. This is Pyboard-only including Pyboard D. + +Official `uasyncio` suffers from high levels of latency when scheduling I/O in +typical applications. It also has an issue which can cause bidirectional +devices such as UART's to block. The `fast_io` version fixes the bug. It also +provides a facility for reducing I/O latency which can substantially improve +the performance of stream I/O drivers. It provides other features aimed at +providing greater control over scheduling behaviour. + +To take advantage of the reduced latency device drivers should be written to +employ stream I/O. To operate at low latency they are simply run under the +`fast_io` version. The [tutorial](./TUTORIAL.md#64-writing-streaming-device-drivers) +has details of how to write streaming drivers. + +The current `fast_io` version 0.24 fixes an issue with task cancellation and +timeouts. In `uasyncio` version 2.0, where a coroutine is waiting on a +`sleep()` or on I/O, a timeout or cancellation is deferred until the coroutine +is next scheduled. This introduces uncertainty into when the coroutine is +stopped. + +## 1.2.1 A Pyboard-only low power module + +This is documented [here](./lowpower/README.md). In essence a Python file is +placed on the device which configures the `fast_io` version of `uasyncio` to +reduce power consumption at times when it is not busy. This provides a means of +using `uasyncio` in battery powered projects. This is decidedly experimental: +hopefully `uasyncio` V3 will introduce power saving in a less hacky manner. + +## 1.3 Under the hood + +[Under the hood](./UNDER_THE_HOOD.md) A guide to help understand the V2 +`uasyncio` code. For scheduler geeks and those wishing to modify `uasyncio`. + +## 1.4 Synchronisation Primitives + +All solutions listed below work with stock `uasyncio` V2 or `fast_io`. + +The CPython `asyncio` library supports these synchronisation primitives: + * `Lock` + * `Event` + * `gather` + * `Semaphore` and `BoundedSemaphore`. + * `Condition`. + * `Queue`. This was implemented by Paul Sokolvsky in `uasyncio.queues`. + +See [CPython docs](https://docs.python.org/3/library/asyncio-sync.html). + +The file `asyn.py` contains implementations of these, also + * `Barrier` An additional synchronisation primitive. + * Cancellation decorators and classes: these are workrounds for the bug where + in V2 cancellation does not occur promptly. + * Support for `gather`. + +The `Event` class in `asyn.py` provides a nonstandard option to supply a data +value to the `.set` method and to retrieve this with `.value`. It is also an +awaitable class. + +#### These are documented [here](./PRIMITIVES.md) + +## 1.5 Switches, Pushbuttons and Timeouts + +The file `aswitch.py` provides support for: + * `Delay_ms` A software retriggerable monostable or watchdog. + * `Switch` Debounced switch and pushbutton classes with callbacks. + * `Pushbutton` + +#### It is documented [here](./DRIVERS.md) + +# 2. Version 2.0 usage notes + +These notes are intended for users familiar with `asyncio` under CPython. + +The MicroPython language is based on CPython 3.4. The `uasyncio` library +supports a subset of the CPython 3.4 `asyncio` library with some V3.5 +extensions. In addition there are non-standard extensions to optimise services +such as millisecond level timing and task cancellation. Its design focus is on +high performance and scheduling is performed without RAM allocation. + +The `uasyncio` library supports the following Python 3.5 features: + + * `async def` and `await` syntax. + * Awaitable classes (using `__iter__` rather than `__await__`). + * Asynchronous context managers. + * Asynchronous iterators. + * Event loop methods `call_soon` and `call_later`. + * `sleep(seconds)`. + +It supports millisecond level timing with the following: + + * Event loop method `call_later_ms` + * uasyncio `sleep_ms(time)` + +`uasyncio` V2 supports coroutine timeouts and cancellation. + + * `wait_for(coro, t_secs)` runs `coro` with a timeout. + * `cancel(coro)` tags `coro` for cancellation when it is next scheduled. + +Classes `Task` and `Future` are not supported. + +## 2.1 Asynchronous I/O + +Asynchronous I/O (`StreamReader` and `StreamWriter` classes) support devices +with streaming drivers, such as UARTs and sockets. It is now possible to write +streaming device drivers in Python. + +## 2.2 Time values + +For timing asyncio uses floating point values of seconds. The `uasyncio.sleep` +method accepts floats (including sub-second values) or integers. Note that in +MicroPython the use of floats implies RAM allocation which incurs a performance +penalty. The design of `uasyncio` enables allocation-free scheduling. In +applications where performance is an issue, integers should be used and the +millisecond level functions (with integer arguments) employed where necessary. + +The `loop.time` method returns an integer number of milliseconds whereas +CPython returns a floating point number of seconds. `call_at` follows the +same convention. diff --git a/TUTORIAL.md b/v2/TUTORIAL.md similarity index 100% rename from TUTORIAL.md rename to v2/TUTORIAL.md diff --git a/UNDER_THE_HOOD.md b/v2/UNDER_THE_HOOD.md similarity index 100% rename from UNDER_THE_HOOD.md rename to v2/UNDER_THE_HOOD.md diff --git a/aledflash.py b/v2/aledflash.py similarity index 100% rename from aledflash.py rename to v2/aledflash.py diff --git a/apoll.py b/v2/apoll.py similarity index 100% rename from apoll.py rename to v2/apoll.py diff --git a/aqtest.py b/v2/aqtest.py similarity index 100% rename from aqtest.py rename to v2/aqtest.py diff --git a/astests.py b/v2/astests.py similarity index 100% rename from astests.py rename to v2/astests.py diff --git a/v2/aswitch.py b/v2/aswitch.py new file mode 100644 index 0000000..4269ce9 --- /dev/null +++ b/v2/aswitch.py @@ -0,0 +1,231 @@ +# aswitch.py Switch and pushbutton classes for asyncio +# Delay_ms A retriggerable delay class. Can schedule a coro on timeout. +# Switch Simple debounced switch class for normally open grounded switch. +# Pushbutton extend the above to support logical state, long press and +# double-click events +# Tested on Pyboard but should run on other microcontroller platforms +# running MicroPython and uasyncio. + +# The MIT License (MIT) +# +# Copyright (c) 2017 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import uasyncio as asyncio +import utime as time +# Remove dependency on asyn to save RAM: +# launch: run a callback or initiate a coroutine depending on which is passed. +async def _g(): + pass +type_coro = type(_g()) + +# If a callback is passed, run it and return. +# If a coro is passed initiate it and return. +# coros are passed by name i.e. not using function call syntax. +def launch(func, tup_args): + res = func(*tup_args) + if isinstance(res, type_coro): + loop = asyncio.get_event_loop() + loop.create_task(res) + + +class Delay_ms: + verbose = False + def __init__(self, func=None, args=(), can_alloc=True, duration=1000): + self.func = func + self.args = args + self.can_alloc = can_alloc + self.duration = duration # Default duration + self._tstop = None # Killer not running + self._running = False # Timer not running + self.loop = asyncio.get_event_loop() + if not can_alloc: + self.loop.create_task(self._run()) + + async def _run(self): + while True: + if not self._running: # timer not running + await asyncio.sleep_ms(0) + else: + await self._killer() + + def stop(self): + self._running = False + # If uasyncio is ever fixed we should cancel .killer + + def trigger(self, duration=0): # Update end time + self._running = True + if duration <= 0: + duration = self.duration + tn = time.ticks_add(time.ticks_ms(), duration) # new end time + self.verbose and self._tstop is not None and self._tstop > tn \ + and print("Warning: can't reduce Delay_ms time.") + # Start killer if can allocate and killer is not running + sk = self.can_alloc and self._tstop is None + # The following indicates ._killer is running: it will be + # started either here or in ._run + self._tstop = tn + if sk: # ._killer stops the delay when its period has elapsed + self.loop.create_task(self._killer()) + + def running(self): + return self._running + + __call__ = running + + async def _killer(self): + twait = time.ticks_diff(self._tstop, time.ticks_ms()) + while twait > 0: # Must loop here: might be retriggered + await asyncio.sleep_ms(twait) + if self._tstop is None: + break # Return if stop() called during wait + twait = time.ticks_diff(self._tstop, time.ticks_ms()) + if self._running and self.func is not None: + launch(self.func, self.args) # Timed out: execute callback + self._tstop = None # killer not running + self._running = False # timer is stopped + +class Switch: + debounce_ms = 50 + def __init__(self, pin): + self.pin = pin # Should be initialised for input with pullup + self._open_func = False + self._close_func = False + self.switchstate = self.pin.value() # Get initial state + loop = asyncio.get_event_loop() + loop.create_task(self.switchcheck()) # Thread runs forever + + def open_func(self, func, args=()): + self._open_func = func + self._open_args = args + + def close_func(self, func, args=()): + self._close_func = func + self._close_args = args + + # Return current state of switch (0 = pressed) + def __call__(self): + return self.switchstate + + async def switchcheck(self): + while True: + state = self.pin.value() + if state != self.switchstate: + # State has changed: act on it now. + self.switchstate = state + if state == 0 and self._close_func: + launch(self._close_func, self._close_args) + elif state == 1 and self._open_func: + launch(self._open_func, self._open_args) + # Ignore further state changes until switch has settled + await asyncio.sleep_ms(Switch.debounce_ms) + +# An alternative Pushbutton solution with lower RAM use is available here +# https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py +class Pushbutton: + debounce_ms = 50 + long_press_ms = 1000 + double_click_ms = 400 + def __init__(self, pin, suppress=False): + self.pin = pin # Initialise for input + self._supp = suppress + self._dblpend = False # Doubleclick waiting for 2nd click + self._dblran = False # Doubleclick executed user function + self._tf = False + self._ff = False + self._df = False + self._lf = False + self._ld = False # Delay_ms instance for long press + self._dd = False # Ditto for doubleclick + self.sense = pin.value() # Convert from electrical to logical value + self.state = self.rawstate() # Initial state + loop = asyncio.get_event_loop() + loop.create_task(self.buttoncheck()) # Thread runs forever + + def press_func(self, func, args=()): + self._tf = func + self._ta = args + + def release_func(self, func, args=()): + self._ff = func + self._fa = args + + def double_func(self, func, args=()): + self._df = func + self._da = args + + def long_func(self, func, args=()): + self._lf = func + self._la = args + + # Current non-debounced logical button state: True == pressed + def rawstate(self): + return bool(self.pin.value() ^ self.sense) + + # Current debounced state of button (True == pressed) + def __call__(self): + return self.state + + def _ddto(self): # Doubleclick timeout: no doubleclick occurred + self._dblpend = False + if self._supp and not self.state: + if not self._ld or (self._ld and not self._ld()): + launch(self._ff, self._fa) + + async def buttoncheck(self): + if self._lf: # Instantiate timers if funcs exist + self._ld = Delay_ms(self._lf, self._la) + if self._df: + self._dd = Delay_ms(self._ddto) + while True: + state = self.rawstate() + # State has changed: act on it now. + if state != self.state: + self.state = state + if state: # Button pressed: launch pressed func + if self._tf: + launch(self._tf, self._ta) + if self._lf: # There's a long func: start long press delay + self._ld.trigger(Pushbutton.long_press_ms) + if self._df: + if self._dd(): # Second click: timer running + self._dd.stop() + self._dblpend = False + self._dblran = True # Prevent suppressed launch on release + launch(self._df, self._da) + else: + # First click: start doubleclick timer + self._dd.trigger(Pushbutton.double_click_ms) + self._dblpend = True # Prevent suppressed launch on release + else: # Button release. Is there a release func? + if self._ff: + if self._supp: + d = self._ld + # If long delay exists, is running and doubleclick status is OK + if not self._dblpend and not self._dblran: + if (d and d()) or not d: + launch(self._ff, self._fa) + else: + launch(self._ff, self._fa) + if self._ld: + self._ld.stop() # Avoid interpreting a second click as a long push + self._dblran = False + # Ignore state changes until switch has settled + await asyncio.sleep_ms(Pushbutton.debounce_ms) diff --git a/v2/asyn.py b/v2/asyn.py new file mode 100644 index 0000000..c87c175 --- /dev/null +++ b/v2/asyn.py @@ -0,0 +1,470 @@ +# asyn.py 'micro' synchronisation primitives for uasyncio +# Test/demo programs asyntest.py, barrier_test.py +# Provides Lock, Event, Barrier, Semaphore, BoundedSemaphore, Condition, +# NamedTask and Cancellable classes, also sleep coro. +# Updated 31 Dec 2017 for uasyncio.core V1.6 and to provide task cancellation. + +# The MIT License (MIT) +# +# Copyright (c) 2017 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# CPython 3.5 compatibility +# (ignore RuntimeWarning: coroutine '_g' was never awaited) + +try: + import uasyncio as asyncio +except ImportError: + import asyncio + + +async def _g(): + pass +type_coro = type(_g()) + +# If a callback is passed, run it and return. +# If a coro is passed initiate it and return. +# coros are passed by name i.e. not using function call syntax. +def launch(func, tup_args): + res = func(*tup_args) + if isinstance(res, type_coro): + loop = asyncio.get_event_loop() + loop.create_task(res) + + +# To access a lockable resource a coro should issue +# async with lock_instance: +# access the locked resource + +# Alternatively: +# await lock.acquire() +# try: +# do stuff with locked resource +# finally: +# lock.release +# Uses normal scheduling on assumption that locks are held briefly. +class Lock(): + def __init__(self, delay_ms=0): + self._locked = False + self.delay_ms = delay_ms + + def locked(self): + return self._locked + + async def __aenter__(self): + await self.acquire() + return self + + async def __aexit__(self, *args): + self.release() + await asyncio.sleep(0) + + async def acquire(self): + while True: + if self._locked: + await asyncio.sleep_ms(self.delay_ms) + else: + self._locked = True + break + + def release(self): + if not self._locked: + raise RuntimeError('Attempt to release a lock which has not been set') + self._locked = False + + +# A coro waiting on an event issues await event +# A coro rasing the event issues event.set() +# When all waiting coros have run +# event.clear() should be issued +class Event(): + def __init__(self, delay_ms=0): + self.delay_ms = delay_ms + self.clear() + + def clear(self): + self._flag = False + self._data = None + + async def wait(self): # CPython comptaibility + while not self._flag: + await asyncio.sleep_ms(self.delay_ms) + + def __await__(self): + while not self._flag: + await asyncio.sleep_ms(self.delay_ms) + + __iter__ = __await__ + + def is_set(self): + return self._flag + + def set(self, data=None): + self._flag = True + self._data = data + + def value(self): + return self._data + +# A Barrier synchronises N coros. Each issues await barrier. +# Execution pauses until all other participant coros are waiting on it. +# At that point the callback is executed. Then the barrier is 'opened' and +# execution of all participants resumes. + +# The nowait arg is to support task cancellation. It enables usage where one or +# more coros can register that they have reached the barrier without waiting +# for it. Any coros waiting normally on the barrier will pause until all +# non-waiting coros have passed the barrier and all waiting ones have reached +# it. The use of nowait promotes efficiency by enabling tasks which have been +# cancelled to leave the task queue as soon as possible. + +class Barrier(): + def __init__(self, participants, func=None, args=()): + self._participants = participants + self._func = func + self._args = args + self._reset(True) + + def __await__(self): + self._update() + if self._at_limit(): # All other threads are also at limit + if self._func is not None: + launch(self._func, self._args) + self._reset(not self._down) # Toggle direction to release others + return + + direction = self._down + while True: # Wait until last waiting thread changes the direction + if direction != self._down: + return + await asyncio.sleep_ms(0) + + __iter__ = __await__ + + def trigger(self): + self._update() + if self._at_limit(): # All other threads are also at limit + if self._func is not None: + launch(self._func, self._args) + self._reset(not self._down) # Toggle direction to release others + + def _reset(self, down): + self._down = down + self._count = self._participants if down else 0 + + def busy(self): + if self._down: + done = self._count == self._participants + else: + done = self._count == 0 + return not done + + def _at_limit(self): # Has count reached up or down limit? + limit = 0 if self._down else self._participants + return self._count == limit + + def _update(self): + self._count += -1 if self._down else 1 + if self._count < 0 or self._count > self._participants: + raise ValueError('Too many tasks accessing Barrier') + +# A Semaphore is typically used to limit the number of coros running a +# particular piece of code at once. The number is defined in the constructor. +class Semaphore(): + def __init__(self, value=1): + self._count = value + + async def __aenter__(self): + await self.acquire() + return self + + async def __aexit__(self, *args): + self.release() + await asyncio.sleep(0) + + async def acquire(self): + while self._count == 0: + await asyncio.sleep_ms(0) + self._count -= 1 + + def release(self): + self._count += 1 + +class BoundedSemaphore(Semaphore): + def __init__(self, value=1): + super().__init__(value) + self._initial_value = value + + def release(self): + if self._count < self._initial_value: + self._count += 1 + else: + raise ValueError('Semaphore released more than acquired') + +# Task Cancellation +try: + StopTask = asyncio.CancelledError # More descriptive name +except AttributeError: + raise OSError('asyn.py requires uasyncio V1.7.1 or above.') + +class TaskId(): + def __init__(self, taskid): + self.taskid = taskid + + def __call__(self): + return self.taskid + +# Sleep coro breaks up a sleep into shorter intervals to ensure a rapid +# response to StopTask exceptions. Only relevant to official uasyncio V2.0. +async def sleep(t, granularity=100): # 100ms default + if granularity <= 0: + raise ValueError('sleep granularity must be > 0') + t = int(t * 1000) # ms + if t <= granularity: + await asyncio.sleep_ms(t) + else: + n, rem = divmod(t, granularity) + for _ in range(n): + await asyncio.sleep_ms(granularity) + await asyncio.sleep_ms(rem) + +# Anonymous cancellable tasks. These are members of a group which is identified +# by a user supplied name/number (default 0). Class method cancel_all() cancels +# all tasks in a group and awaits confirmation. Confirmation of ending (whether +# normally or by cancellation) is signalled by a task calling the _stopped() +# class method. Handled by the @cancellable decorator. + + +class Cancellable(): + task_no = 0 # Generated task ID, index of tasks dict + tasks = {} # Value is [coro, group, barrier] indexed by integer task_no + + @classmethod + def _cancel(cls, task_no): + task = cls.tasks[task_no][0] + asyncio.cancel(task) + + @classmethod + async def cancel_all(cls, group=0, nowait=False): + tokill = cls._get_task_nos(group) + barrier = Barrier(len(tokill) + 1) # Include this task + for task_no in tokill: + cls.tasks[task_no][2] = barrier + cls._cancel(task_no) + if nowait: + barrier.trigger() + else: + await barrier + + @classmethod + def _is_running(cls, group=0): + tasks = cls._get_task_nos(group) + if tasks == []: + return False + for task_no in tasks: + barrier = cls.tasks[task_no][2] + if barrier is None: # Running, not yet cancelled + return True + if barrier.busy(): + return True + return False + + @classmethod + def _get_task_nos(cls, group): # Return task nos in a group + return [task_no for task_no in cls.tasks if cls.tasks[task_no][1] == group] + + @classmethod + def _get_group(cls, task_no): # Return group given a task_no + return cls.tasks[task_no][1] + + @classmethod + def _stopped(cls, task_no): + if task_no in cls.tasks: + barrier = cls.tasks[task_no][2] + if barrier is not None: # Cancellation in progress + barrier.trigger() + del cls.tasks[task_no] + + def __init__(self, gf, *args, group=0, **kwargs): + task = gf(TaskId(Cancellable.task_no), *args, **kwargs) + if task in self.tasks: + raise ValueError('Task already exists.') + self.tasks[Cancellable.task_no] = [task, group, None] + self.task_no = Cancellable.task_no # For subclass + Cancellable.task_no += 1 + self.task = task + + def __call__(self): + return self.task + + def __await__(self): # Return any value returned by task. + return (yield from self.task) + + __iter__ = __await__ + + +# @cancellable decorator + +def cancellable(f): + def new_gen(*args, **kwargs): + if isinstance(args[0], TaskId): # Not a bound method + task_id = args[0] + g = f(*args[1:], **kwargs) + else: # Task ID is args[1] if a bound method + task_id = args[1] + args = (args[0],) + args[2:] + g = f(*args, **kwargs) + try: + res = await g + return res + finally: + NamedTask._stopped(task_id) + return new_gen + +# The NamedTask class enables a coro to be identified by a user defined name. +# It constrains Cancellable to allow groups of one coro only. +# It maintains a dict of barriers indexed by name. +class NamedTask(Cancellable): + instances = {} + + @classmethod + async def cancel(cls, name, nowait=True): + if name in cls.instances: + await cls.cancel_all(group=name, nowait=nowait) + return True + return False + + @classmethod + def is_running(cls, name): + return cls._is_running(group=name) + + @classmethod + def _stopped(cls, task_id): # On completion remove it + name = cls._get_group(task_id()) # Convert task_id to task_no + if name in cls.instances: + instance = cls.instances[name] + barrier = instance.barrier + if barrier is not None: + barrier.trigger() + del cls.instances[name] + Cancellable._stopped(task_id()) + + def __init__(self, name, gf, *args, barrier=None, **kwargs): + if name in self.instances: + raise ValueError('Task name "{}" already exists.'.format(name)) + super().__init__(gf, *args, group=name, **kwargs) + self.barrier = barrier + self.instances[name] = self + + +# @namedtask +namedtask = cancellable # compatibility with old code + +# Condition class + +class Condition(): + def __init__(self, lock=None): + self.lock = Lock() if lock is None else lock + self.events = [] + + async def acquire(self): + await self.lock.acquire() + +# enable this syntax: +# with await condition [as cond]: + def __await__(self): + yield from self.lock.acquire() + return self + + __iter__ = __await__ + + def __enter__(self): + return self + + def __exit__(self, *_): + self.lock.release() + + def locked(self): + return self.lock.locked() + + def release(self): + self.lock.release() # Will raise RuntimeError if not locked + + def notify(self, n=1): # Caller controls lock + if not self.lock.locked(): + raise RuntimeError('Condition notify with lock not acquired.') + for _ in range(min(n, len(self.events))): + ev = self.events.pop() + ev.set() + + def notify_all(self): + self.notify(len(self.events)) + + async def wait(self): + if not self.lock.locked(): + raise RuntimeError('Condition wait with lock not acquired.') + ev = Event() + self.events.append(ev) + self.lock.release() + await ev + await self.lock.acquire() + assert ev not in self.events, 'condition wait assertion fail' + return True # CPython compatibility + + async def wait_for(self, predicate): + result = predicate() + while not result: + await self.wait() + result = predicate() + return result + +# Provide functionality similar to asyncio.gather() + +class Gather(): + def __init__(self, gatherables): + ncoros = len(gatherables) + self.barrier = Barrier(ncoros + 1) + self.results = [None] * ncoros + loop = asyncio.get_event_loop() + for n, gatherable in enumerate(gatherables): + loop.create_task(self.wrap(gatherable, n)()) + + def __iter__(self): + yield from self.barrier.__await__() + return self.results + + def wrap(self, gatherable, idx): + async def wrapped(): + coro, args, kwargs = gatherable() + try: + tim = kwargs.pop('timeout') + except KeyError: + self.results[idx] = await coro(*args, **kwargs) + else: + self.results[idx] = await asyncio.wait_for(coro(*args, **kwargs), tim) + self.barrier.trigger() + return wrapped + +class Gatherable(): + def __init__(self, coro, *args, **kwargs): + self.arguments = coro, args, kwargs + + def __call__(self): + return self.arguments diff --git a/asyn_demos.py b/v2/asyn_demos.py similarity index 100% rename from asyn_demos.py rename to v2/asyn_demos.py diff --git a/asyntest.py b/v2/asyntest.py similarity index 100% rename from asyntest.py rename to v2/asyntest.py diff --git a/auart.py b/v2/auart.py similarity index 100% rename from auart.py rename to v2/auart.py diff --git a/auart_hd.py b/v2/auart_hd.py similarity index 100% rename from auart_hd.py rename to v2/auart_hd.py diff --git a/awaitable.py b/v2/awaitable.py similarity index 100% rename from awaitable.py rename to v2/awaitable.py diff --git a/benchmarks/call_lp.py b/v2/benchmarks/call_lp.py similarity index 100% rename from benchmarks/call_lp.py rename to v2/benchmarks/call_lp.py diff --git a/benchmarks/latency.py b/v2/benchmarks/latency.py similarity index 100% rename from benchmarks/latency.py rename to v2/benchmarks/latency.py diff --git a/benchmarks/overdue.py b/v2/benchmarks/overdue.py similarity index 100% rename from benchmarks/overdue.py rename to v2/benchmarks/overdue.py diff --git a/benchmarks/priority_test.py b/v2/benchmarks/priority_test.py similarity index 100% rename from benchmarks/priority_test.py rename to v2/benchmarks/priority_test.py diff --git a/benchmarks/rate.py b/v2/benchmarks/rate.py similarity index 100% rename from benchmarks/rate.py rename to v2/benchmarks/rate.py diff --git a/benchmarks/rate_esp.py b/v2/benchmarks/rate_esp.py similarity index 100% rename from benchmarks/rate_esp.py rename to v2/benchmarks/rate_esp.py diff --git a/benchmarks/rate_fastio.py b/v2/benchmarks/rate_fastio.py similarity index 100% rename from benchmarks/rate_fastio.py rename to v2/benchmarks/rate_fastio.py diff --git a/cantest.py b/v2/cantest.py similarity index 100% rename from cantest.py rename to v2/cantest.py diff --git a/chain.py b/v2/chain.py similarity index 100% rename from chain.py rename to v2/chain.py diff --git a/check_async_code.py b/v2/check_async_code.py similarity index 100% rename from check_async_code.py rename to v2/check_async_code.py diff --git a/client_server/heartbeat.py b/v2/client_server/heartbeat.py similarity index 100% rename from client_server/heartbeat.py rename to v2/client_server/heartbeat.py diff --git a/client_server/uclient.py b/v2/client_server/uclient.py similarity index 100% rename from client_server/uclient.py rename to v2/client_server/uclient.py diff --git a/client_server/userver.py b/v2/client_server/userver.py similarity index 100% rename from client_server/userver.py rename to v2/client_server/userver.py diff --git a/fast_io/__init__.py b/v2/fast_io/__init__.py similarity index 100% rename from fast_io/__init__.py rename to v2/fast_io/__init__.py diff --git a/fast_io/core.py b/v2/fast_io/core.py similarity index 100% rename from fast_io/core.py rename to v2/fast_io/core.py diff --git a/fast_io/fast_can_test.py b/v2/fast_io/fast_can_test.py similarity index 100% rename from fast_io/fast_can_test.py rename to v2/fast_io/fast_can_test.py diff --git a/fast_io/iorw_can.py b/v2/fast_io/iorw_can.py similarity index 100% rename from fast_io/iorw_can.py rename to v2/fast_io/iorw_can.py diff --git a/fast_io/iorw_to.py b/v2/fast_io/iorw_to.py similarity index 100% rename from fast_io/iorw_to.py rename to v2/fast_io/iorw_to.py diff --git a/fast_io/ms_timer.py b/v2/fast_io/ms_timer.py similarity index 100% rename from fast_io/ms_timer.py rename to v2/fast_io/ms_timer.py diff --git a/fast_io/ms_timer_test.py b/v2/fast_io/ms_timer_test.py similarity index 100% rename from fast_io/ms_timer_test.py rename to v2/fast_io/ms_timer_test.py diff --git a/fast_io/pin_cb.py b/v2/fast_io/pin_cb.py similarity index 100% rename from fast_io/pin_cb.py rename to v2/fast_io/pin_cb.py diff --git a/fast_io/pin_cb_test.py b/v2/fast_io/pin_cb_test.py similarity index 100% rename from fast_io/pin_cb_test.py rename to v2/fast_io/pin_cb_test.py diff --git a/gps/LICENSE b/v2/gps/LICENSE similarity index 100% rename from gps/LICENSE rename to v2/gps/LICENSE diff --git a/gps/README.md b/v2/gps/README.md similarity index 100% rename from gps/README.md rename to v2/gps/README.md diff --git a/gps/as_GPS.py b/v2/gps/as_GPS.py similarity index 100% rename from gps/as_GPS.py rename to v2/gps/as_GPS.py diff --git a/gps/as_GPS_time.py b/v2/gps/as_GPS_time.py similarity index 100% rename from gps/as_GPS_time.py rename to v2/gps/as_GPS_time.py diff --git a/gps/as_GPS_utils.py b/v2/gps/as_GPS_utils.py similarity index 100% rename from gps/as_GPS_utils.py rename to v2/gps/as_GPS_utils.py diff --git a/gps/as_rwGPS.py b/v2/gps/as_rwGPS.py similarity index 100% rename from gps/as_rwGPS.py rename to v2/gps/as_rwGPS.py diff --git a/gps/as_rwGPS_time.py b/v2/gps/as_rwGPS_time.py similarity index 100% rename from gps/as_rwGPS_time.py rename to v2/gps/as_rwGPS_time.py diff --git a/gps/as_tGPS.py b/v2/gps/as_tGPS.py similarity index 100% rename from gps/as_tGPS.py rename to v2/gps/as_tGPS.py diff --git a/gps/ast_pb.py b/v2/gps/ast_pb.py similarity index 100% rename from gps/ast_pb.py rename to v2/gps/ast_pb.py diff --git a/gps/ast_pbrw.py b/v2/gps/ast_pbrw.py similarity index 100% rename from gps/ast_pbrw.py rename to v2/gps/ast_pbrw.py diff --git a/gps/astests.py b/v2/gps/astests.py similarity index 100% rename from gps/astests.py rename to v2/gps/astests.py diff --git a/gps/astests_pyb.py b/v2/gps/astests_pyb.py similarity index 100% rename from gps/astests_pyb.py rename to v2/gps/astests_pyb.py diff --git a/gps/log.kml b/v2/gps/log.kml similarity index 100% rename from gps/log.kml rename to v2/gps/log.kml diff --git a/gps/log_kml.py b/v2/gps/log_kml.py similarity index 100% rename from gps/log_kml.py rename to v2/gps/log_kml.py diff --git a/htu21d/README.md b/v2/htu21d/README.md similarity index 100% rename from htu21d/README.md rename to v2/htu21d/README.md diff --git a/htu21d/htu21d_mc.py b/v2/htu21d/htu21d_mc.py similarity index 100% rename from htu21d/htu21d_mc.py rename to v2/htu21d/htu21d_mc.py diff --git a/htu21d/htu_test.py b/v2/htu21d/htu_test.py similarity index 100% rename from htu21d/htu_test.py rename to v2/htu21d/htu_test.py diff --git a/i2c/README.md b/v2/i2c/README.md similarity index 100% rename from i2c/README.md rename to v2/i2c/README.md diff --git a/i2c/asi2c.py b/v2/i2c/asi2c.py similarity index 100% rename from i2c/asi2c.py rename to v2/i2c/asi2c.py diff --git a/i2c/asi2c_i.py b/v2/i2c/asi2c_i.py similarity index 100% rename from i2c/asi2c_i.py rename to v2/i2c/asi2c_i.py diff --git a/i2c/i2c_esp.py b/v2/i2c/i2c_esp.py similarity index 100% rename from i2c/i2c_esp.py rename to v2/i2c/i2c_esp.py diff --git a/i2c/i2c_init.py b/v2/i2c/i2c_init.py similarity index 100% rename from i2c/i2c_init.py rename to v2/i2c/i2c_init.py diff --git a/i2c/i2c_resp.py b/v2/i2c/i2c_resp.py similarity index 100% rename from i2c/i2c_resp.py rename to v2/i2c/i2c_resp.py diff --git a/io.py b/v2/io.py similarity index 100% rename from io.py rename to v2/io.py diff --git a/iorw.py b/v2/iorw.py similarity index 100% rename from iorw.py rename to v2/iorw.py diff --git a/lowpower/README.md b/v2/lowpower/README.md similarity index 100% rename from lowpower/README.md rename to v2/lowpower/README.md diff --git a/lowpower/current.png b/v2/lowpower/current.png similarity index 100% rename from lowpower/current.png rename to v2/lowpower/current.png diff --git a/lowpower/current1.png b/v2/lowpower/current1.png similarity index 100% rename from lowpower/current1.png rename to v2/lowpower/current1.png diff --git a/lowpower/lp_uart.py b/v2/lowpower/lp_uart.py similarity index 100% rename from lowpower/lp_uart.py rename to v2/lowpower/lp_uart.py diff --git a/lowpower/lpdemo.py b/v2/lowpower/lpdemo.py similarity index 100% rename from lowpower/lpdemo.py rename to v2/lowpower/lpdemo.py diff --git a/lowpower/mqtt_log.py b/v2/lowpower/mqtt_log.py similarity index 100% rename from lowpower/mqtt_log.py rename to v2/lowpower/mqtt_log.py diff --git a/lowpower/rtc_time.py b/v2/lowpower/rtc_time.py similarity index 100% rename from lowpower/rtc_time.py rename to v2/lowpower/rtc_time.py diff --git a/lowpower/rtc_time_cfg.py b/v2/lowpower/rtc_time_cfg.py similarity index 100% rename from lowpower/rtc_time_cfg.py rename to v2/lowpower/rtc_time_cfg.py diff --git a/nec_ir/README.md b/v2/nec_ir/README.md similarity index 100% rename from nec_ir/README.md rename to v2/nec_ir/README.md diff --git a/nec_ir/aremote.py b/v2/nec_ir/aremote.py similarity index 100% rename from nec_ir/aremote.py rename to v2/nec_ir/aremote.py diff --git a/nec_ir/art.py b/v2/nec_ir/art.py similarity index 100% rename from nec_ir/art.py rename to v2/nec_ir/art.py diff --git a/nec_ir/art1.py b/v2/nec_ir/art1.py similarity index 100% rename from nec_ir/art1.py rename to v2/nec_ir/art1.py diff --git a/roundrobin.py b/v2/roundrobin.py similarity index 100% rename from roundrobin.py rename to v2/roundrobin.py diff --git a/sock_nonblock.py b/v2/sock_nonblock.py similarity index 100% rename from sock_nonblock.py rename to v2/sock_nonblock.py diff --git a/syncom_as/README.md b/v2/syncom_as/README.md similarity index 100% rename from syncom_as/README.md rename to v2/syncom_as/README.md diff --git a/syncom_as/main.py b/v2/syncom_as/main.py similarity index 100% rename from syncom_as/main.py rename to v2/syncom_as/main.py diff --git a/syncom_as/sr_init.py b/v2/syncom_as/sr_init.py similarity index 100% rename from syncom_as/sr_init.py rename to v2/syncom_as/sr_init.py diff --git a/syncom_as/sr_passive.py b/v2/syncom_as/sr_passive.py similarity index 100% rename from syncom_as/sr_passive.py rename to v2/syncom_as/sr_passive.py diff --git a/syncom_as/syncom.py b/v2/syncom_as/syncom.py similarity index 100% rename from syncom_as/syncom.py rename to v2/syncom_as/syncom.py diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 9967fcd..2ec9841 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -657,7 +657,7 @@ be queued for execution. Note that the synchronous sequence event.set() event.clear() ``` -will cause waiting task(s) to resume in round-robin order. +will cause any tasks waiting on the event to resume in round-robin order. The `Event` class is an efficient and effective way to synchronise tasks, but firmware applications often have multiple tasks running `while True:` loops. @@ -670,7 +670,7 @@ approaches: `Event`s are set before clearing them and setting its own `Event`. 2. Consumers do not loop, running to completion. Producer uses `gather` to instantiate consumer tasks and wait on their completion. - 3. `Event`s are replaced with a single [Barrier](./TUTORIAL.md#37-barrier) + 3. `Event` instances are replaced with a single [Barrier](./TUTORIAL.md#37-barrier) instance. Solution 1 suffers a proliferation of `Event`s and suffers an inefficient @@ -678,7 +678,7 @@ busy-wait where the producer waits on N events. Solution 2 is inefficient with constant creation of tasks. Arguably the `Barrier` class is the best approach. **NOTE NOT YET SUPPORTED - see Message class** -An Event can also provide a means of communication between an interrupt handler +An Event can also provide a means of communication between a soft interrupt handler and a task. The handler services the hardware and sets an event which is tested in slow time by the task. See [PR6106](https://github.com/micropython/micropython/pull/6106). From 390dd05d7b2b5fa3ab3f7d5c3e5971866a277f4b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 6 Sep 2020 16:03:03 +0100 Subject: [PATCH 045/305] Move V2 resources to v2 directory. --- README.md | 195 +++++------------------------------------------------- 1 file changed, 18 insertions(+), 177 deletions(-) diff --git a/README.md b/README.md index 1ac319a..9457625 100644 --- a/README.md +++ b/README.md @@ -5,189 +5,30 @@ MicroPython provides `uasyncio` which is a subset of this, optimised for small code size and high performance on bare metal targets. This repository provides documentation, tutorial material and code to aid in its effective use. -## uasyncio versions +# uasyncio version 3 -Damien has completely rewritten `uasyncio` which has been released as V3.0. See -[PR5332](https://github.com/micropython/micropython/pull/5332). +Damien has completely rewritten `uasyncio` which was released as V3.0. See +[PR5332](https://github.com/micropython/micropython/pull/5332). This is now +incorporated in release build V1.13 and subsequent daily builds. -There is currently a choice to be made over whether to run V2 or V3. To run V2, -ensure your firmware build is official MicroPython V1.12 and follow the -`uasyncio` installation instructions in [the V2 tutorial](./TUTORIAL.md). For -V3, install the latest daily build which includes `uasyncio`. +Resources for V3 may be found in the `v3` directory. These include a guide to +porting applications from V2, an updated tutorial, synchronisation primitives +and various applications and demos. -I strongly recommend V3 unless you need the `fast_io` variant of V2. When V3 -acquires this ability (it is planned) and appears in a release build I expect -to obsolete all V2 material in this repo. - -Resources for V3 and an updated tutorial may be found in the v3 directory. +V2 should now be regarded as obsolete for almost all applications with the +possible exception mentioned below. ### [Go to V3 docs](./v3/README.md) -The remainder of this document is for users of V2 and its `fast_io` variant. - -# 1. uasyncio V2 - -This repo also contains an optional `fast_io` variant of `uasyncio` V2. This -variant offers high I/O performance and also includes workrounds for many of -the bugs in V2. (Bugs properly fixed in V3.) - -## Reasons for running V2 - -In general I recommend V3, especially for new projects. It is better in every -respect bar one: the `fast_io` variant of V2 currently offers superior I/O -performance, relative both to V2 and V3. - -The main reason for running official V2 is that many existing libraries have -not yet been ported to V3. Some will run without change, but those using more -advanced features of `uasyncio` may not. - -## 1.1 Resources - - * [A tutorial](./TUTORIAL.md) An introductory tutorial on asynchronous - programming and the use of the `uasyncio` library. - * [Asynchronous device drivers](./DRIVERS.md). A module providing drivers for - devices such as switches and pushbuttons. - * [Synchronisation primitives](./PRIMITIVES.md). Provides commonly used - synchronisation primitives plus an API for task cancellation and monitoring. - * [A driver for an IR remote control](./nec_ir/README.md) This is intended as - an example of an asynchronous device driver. It decodes signals received from - infra red remote controls using the popular NEC protocol. - * [A driver for the HTU21D](./htu21d/README.md) temperature and humidity - sensor. This is intended to be portable across platforms and is another - example of an asynchronous device driver. - * [A driver for character LCD displays](./HD44780/README.md). A simple - asynchronous interface to displays based on the Hitachi HD44780 chip. - * [A driver for GPS modules](./gps/README.md) Runs a background task to read - and decode NMEA sentences, providing constantly updated position, course, - altitude and time/date information. - * [Communication using I2C slave mode.](./i2c/README.md) Enables a Pyboard to - to communicate with another MicroPython device using stream I/O. The Pyboard - achieves bidirectional communication with targets such as an ESP8266. - * [Communication between devices](./syncom_as/README.md) Enables MicroPython - boards to communicate without using a UART. This is hardware agnostic but - slower than the I2C version. - -## 1.2 The fast_io variant - -This comprises two parts. - 1. The [fast_io](./FASTPOLL.md) version of `uasyncio` is a "drop in" - replacement for the official version 2 providing bug fixes, additional - functionality and, in certain respects, higher performance. - 2. An optional extension module enabling the [fast_io](./FASTPOLL.md) version - to run with very low power draw. This is Pyboard-only including Pyboard D. - -Official `uasyncio` suffers from high levels of latency when scheduling I/O in -typical applications. It also has an issue which can cause bidirectional -devices such as UART's to block. The `fast_io` version fixes the bug. It also -provides a facility for reducing I/O latency which can substantially improve -the performance of stream I/O drivers. It provides other features aimed at -providing greater control over scheduling behaviour. - -To take advantage of the reduced latency device drivers should be written to -employ stream I/O. To operate at low latency they are simply run under the -`fast_io` version. The [tutorial](./TUTORIAL.md#64-writing-streaming-device-drivers) -has details of how to write streaming drivers. - -The current `fast_io` version 0.24 fixes an issue with task cancellation and -timeouts. In `uasyncio` version 2.0, where a coroutine is waiting on a -`sleep()` or on I/O, a timeout or cancellation is deferred until the coroutine -is next scheduled. This introduces uncertainty into when the coroutine is -stopped. - -## 1.2.1 A Pyboard-only low power module - -This is documented [here](./lowpower/README.md). In essence a Python file is -placed on the device which configures the `fast_io` version of `uasyncio` to -reduce power consumption at times when it is not busy. This provides a means of -using `uasyncio` in battery powered projects. This is decidedly experimental: -hopefully `uasyncio` V3 will introduce power saving in a less hacky manner. - -## 1.3 Under the hood - -[Under the hood](./UNDER_THE_HOOD.md) A guide to help understand the V2 -`uasyncio` code. For scheduler geeks and those wishing to modify `uasyncio`. - -## 1.4 Synchronisation Primitives - -All solutions listed below work with stock `uasyncio` V2 or `fast_io`. - -The CPython `asyncio` library supports these synchronisation primitives: - * `Lock` - * `Event` - * `gather` - * `Semaphore` and `BoundedSemaphore`. - * `Condition`. - * `Queue`. This was implemented by Paul Sokolvsky in `uasyncio.queues`. - -See [CPython docs](https://docs.python.org/3/library/asyncio-sync.html). - -The file `asyn.py` contains implementations of these, also - * `Barrier` An additional synchronisation primitive. - * Cancellation decorators and classes: these are workrounds for the bug where - in V2 cancellation does not occur promptly. - * Support for `gather`. - -The `Event` class in `asyn.py` provides a nonstandard option to supply a data -value to the `.set` method and to retrieve this with `.value`. It is also an -awaitable class. - -#### These are documented [here](./PRIMITIVES.md) - -## 1.5 Switches, Pushbuttons and Timeouts - -The file `aswitch.py` provides support for: - * `Delay_ms` A software retriggerable monostable or watchdog. - * `Switch` Debounced switch and pushbutton classes with callbacks. - * `Pushbutton` - -#### It is documented [here](./DRIVERS.md) - -# 2. Version 2.0 usage notes - -These notes are intended for users familiar with `asyncio` under CPython. - -The MicroPython language is based on CPython 3.4. The `uasyncio` library -supports a subset of the CPython 3.4 `asyncio` library with some V3.5 -extensions. In addition there are non-standard extensions to optimise services -such as millisecond level timing and task cancellation. Its design focus is on -high performance and scheduling is performed without RAM allocation. - -The `uasyncio` library supports the following Python 3.5 features: - - * `async def` and `await` syntax. - * Awaitable classes (using `__iter__` rather than `__await__`). - * Asynchronous context managers. - * Asynchronous iterators. - * Event loop methods `call_soon` and `call_later`. - * `sleep(seconds)`. - -It supports millisecond level timing with the following: - - * Event loop method `call_later_ms` - * uasyncio `sleep_ms(time)` - -`uasyncio` V2 supports coroutine timeouts and cancellation. - - * `wait_for(coro, t_secs)` runs `coro` with a timeout. - * `cancel(coro)` tags `coro` for cancellation when it is next scheduled. - -Classes `Task` and `Future` are not supported. - -## 2.1 Asynchronous I/O - -Asynchronous I/O (`StreamReader` and `StreamWriter` classes) support devices -with streaming drivers, such as UARTs and sockets. It is now possible to write -streaming device drivers in Python. +# uasyncio version 2 -## 2.2 Time values +The official version 2 is entirely superseded by V3, which improves on it in +every respect. -For timing asyncio uses floating point values of seconds. The `uasyncio.sleep` -method accepts floats (including sub-second values) or integers. Note that in -MicroPython the use of floats implies RAM allocation which incurs a performance -penalty. The design of `uasyncio` enables allocation-free scheduling. In -applications where performance is an issue, integers should be used and the -millisecond level functions (with integer arguments) employed where necessary. +I produced a modified `fast_io` variant of V2 which is in use for some +specialist purposes. It enables I/O to be scheduled at high priority. Currently +this schedules I/O significantly faster than V3; the maintainers plan to +improve `uasyncio` I/O scheduling. When this is complete I intend to delete all +V2 material. -The `loop.time` method returns an integer number of milliseconds whereas -CPython returns a floating point number of seconds. `call_at` follows the -same convention. +All V2 resources are in the V2 subdirectory: [see this README](./v2/README.md). From 8dc241f5126177d73831c19688e59ce765515ab1 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 7 Sep 2020 10:30:06 +0100 Subject: [PATCH 046/305] V3 tutorial: add note re V1.13 release. --- v3/docs/TUTORIAL.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 2ec9841..c426d5d 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -108,8 +108,9 @@ CPython V3.8 and above. ## 0.1 Installing uasyncio on bare metal -No installation is necessary if a daily build of firmware is installed. The -version may be checked by issuing at the REPL: +No installation is necessary if a daily build of firmware is installed or +release build V1.13 or later. The version may be checked by issuing at +the REPL: ```python import uasyncio print(uasyncio.__version__) From 84ae852f3e01602342399fbd4b1839497fc88c12 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 9 Sep 2020 09:40:20 +0100 Subject: [PATCH 047/305] primitives/irq_event.py added. Document ISR interfacing. --- v3/docs/DRIVERS.md | 102 ++++++++++++++++++++++++-- v3/docs/TUTORIAL.md | 40 +++++++--- v3/primitives/irq_event.py | 43 +++++++++++ v3/primitives/message.py | 3 +- v3/primitives/tests/irq_event_test.py | 57 ++++++++++++++ 5 files changed, 227 insertions(+), 18 deletions(-) create mode 100644 v3/primitives/irq_event.py create mode 100644 v3/primitives/tests/irq_event_test.py diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 11b7cfe..260a66b 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -11,6 +11,9 @@ events. The asynchronous ADC supports pausing a task until the value read from an ADC goes outside defined bounds. +An IRQ_EVENT class provides a means of interfacing uasyncio to hard or soft +interrupt service routines. + # 1. Contents 1. [Contents](./DRIVERS.md#1-contents) @@ -24,9 +27,10 @@ goes outside defined bounds. 5. [ADC monitoring](./DRIVERS.md#5-adc-monitoring) Pause until an ADC goes out of bounds 5.1 [AADC class](./DRIVERS.md#51-aadc-class) 5.2 [Design note](./DRIVERS.md#52-design-note) - 6. [Additional functions](./DRIVERS.md#6-additional-functions) - 6.1 [launch](./DRIVERS.md#61-launch) Run a coro or callback interchangeably - 6.2 [set_global_exception](./DRIVERS.md#62-set_global_exception) Simplify debugging with a global exception handler + 6. [IRQ_EVENT](./DRIVERS.md#6-irq_event) + 7. [Additional functions](./DRIVERS.md#6-additional-functions) + 7.1 [launch](./DRIVERS.md#71-launch) Run a coro or callback interchangeably + 7.2 [set_global_exception](./DRIVERS.md#72-set_global_exception) Simplify debugging with a global exception handler ###### [Tutorial](./TUTORIAL.md#contents) @@ -331,9 +335,95 @@ this for applications requiring rapid response. ###### [Contents](./DRIVERS.md#1-contents) -# 6. Additional functions +# 6. IRQ_EVENT + +Interfacing an interrupt service routine to `uasyncio` requires care. It is +invalid to issue `create_task` or to trigger an `Event` in an ISR as it can +cause a race condition in the scheduler. It is intended that `Event` will +become compatible with soft IRQ's in a future revison of `uasyncio`. + +Currently there are two ways of interfacing hard or soft IRQ's with `uasyncio`. +One is to use a busy-wait loop as per the +[Message](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#36-message) +primitive. A more efficient approach is to use this `IRQ_EVENT` class. The API +is a subset of the `Event` class, so if official `Event` becomes thread-safe +it may readily be substituted. The `IRQ_EVENT` class uses uses the `uasyncio` +I/O mechanism to achieve thread-safe operation. + +Unlike `Event` only one task can wait on an `IRQ_EVENT`. + +Constructor: + * This has no args. + +Synchronous Methods: + * `set()` Initiates the event. May be called from a hard or soft ISR. Returns + fast. + * `is_set()` Returns `True` if the irq_event is set. + * `clear()` This does nothing; its purpose is to enable code to be written + compatible with a future thread-safe `Event` class, with the ISR setting then + immediately clearing the event. + +Asynchronous Method: + * `wait` Pause until irq_event is set. The irq_event is cleared. + +A single task waits on the event by issuing `await irq_event.wait()`; execution +pauses until the ISR issues `irq_event.set()`. Execution of the paused task +resumes when it is next scheduled. Under current `uasyncio` (V3.0.0) scheduling +of the paused task does not occur any faster than using busy-wait. In typical +use the ISR services the interrupting device, saving received data, then sets +the irq_event to trigger processing of the received data. + +If interrupts occur faster than `uasyncio` can schedule the paused task, more +than one interrupt may occur before the paused task runs. + +Example usage (assumes a Pyboard with pins X1 and X2 linked): +```python +from machine import Pin +from pyb import LED +import uasyncio as asyncio +import micropython +from primitives.irq_event import IRQ_EVENT + +micropython.alloc_emergency_exception_buf(100) + +driver = Pin(Pin.board.X2, Pin.OUT) +receiver = Pin(Pin.board.X1, Pin.IN) +evt_rx = IRQ_EVENT() # IRQ_EVENT instance for receiving Pin + +def pin_han(pin): # Hard IRQ handler. Typically services a device + evt_rx.set() # then issues this which returns quickly + +receiver.irq(pin_han, Pin.IRQ_FALLING, hard=True) # Set up hard ISR + +async def pulse_gen(pin): + while True: + await asyncio.sleep_ms(500) + pin(not pin()) + +async def red_handler(evt_rx, iterations): + led = LED(1) + for x in range(iterations): + await evt_rx.wait() # Pause until next interrupt + print(x) + led.toggle() + +async def irq_test(iterations): + pg = asyncio.create_task(pulse_gen(driver)) + await red_handler(evt_rx, iterations) + pg.cancel() + +def test(iterations=20): + try: + asyncio.run(irq_test(iterations)) + finally: + asyncio.new_event_loop() +``` + +###### [Contents](./DRIVERS.md#1-contents) + +# 7. Additional functions -## 6.1 Launch +## 7.1 Launch Importe as follows: ```python @@ -345,7 +435,7 @@ runs it and returns the callback's return value. If a coro is passed, it is converted to a `task` and run asynchronously. The return value is the `task` instance. A usage example is in `primitives/switch.py`. -## 6.2 set_global_exception +## 7.2 set_global_exception Import as follows: ```python diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index c426d5d..e44b4e8 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -35,7 +35,7 @@ REPL. 3.7 [Barrier](./TUTORIAL.md#37-barrier) 3.8 [Delay_ms](./TUTORIAL.md#38-delay_ms-class) Software retriggerable delay. 3.9 [Synchronising to hardware](./TUTORIAL.md#39-synchronising-to-hardware) - Debouncing switches and pushbuttons. Taming ADC's. + Debouncing switches and pushbuttons. Taming ADC's. Interfacing interrupts. 4. [Designing classes for asyncio](./TUTORIAL.md#4-designing-classes-for-asyncio) 4.1 [Awaitable classes](./TUTORIAL.md#41-awaitable-classes)      4.1.1 [Use in context managers](./TUTORIAL.md#411-use-in-context-managers) @@ -879,22 +879,30 @@ asyncio.run(queue_go(4)) This is an unofficial primitive with no counterpart in CPython asyncio. -This is similar to the `Event` class. It provides the following: +This is similar to the `Event` class. It differs in that: * `.set()` has an optional data payload. * `.set()` is capable of being called from a hard or soft interrupt service routine - a feature not yet available in the more efficient official `Event`. * It is an awaitable class. -The `.set()` method can accept an optional data value of any type. A task +For interfacing to interrupt service routines see also +[the IRQ_EVENT class](./DRIVERS.md#6-irq_event) which is more efficient but +lacks the payload feature. + +Limitation: `Message` is intended for 1:1 operation where a single task waits +on a message from another task or ISR. The receiving task should issue +`.clear`. + +The `.set()` method can accept an optional data value of any type. The task waiting on the `Message` can retrieve it by means of `.value()`. Note that `.clear()` will set the value to `None`. One use for this is for the task -setting the `Message` to issue `.set(utime.ticks_ms())`. A task waiting on the -`Message` can determine the latency incurred, for example to perform +setting the `Message` to issue `.set(utime.ticks_ms())`. The task waiting on +the `Message` can determine the latency incurred, for example to perform compensation for this. -Like `Event`, `Message` provides a way for one or more tasks to pause until -another flags them to continue. A `Message` object is instantiated and made -accessible to all tasks using it: +Like `Event`, `Message` provides a way a task to pause until another flags it +to continue. A `Message` object is instantiated and made accessible to the task +using it: ```python import uasyncio as asyncio @@ -920,9 +928,16 @@ A `Message` can provide a means of communication between an interrupt handler and a task. The handler services the hardware and issues `.set()` which is tested in slow time by the task. -Currently its behaviour differs from that of `Event` where multiple tasks wait -on a `Message`. This may change: it is therefore recommended to use `Message` -instances with only one receiving task. +Constructor: + * Optional arg `delay_ms=0` Polling interval. +Synchronous methods: + * `set(data=None)` Trigger the message with optional payload. + * `is_set()` Return `True` if the message is set. + * `clear()` Clears the triggered status and sets payload to `None`. + * `value()` Return the payload. +Asynchronous Method: + * `wait` Pause until message is triggered. You can also `await` the message as + per the above example. ###### [Contents](./TUTORIAL.md#contents) @@ -1110,6 +1125,9 @@ The following hardware-related classes are documented [here](./DRIVERS.md): * `AADC` Asynchronous ADC. A task can pause until the value read from an ADC goes outside defined bounds. Bounds can be absolute or relative to the current value. + * `IRQ_EVENT` A way to interface between hard or soft interrupt service + routines and `uasyncio`. Discusses the hazards of apparently obvious ways such + as issuing `.create_task` or using the `Event` class. ###### [Contents](./TUTORIAL.md#contents) diff --git a/v3/primitives/irq_event.py b/v3/primitives/irq_event.py new file mode 100644 index 0000000..fa3fab5 --- /dev/null +++ b/v3/primitives/irq_event.py @@ -0,0 +1,43 @@ +# irq_event.py Interface between uasyncio and asynchronous events +# A thread-safe class. API is a subset of Event. + +# Copyright (c) 2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +import io + +MP_STREAM_POLL_RD = const(1) +MP_STREAM_POLL = const(3) +MP_STREAM_ERROR = const(-1) + +class IRQ_EVENT(io.IOBase): + def __init__(self): + self.state = False # False=unset; True=set + self.sreader = asyncio.StreamReader(self) + + def wait(self): + await self.sreader.readline() + self.state = False + + def set(self): + self.state = True + return self + + def is_set(self): + return self.state + + def readline(self): + return b'\n' + + def clear(self): + pass # See docs + + def ioctl(self, req, arg): + ret = MP_STREAM_ERROR + if req == MP_STREAM_POLL: + ret = 0 + if arg & MP_STREAM_POLL_RD: + if self.state: + ret |= MP_STREAM_POLL_RD + return ret diff --git a/v3/primitives/message.py b/v3/primitives/message.py index bf06558..fc24bb7 100644 --- a/v3/primitives/message.py +++ b/v3/primitives/message.py @@ -16,7 +16,8 @@ # message.clear() should be issued # This more efficient version is commented out because Event.set is not ISR -# friendly. TODO If it gets fixed, reinstate this (tested) version. +# friendly. TODO If it gets fixed, reinstate this (tested) version and update +# tutorial for 1:n operation. #class Message(asyncio.Event): #def __init__(self, _=0): #self._data = None diff --git a/v3/primitives/tests/irq_event_test.py b/v3/primitives/tests/irq_event_test.py new file mode 100644 index 0000000..fa24f5c --- /dev/null +++ b/v3/primitives/tests/irq_event_test.py @@ -0,0 +1,57 @@ +# irq_event_test.py Test for irq_event class +# Run on Pyboard with link between X1 and X2 + +# Copyright (c) 2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +# from primitives.tests.irq_event_test import test +# test() + +from machine import Pin +from pyb import LED +import uasyncio as asyncio +import micropython +from primitives.irq_event import IRQ_EVENT + +def printexp(): + print('Test expects a Pyboard with X1 and X2 linked. Expected output:') + print('\x1b[32m') + print('Flashes red LED and prints numbers 0-19') + print('\x1b[39m') + print('Runtime: 20s') + +printexp() + +micropython.alloc_emergency_exception_buf(100) + +driver = Pin(Pin.board.X2, Pin.OUT) +receiver = Pin(Pin.board.X1, Pin.IN) +evt_rx = IRQ_EVENT() + +def pin_han(pin): + evt_rx.set() + +receiver.irq(pin_han, Pin.IRQ_FALLING, hard=True) + +async def pulse_gen(pin): + while True: + await asyncio.sleep_ms(500) + pin(not pin()) + +async def red_handler(evt_rx): + led = LED(1) + for x in range(20): + await evt_rx.wait() + print(x) + led.toggle() + +async def irq_test(): + pg = asyncio.create_task(pulse_gen(driver)) + await red_handler(evt_rx) + pg.cancel() + +def test(): + try: + asyncio.run(irq_test()) + finally: + asyncio.new_event_loop() From 931dc7519665ad38770bee869c1d660ae9cf3005 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 9 Sep 2020 10:02:19 +0100 Subject: [PATCH 048/305] v3/docs/DRIVERS.md Imporove IRQ_EVENT documentation. --- v3/docs/DRIVERS.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 260a66b..d4cb2d1 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -27,8 +27,8 @@ interrupt service routines. 5. [ADC monitoring](./DRIVERS.md#5-adc-monitoring) Pause until an ADC goes out of bounds 5.1 [AADC class](./DRIVERS.md#51-aadc-class) 5.2 [Design note](./DRIVERS.md#52-design-note) - 6. [IRQ_EVENT](./DRIVERS.md#6-irq_event) - 7. [Additional functions](./DRIVERS.md#6-additional-functions) + 6. [IRQ_EVENT](./DRIVERS.md#6-irq_event) Interfacing to interrupt service routines. + 7. [Additional functions](./DRIVERS.md#7-additional-functions) 7.1 [launch](./DRIVERS.md#71-launch) Run a coro or callback interchangeably 7.2 [set_global_exception](./DRIVERS.md#72-set_global_exception) Simplify debugging with a global exception handler @@ -44,6 +44,7 @@ Drivers are imported with: from primitives.switch import Switch from primitives.pushbutton import Pushbutton from primitives.aadc import AADC +from primitives.irq_event import IRQ_EVENT ``` There is a test/demo program for the Switch and Pushbutton classes. On import this lists available tests. It assumes a Pyboard with a switch or pushbutton @@ -58,6 +59,12 @@ is run as follows: from primitives.tests.adctest import test test() ``` +The test for the `IRQ_EVENT` class requires a Pyboard with pins X1 and X2 +linked. It is run as follows: +```python +from primitives.tests.irq_event_test import test +test() +``` ###### [Contents](./DRIVERS.md#1-contents) @@ -340,7 +347,10 @@ this for applications requiring rapid response. Interfacing an interrupt service routine to `uasyncio` requires care. It is invalid to issue `create_task` or to trigger an `Event` in an ISR as it can cause a race condition in the scheduler. It is intended that `Event` will -become compatible with soft IRQ's in a future revison of `uasyncio`. +become compatible with soft IRQ's in a future revison of `uasyncio`. See +[iss 6415](https://github.com/micropython/micropython/issues/6415), +[PR 6106](https://github.com/micropython/micropython/pull/6106) and +[iss 5795](https://github.com/micropython/micropython/issues/5795). Currently there are two ways of interfacing hard or soft IRQ's with `uasyncio`. One is to use a busy-wait loop as per the From d6ae022d077e9c5b0b02c76ec44cf2146712e0be Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 9 Sep 2020 13:56:12 +0100 Subject: [PATCH 049/305] v3/primitives/irq_event.py simplify. --- v3/docs/TUTORIAL.md | 40 +++++++++++++++++++------------------- v3/primitives/irq_event.py | 7 +++---- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index e44b4e8..ee20a12 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1612,15 +1612,20 @@ The behaviour is "correct": CPython `asyncio` behaves identically. Ref At heart all interfaces between `uasyncio` and external asynchronous events rely on polling. Hardware requiring a fast response may use an interrupt. But the interface between the interrupt service routine (ISR) and a user task will -be polled. For example the ISR might trigger an `Event` or set a global flag, -while a task awaiting the outcome polls the object each time it is -scheduled. +be polled. For example the ISR might set a global flag with the task awaiting +the outcome polling the flag each time it is scheduled. This is explicit +polling. -Polling may be effected in two ways, explicitly or implicitly. The latter is -performed by using the `stream I/O` mechanism which is a system designed for -stream devices such as UARTs and sockets. At its simplest explicit polling may -consist of code like this: +Polling may also be effected implicitly. This is performed by using the +`stream I/O` mechanism which is a system designed for stream devices such as +UARTs and sockets. +There are hazards involved with approaches to interfacing ISR's which appear to +avoid polling. See [the IRQ_EVENT class](./DRIVERS.md#6-irq_event) for details. +This class is a thread-safe way to implement this interface with efficient +implicit polling. + + At its simplest explicit polling may consist of code like this: ```python async def poll_my_device(): global my_flag # Set by device ISR @@ -1631,9 +1636,9 @@ async def poll_my_device(): await asyncio.sleep(0) ``` -In place of a global, an instance variable, an `Event` object or an instance of -an awaitable class might be used. Explicit polling is discussed -further [below](./TUTORIAL.md#62-polling-hardware-with-a-task). +In place of a global, an instance variable or an instance of an awaitable class +might be used. Explicit polling is discussed further +[below](./TUTORIAL.md#62-polling-hardware-with-a-task). Implicit polling consists of designing the driver to behave like a stream I/O device such as a socket or UART, using `stream I/O`. This polls devices using @@ -1641,7 +1646,7 @@ Python's `select.poll` system: because the polling is done in C it is faster and more efficient than explicit polling. The use of `stream I/O` is discussed [here](./TUTORIAL.md#63-using-the-stream-mechanism). -Owing to its efficiency implicit polling benefits most fast I/O device drivers: +Owing to its efficiency implicit polling most benefits fast I/O device drivers: streaming drivers can be written for many devices not normally considered as streaming devices [section 6.4](./TUTORIAL.md#64-writing-streaming-device-drivers). @@ -1678,10 +1683,6 @@ and `sleep_ms()` functions. The worst-case value for this overrun may be calculated by summing, for every other task, the worst-case execution time between yielding to the scheduler. -The [fast_io](./FASTPOLL.md) version of `uasyncio` in this repo provides a way -to ensure that stream I/O is polled on every iteration of the scheduler. It is -hoped that official `uasyncio` will adopt code to this effect in due course. - ###### [Contents](./TUTORIAL.md#contents) ## 6.2 Polling hardware with a task @@ -1915,14 +1916,14 @@ class MillisecTimer(io.IOBase): self.sreader = asyncio.StreamReader(self) def __iter__(self): - await self.sreader.readline() + await self.sreader.read(1) def __call__(self, ms): self.end = utime.ticks_add(utime.ticks_ms(), ms) return self - def readline(self): - return b'\n' + def read(self, _): + pass def ioctl(self, req, arg): ret = MP_STREAM_ERROR @@ -1979,10 +1980,9 @@ class PinCall(io.IOBase): v = self.pinval if v and self.cb_rise is not None: self.cb_rise(*self.cbr_args) - return b'\n' + return if not v and self.cb_fall is not None: self.cb_fall(*self.cbf_args) - return b'\n' def ioctl(self, req, arg): ret = MP_STREAM_ERROR diff --git a/v3/primitives/irq_event.py b/v3/primitives/irq_event.py index fa3fab5..8b59fb8 100644 --- a/v3/primitives/irq_event.py +++ b/v3/primitives/irq_event.py @@ -17,18 +17,17 @@ def __init__(self): self.sreader = asyncio.StreamReader(self) def wait(self): - await self.sreader.readline() + await self.sreader.read(1) self.state = False def set(self): self.state = True - return self def is_set(self): return self.state - def readline(self): - return b'\n' + def read(self, _): + pass def clear(self): pass # See docs From da6362665f54d318ab01b58fc75cdedc7482ae09 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 10 Oct 2020 11:26:31 +0100 Subject: [PATCH 050/305] sched.py Fix bug where coro was scheduled after 1000s. --- v3/as_drivers/sched/sched.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/v3/as_drivers/sched/sched.py b/v3/as_drivers/sched/sched.py index bbf2a2b..2993d5b 100644 --- a/v3/as_drivers/sched/sched.py +++ b/v3/as_drivers/sched/sched.py @@ -14,8 +14,7 @@ async def schedule(func, *args, times=None, **kwargs): while times is None or times > 0: tw = fcron(int(time())) # Time to wait (s) while tw > 0: # While there is still time to wait - tw = min(tw, maxt) - await asyncio.sleep(tw) + await asyncio.sleep(min(tw, maxt)) tw -= maxt res = launch(func, args) if times is not None: From 706f928b6f36132ac08793974f2def3def7f280e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 26 Oct 2020 08:59:07 +0000 Subject: [PATCH 051/305] v3 tutorial: add note about polling vs interrupts. --- v3/docs/TUTORIAL.md | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index ee20a12..c91502e 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -77,7 +77,8 @@ REPL. 8.4 [Scheduling in uasyncio](./TUTORIAL.md#84-scheduling-in-uasyncio) 8.5 [Why cooperative rather than pre-emptive?](./TUTORIAL.md#85-why-cooperative-rather-than-pre-emptive) 8.6 [Communication](./TUTORIAL.md#86-communication) - 8.7 [Polling](./TUTORIAL.md#87-polling) +9. [Polling vs Interrupts](./TUTORIAL.md#9-polling-vs-interrupts) A common +source of confusion. ###### [Main README](../README.md) @@ -2558,12 +2559,34 @@ communications; in a cooperative system these are seldom required. ###### [Contents](./TUTORIAL.md#contents) -## 8.7 Polling - -Some hardware devices such as the Pyboard accelerometer don't support -interrupts, and therefore must be polled (i.e. checked periodically). Polling -can also be used in conjunction with interrupt handlers: the interrupt handler -services the hardware and sets a flag. A task polls the flag: if it's set it -handles the data and clears the flag. A better approach is to use an `Event`. +# 9. Polling vs Interrupts + +The role of interrupts in cooperative systems has proved to be a source of +confusion in the forum. The merit of an interrupt service routine (ISR) is that +it runs very soon after the event causing it. On a Pyboard, Python code may be +running 15μs after a hardware change, enabling prompt servicing of hardware and +accurate timing of signals. + +The question arises whether it is possible to use interrupts to cause a task to +be scheduled at reduced latency. It is easy to show that, in a cooperative +scheduler, interrupts offer no latency benefit compared to polling the hardware +directly. + +The reason for this is that a cooperative scheduler only schedules tasks when +another task has yielded control. Consider a system with a number of concurrent +tasks, where the longest any task blocks before yielding to the scheduler is +`N`ms. In such a system, even with an ideal scheduler, the worst-case latency +between a hardware event occurring and its handling task beingnscheduled is +`N`ms, assuming that the mechanism for detecting the event adds no latency of +its own. + +In practice `N` is likely to be on the order of many ms. On fast hardware there +will be a negligible performance difference between polling the hardware and +polling a flag set by an ISR. On hardware such as ESP8266 and ESP32 the ISR +approach will probably be slower owing to the long and variable interrupt +latency of these platforms. + +Using an ISR to set a flag is probably best reserved for situations where an +ISR is already needed for other reasons. ###### [Contents](./TUTORIAL.md#contents) From 6069a28eb0230eff5fc17027e34f30e916130d08 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 26 Oct 2020 15:59:06 +0000 Subject: [PATCH 052/305] v3 tutorial: add note about polling vs interrupts. --- v3/docs/TUTORIAL.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index c91502e..b1edf30 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2589,4 +2589,8 @@ latency of these platforms. Using an ISR to set a flag is probably best reserved for situations where an ISR is already needed for other reasons. +The above comments refer to an ideal scheduler. Currently `uasyncio` is not in +this category, with worst-case latency being > `N`ms. The conclusions remain +valid. + ###### [Contents](./TUTORIAL.md#contents) From 7b7653ed0e47902bb24ee6aeaee40a123e30f3d6 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 4 Dec 2020 14:00:41 +0000 Subject: [PATCH 053/305] Tutorial: fix broken link. --- v3/docs/TUTORIAL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index b1edf30..3808252 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1820,7 +1820,7 @@ provide a solution if the data source supports it. ### 6.3.1 A UART driver example -The program [auart_hd.py](./as_demos/auart_hd.py) illustrates a method of +The program [auart_hd.py](../as_demos/auart_hd.py) illustrates a method of communicating with a half duplex device such as one responding to the modem 'AT' command set. Half duplex means that the device never sends unsolicited data: its transmissions are always in response to a command from the master. From cc73e0773103e511a72d7298ed950be3586d8956 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 17 Dec 2020 16:51:45 +0000 Subject: [PATCH 054/305] Fix close bug in userver demo. --- v3/as_drivers/client_server/heartbeat.py | 5 +++++ v3/as_drivers/client_server/userver.py | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/v3/as_drivers/client_server/heartbeat.py b/v3/as_drivers/client_server/heartbeat.py index 68a821e..822eac5 100644 --- a/v3/as_drivers/client_server/heartbeat.py +++ b/v3/as_drivers/client_server/heartbeat.py @@ -14,6 +14,11 @@ async def heartbeat(tms): elif platform == 'esp8266': from machine import Pin led = Pin(2, Pin.OUT, value=1) + elif platform == 'esp32': + # Some boards have an LED + #from machine import Pin + #led = Pin(2, Pin.OUT, value=1) + return # Reference board has no LED elif platform == 'linux': return # No LED else: diff --git a/v3/as_drivers/client_server/userver.py b/v3/as_drivers/client_server/userver.py index 0bebc5f..f87a31a 100644 --- a/v3/as_drivers/client_server/userver.py +++ b/v3/as_drivers/client_server/userver.py @@ -45,11 +45,11 @@ async def run_client(self, sreader, swriter): await sreader.wait_closed() print('Client {} socket closed.'.format(self.cid)) - def close(self): + async def close(self): print('Closing server') self.server.close() await self.server.wait_closed() - print('Server closed') + print('Server closed.') server = Server() try: @@ -57,5 +57,5 @@ def close(self): except KeyboardInterrupt: print('Interrupted') # This mechanism doesn't work on Unix build. finally: - server.close() + asyncio.run(server.close()) _ = asyncio.new_event_loop() From 0302d6d416b55f6ce7fba835d3e4126346d1dc6a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 21 Dec 2020 18:17:59 +0000 Subject: [PATCH 055/305] Add undocumented metrics module. --- v3/as_drivers/metrics/__init__.py | 0 v3/as_drivers/metrics/metrics.py | 37 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 v3/as_drivers/metrics/__init__.py create mode 100644 v3/as_drivers/metrics/metrics.py diff --git a/v3/as_drivers/metrics/__init__.py b/v3/as_drivers/metrics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v3/as_drivers/metrics/metrics.py b/v3/as_drivers/metrics/metrics.py new file mode 100644 index 0000000..fffb365 --- /dev/null +++ b/v3/as_drivers/metrics/metrics.py @@ -0,0 +1,37 @@ +import uasyncio as asyncio +from utime import ticks_us, ticks_diff + + +def metrics(): + ncalls = 0 + max_d = 0 + min_d = 100_000_000 + tot_d = 0 + st = 'Max {}μs Min {}μs Avg {}μs No. of calls {} Freq {}' + async def func(): + nonlocal ncalls, max_d, min_d, tot_d + while True: + tstart = ticks_us() + t_last = None + while ticks_diff(t := ticks_us(), tstart) < 10_000_000: + await asyncio.sleep(0) + if ncalls: + dt = ticks_diff(t, t_last) + max_d = max(max_d, dt) + min_d = min(min_d, dt) + tot_d += dt + ncalls += 1 + t_last = t + print(st.format(max_d, min_d, tot_d//ncalls, ncalls, ncalls//10)) + ncalls = 0 + max_d = 0 + min_d = 100_000_000 + tot_d = 0 + return func + +async def main(): + asyncio.create_task(metrics()()) + while True: + await asyncio.sleep(0) + +asyncio.run(main()) From 3f3b9c37cc90ca2d83cdd755bb4046dc68e74cf0 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 22 Dec 2020 12:19:51 +0000 Subject: [PATCH 056/305] metrics.py: add free RAM figure. --- v3/as_drivers/metrics/metrics.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/v3/as_drivers/metrics/metrics.py b/v3/as_drivers/metrics/metrics.py index fffb365..4623ea0 100644 --- a/v3/as_drivers/metrics/metrics.py +++ b/v3/as_drivers/metrics/metrics.py @@ -1,4 +1,9 @@ +# metrics.py Check on scheduling performance of an application +# Released under the MIT licence +# Copyright (c) Peter Hinch 2020 + import uasyncio as asyncio +import gc from utime import ticks_us, ticks_diff @@ -23,15 +28,18 @@ async def func(): ncalls += 1 t_last = t print(st.format(max_d, min_d, tot_d//ncalls, ncalls, ncalls//10)) + gc.collect() + print('mem free', gc.mem_free()) ncalls = 0 max_d = 0 min_d = 100_000_000 tot_d = 0 return func +# Example of call async def main(): - asyncio.create_task(metrics()()) + asyncio.create_task(metrics()()) # Note the syntax while True: await asyncio.sleep(0) -asyncio.run(main()) +#asyncio.run(main()) From db07eb2574a2303b36bb4a6b511c3dbf2dadf870 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 30 Dec 2020 14:16:41 +0000 Subject: [PATCH 057/305] Tutorial: update link to official docs --- v3/docs/TUTORIAL.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 3808252..e57896d 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2081,8 +2081,7 @@ You may want to consider running a task which issues: ``` This assumes `import gc` has been issued. The purpose of this is discussed -[here](http://docs.micropython.org/en/latest/pyboard/reference/constrained.html) -in the section on the heap. +[here](http://docs.micropython.org/en/latest/reference/constrained.html#the-heap). ###### [Contents](./TUTORIAL.md#contents) From 690d0fcdfc7d78c85947b7348f100433d3a7efba Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 11 Feb 2021 12:17:32 +0000 Subject: [PATCH 058/305] Add updated syncom. --- v3/as_drivers/syncom/main.py | 4 + v3/as_drivers/syncom/sr_init.py | 86 +++++++++++ v3/as_drivers/syncom/sr_passive.py | 64 ++++++++ v3/as_drivers/syncom/syncom.py | 239 +++++++++++++++++++++++++++++ v3/docs/SYNCOM.md | 219 ++++++++++++++++++++++++++ 5 files changed, 612 insertions(+) create mode 100644 v3/as_drivers/syncom/main.py create mode 100644 v3/as_drivers/syncom/sr_init.py create mode 100644 v3/as_drivers/syncom/sr_passive.py create mode 100644 v3/as_drivers/syncom/syncom.py create mode 100644 v3/docs/SYNCOM.md diff --git a/v3/as_drivers/syncom/main.py b/v3/as_drivers/syncom/main.py new file mode 100644 index 0000000..3397298 --- /dev/null +++ b/v3/as_drivers/syncom/main.py @@ -0,0 +1,4 @@ +import webrepl +webrepl.start() +import sr_passive +sr_passive.test() diff --git a/v3/as_drivers/syncom/sr_init.py b/v3/as_drivers/syncom/sr_init.py new file mode 100644 index 0000000..8953751 --- /dev/null +++ b/v3/as_drivers/syncom/sr_init.py @@ -0,0 +1,86 @@ +# sr_init.py Test of synchronous comms library. Initiator end. + +# The MIT License (MIT) +# +# Copyright (c) 2016 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Run on Pyboard +from machine import Pin, Signal +from pyb import LED +import uasyncio as asyncio +from utime import ticks_ms, ticks_diff +from syncom import SynCom, SynComError + + +async def initiator_task(channel): + while True: + so = ['test', 0, 0] + for x in range(4): # Test full duplex by sending 4 in succession + so[1] = x + channel.send(so) + await asyncio.sleep_ms(0) + while True: # Receive the four responses + si = await channel.await_obj() # Deal with queue + if si is None: + print('Timeout: restarting.') + return + print('initiator received', si) + if si[1] == 3: # received last one + break + while True: # At 2 sec intervals send an object and get response + await asyncio.sleep(2) + print('sending', so) + channel.send(so) + tim = ticks_ms() + so = await channel.await_obj() # wait for response + duration = ticks_diff(ticks_ms(), tim) + if so is None: + print('Timeout: restarting.') + return + print('initiator received', so, 'timing', duration) + +async def heartbeat(): + led = LED(1) + while True: + await asyncio.sleep_ms(500) + led.toggle() + +def test(): + dout = Pin(Pin.board.Y5, Pin.OUT_PP, value = 0) # Define pins + ckout = Pin(Pin.board.Y6, Pin.OUT_PP, value = 0) # Don't assert clock until data is set + din = Pin(Pin.board.Y7, Pin.IN) + ckin = Pin(Pin.board.Y8, Pin.IN) + reset = Pin(Pin.board.Y4, Pin.OPEN_DRAIN) + sig_reset = Signal(reset, invert = True) + + channel = SynCom(False, ckin, ckout, din, dout, sig_reset, 10000) + + loop = asyncio.get_event_loop() + loop.create_task(heartbeat()) + loop.create_task(channel.start(initiator_task)) + try: + loop.run_forever() + except KeyboardInterrupt: + pass + finally: + ckout.value(0) + +test() diff --git a/v3/as_drivers/syncom/sr_passive.py b/v3/as_drivers/syncom/sr_passive.py new file mode 100644 index 0000000..652d8b5 --- /dev/null +++ b/v3/as_drivers/syncom/sr_passive.py @@ -0,0 +1,64 @@ +# sr_passive.py Test of synchronous comms library. Passive end. + +# The MIT License (MIT) +# +# Copyright (c) 2016 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Run on ESP8266 +import uasyncio as asyncio +from syncom import SynCom +from machine import Pin, freq +import gc + +async def passive_task(chan): + while True: + obj = await chan.await_obj() + if obj is not None: # Ignore timeouts +# print('passive received: ', obj) + obj[2] += 1 # modify object and send it back + chan.send(obj) + +async def heartbeat(): + led = Pin(2, Pin.OUT) + while True: + await asyncio.sleep_ms(500) + led(not led()) + gc.collect() + +def test(): + freq(160000000) + dout = Pin(14, Pin.OUT, value = 0) # Define pins + ckout = Pin(15, Pin.OUT, value = 0) # clocks must be initialised to zero. + din = Pin(13, Pin.IN) + ckin = Pin(12, Pin.IN) + + channel = SynCom(True, ckin, ckout, din, dout) + loop = asyncio.get_event_loop() + loop.create_task(heartbeat()) + loop.create_task(channel.start(passive_task)) + try: + loop.run_forever() + except KeyboardInterrupt: + pass + finally: + ckout(0) + +test() diff --git a/v3/as_drivers/syncom/syncom.py b/v3/as_drivers/syncom/syncom.py new file mode 100644 index 0000000..82b665e --- /dev/null +++ b/v3/as_drivers/syncom/syncom.py @@ -0,0 +1,239 @@ +# syncom.py Synchronous communication channel between two MicroPython +# platforms. 4 June 2017 +# Uses uasyncio. + +# The MIT License (MIT) +# +# Copyright (c) 2017-2021 Peter Hinch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Timing: was 4.5mS per char between Pyboard and ESP8266 i.e. ~1.55Kbps. But +# this version didn't yield on every bit, invalidating t/o detection. +# New asyncio version yields on every bit. +# Instantaneous bit rate running ESP8266 at 160MHz: 1.6Kbps +# Mean throughput running test programs 8.8ms per char (800bps). + +from utime import ticks_diff, ticks_ms +import uasyncio as asyncio +from micropython import const +import ujson + +_BITS_PER_CH = const(7) +_BITS_SYN = const(8) +_SYN = const(0x9d) +_RX_BUFLEN = const(100) + +class SynComError(Exception): + pass + +class SynCom: + def __init__(self, passive, ckin, ckout, din, dout, pin_reset=None, + timeout=0, string_mode=False, verbose=True): # Signal unsupported on rp2 + self.passive = passive + self.string_mode = string_mode + self._running = False # _run coro is down + self._synchronised = False + self.verbose = verbose + self.idstr = 'passive' if self.passive else 'initiator' + + self.ckin = ckin # Interface pins + self.ckout = ckout + self.din = din + self.dout = dout + self.pin_reset = pin_reset + + self._timeout = timeout # In ms. 0 == No timeout. + self.lsttx = [] # Queue of strings to send + self.lstrx = [] # Queue of received strings + +# Start interface and initiate an optional user task. If a timeout and reset +# signal are specified and the target times out, the target is reset and the +# interface restarted. If a user task is provided, this must return if a +# timeout occurs (i.e. not running() or await_obj returns None). +# If it returns for other (error) reasons, a timeout event is forced. + async def start(self, user_task=None, awaitable=None): + while True: + if not self._running: # Restarting + self.lstrx = [] # Clear down queues + self.lsttx = [] + self._synchronised = False + asyncio.create_task(self._run()) # Reset target (if possible) + while not self._synchronised: # Wait for sync + await asyncio.sleep_ms(100) + if user_task is None: + while self._running: + await asyncio.sleep_ms(100) + else: + await user_task(self) # User task must quit on timeout + # If it quit for other reasons force a t/o exception + self.stop() + await asyncio.sleep_ms(0) + if awaitable is not None: + await awaitable() # Optional user coro + +# Can be used to force a failure + def stop(self): + self._running = False + self.dout(0) + self.ckout(0) + +# Queue an object for tx. Convert to string NOW: snapshot of current +# object state + def send(self, obj): + if self.string_mode: + self.lsttx.append(obj) # strings are immutable + else: + self.lsttx.append(ujson.dumps(obj)) + +# Number of queued objects (None on timeout) + def any(self): + if self._running: + return len(self.lstrx) + +# Wait for an object. Return None on timeout. +# If in string mode returns a string (or None on t/o) + async def await_obj(self, t_ms=10): + while self._running: + await asyncio.sleep_ms(t_ms) + if len(self.lstrx): + return self.lstrx.pop(0) + +# running() is False if the target has timed out. + def running(self): + return self._running + +# Private methods + async def _run(self): + self.indata = 0 # Current data bits + self.inbits = 0 + self.odata = _SYN + self.phase = 0 # Interface initial conditions + if self.passive: + self.dout(0) + self.ckout(0) + else: + self.dout(self.odata & 1) + self.ckout(1) + self.odata >>= 1 # we've sent that bit + self.phase = 1 + if self.pin_reset is not None: + self.verbose and print(self.idstr, ' resetting target...') + self.pin_reset(0) + await asyncio.sleep_ms(100) + self.pin_reset(1) + await asyncio.sleep(1) # let target settle down + + self.verbose and print(self.idstr, ' awaiting sync...') + try: + self._running = True # False on failure: can be cleared by other tasks + while self.indata != _SYN: # Don't hog CPU while waiting for start + await self._synchronise() + self._synchronised = True + self.verbose and print(self.idstr, ' synchronised.') + + sendstr = '' # string for transmission + send_idx = None # character index. None: no current string + getstr = '' # receive string + rxbuf = bytearray(_RX_BUFLEN) + rxidx = 0 + while True: + if send_idx is None: + if len(self.lsttx): + sendstr = self.lsttx.pop(0) # oldest first + send_idx = 0 + if send_idx is not None: + if send_idx < len(sendstr): + self.odata = ord(sendstr[send_idx]) + send_idx += 1 + else: + send_idx = None + if send_idx is None: # send zeros when nothing to send + self.odata = 0 + if self.passive: + await self._get_byte_passive() + else: + await self._get_byte_active() + if self.indata: # Optimisation: buffer reduces allocations. + if rxidx >= _RX_BUFLEN: # Buffer full: append to string. + getstr = ''.join((getstr, bytes(rxbuf).decode())) + rxidx = 0 + rxbuf[rxidx] = self.indata + rxidx += 1 + elif rxidx or len(getstr): # Got 0 but have data so string is complete. + # Append buffer. + getstr = ''.join((getstr, bytes(rxbuf[:rxidx]).decode())) + if self.string_mode: + self.lstrx.append(getstr) + else: + try: + self.lstrx.append(ujson.loads(getstr)) + except: # ujson fail means target has crashed + raise SynComError + getstr = '' # Reset for next string + rxidx = 0 + + except SynComError: + if self._running: + self.verbose and print('SynCom Timeout.') + else: + self.verbose and print('SynCom was stopped.') + finally: + self.stop() + + async def _get_byte_active(self): + inbits = 0 + for _ in range(_BITS_PER_CH): + inbits = await self._get_bit(inbits) # LSB first + self.indata = inbits + + async def _get_byte_passive(self): + self.indata = await self._get_bit(self.inbits) # MSB is outstanding + inbits = 0 + for _ in range(_BITS_PER_CH - 1): + inbits = await self._get_bit(inbits) + self.inbits = inbits + + async def _synchronise(self): # wait for clock + t = ticks_ms() + while self.ckin() == self.phase ^ self.passive ^ 1: + # Other tasks can clear self._running by calling stop() + if (self._timeout and ticks_diff(ticks_ms(), t) > self._timeout) or not self._running: + raise SynComError + await asyncio.sleep_ms(0) + self.indata = (self.indata | (self.din() << _BITS_SYN)) >> 1 + odata = self.odata + self.dout(odata & 1) + self.odata = odata >> 1 + self.phase ^= 1 + self.ckout(self.phase) # set clock + + async def _get_bit(self, dest): + t = ticks_ms() + while self.ckin() == self.phase ^ self.passive ^ 1: + if (self._timeout and ticks_diff(ticks_ms(), t) > self._timeout) or not self._running: + raise SynComError + await asyncio.sleep_ms(0) + dest = (dest | (self.din() << _BITS_PER_CH)) >> 1 + obyte = self.odata + self.dout(obyte & 1) + self.odata = obyte >> 1 + self.phase ^= 1 + self.ckout(self.phase) + return dest diff --git a/v3/docs/SYNCOM.md b/v3/docs/SYNCOM.md new file mode 100644 index 0000000..4eeafe8 --- /dev/null +++ b/v3/docs/SYNCOM.md @@ -0,0 +1,219 @@ +# Communication between MicroPython hardware boards + +This provides a means of communication between two devices, each running +MicroPython, where a UART cannot be used. An example is where one device is an +ESP8266 board. While this has one bidirectional UART, this may be in use either +as a REPL console, for viewing debug output, or for other puposes. + +It is intended for use in asynchronous programs and uses uasyncio. + +The module offers a bidirectional full duplex communication channel between two +hardware devices. Its unit of communication is an arbitrary Python object +making for simple application. In an alternative mode for resource constrained +devices, the unit of communication is a string. + +Physically it uses a 4-wire interface plus an additional wire to enable the +host to issue a hardware reset to the target in the event that the target +crashes or becomes unresponsive. Where the target is an ESP8266 this can occur +for various reasons including network issues where sockets can block +indefinitely. + +The module will run on devices with minimal features and makes no assumptions +about processing performance: at a physical level the interface is synchronous. +If each device has two pins which can be used for output and two for input and +supports uasyncio it should work. + +###### [Main README](./README.md) + +## Example usage + +```python +import uasyncio as asyncio +from syncom import SynCom +from machine import Pin + + # Task just echoes objects back +async def passive_task(chan): + while True: + obj = await chan.await_obj() + chan.send(obj) + +mtx = Pin(14, Pin.OUT, value = 0) # Define pins +mckout = Pin(15, Pin.OUT, value = 0) # clock must be initialised to zero. +mrx = Pin(13, Pin.IN) +mckin = Pin(12, Pin.IN) + +channel = SynCom(True, mckin, mckout, mrx, mtx) +try: + asyncio.run(channel.start(passive_task)) +except KeyboardInterrupt: + pass +finally: + mckout(0) # For a subsequent run + _ = asyncio.new_event_loop() +``` + +## Advantages + + * Readily portable to any MicroPython platform. + * It does not use hardware features such as interrupts or timers. + * Hardware requirement: two arbitrary output pins and two input pins on each + device. + * The interface is synchronous, having no timing dependencies. + * It supports full duplex communications (concurrent send and receive). + * The unit of transmission is an arbitrary Python object. + * All methods are non-blocking. + * Small: <200 lines of Python. + +## Limitations + + * The interface is an alternative to I2C or SPI and is intended for directly + linked devices sharing a common power supply. + * It is slow. With a Pyboard linked to an ESP8266 clocked at 160MHz, the + peak bit rate is 1.6Kbps. Mean throughput is about 800bps. + In practice throughput will depend on the performance of the slowest device + and the behaviour of other tasks. + +## Rationale + +The obvious question is why not use I2C or SPI. The reason is the nature of the +slave interfaces: these protocols are designed for the case where the slave is +a hardware device which guarantees a timely response. The MicroPython slave +drivers achieve this by means of blocking system calls. Synchronising master +and slave is difficult because the master needs to ensure that the slave is +running the blocking call before transmitting. For the slave to do anything +useful the code must be designed to ensure that the call exits at the end of a +message. + +Further such blocking calls are incompatible with asynchronous programming. + +The two ends of the link are defined as `initiator` and `passive`. These +describe their roles in initialisation. Once running the protocol is +symmetrical and the choice as to which unit to assign to each role is +arbitrary: the test programs assume that the Pyboard is the initiator. + +# Files + + * syncom.py The library. + * sr_init.py Test program configured for Pyboard: run with sr_passive.py on + the other device. + * sr_passive.py Test program configured for ESP8266: sr_init.py runs on other + end of link. + +# Hardware connections + +Each device has the following logical connections, `din`, `dout`, `ckin`, +`ckout`. The `din` (data in) of one device is linked to `dout` (data out) +of the other, and vice versa. Likewise the clock signals `ckin` and `ckout`. + +To enable a response to crash detection a pin on the Pyboard is connected to +the Reset pin on the target. The polarity of the reset pulse is assumed to be +active low. + +| Initiator | Passive | Pyboard | ESP8266 | +|:-----------:|:-----------:|:-------:|:-------:| +| reset (o/p) | reset (i/p) | Y4 | reset | +| dout (o/p) | din (i/p) | Y5 | 14 | +| ckout (o/p) | ckin (i/p) | Y6 | 15 | +| din (i/p) | dout (o/p) | Y7 | 13 | +| ckin (i/p) | ckout (o/p) | Y8 | 12 | + + +# class SynCom + +A SynCom instance is idle until its `start` task is scheduled. The driver +causes the host device to resets the target and wait for synchronisation. When +the interface is running the passed user task is launched; unless an error +occurs this runs forever using the interface as required by the application. If +crash detection is required the user task should check for a timeout. In this +event the user task should return. This causes the target to be reset and the +interface to re-synchronise. The user task is then re-launched. + +## Constructor + +Positional arguments: + + 1. `passive` Boolean. One end of the link sets this `True`, the other + `False`. + 2. `ckin` An initialised input `Pin` instance. + 3. `ckout` An initialised output `Pin` instance. It should be set to zero. + 4. `din` An initialised input `Pin` instance. + 5. `dout` An initialised output `Pin` instance. + 6. `sig_reset` (optional) default `None`. A `Pin` instance. + 7. `timeout` (optional) default 0. Units ms. See below. + 8. `string_mode` (optional) default `False`. See String Mode below. + 9. `verbose` (optional) default `True`. If set, debug messages will be + output to the REPL. + +## Synchronous Methods + + * `send` Argument an arbitrary Python object (or a string in string mode). + Puts the item on the queue for transmission. + * `any` No args. + Returns the number of received objects on the receive queue. + * `running` No args. + Returns `True` if the channel is running, `False` if the target has timed + out. + +## Asynchronous Methods (tasks) + + * `await_obj` Argument `t_ms` default 10ms. See below. + Wait for reception of a Python object or string and return it. If the + interface times out (because the target has crashed) return `None`. + * `start` Optional args `user_task`, `fail_delay`. + Starts the interface. If a user_task is provided this will be launched when + synchronisation is achived. The user task should return if a timeout is + detected (by `await_obj` returning `None`). On return the driver will wait + for `fail_delay` (see below) before asserting the reset signal to reset the + target. The user task will be re-launched when synchronisation is achieved. + The user_task is passed a single argument: the SynCom instance. If the user + task is a bound method it should therefore be declared as taking two args: + `self` and the channel. + +The `fail_delay` (in seconds) is a convenience to allow user tasks to +terminate before the user task is restarted. On detection of a timeout an +application should set a flag to cause tasks instantiated by the user task to +terminate, then issue `return`. This avoids unlimited growth of the task +queue. + +The `t_ms` argument to `await_obj` determines how long the task pauses +between checks for received data. Longer intervals increase latency but +(possibly) improve raw throughput. + +# Notes + +## Synchronisation + +When the host launches the `start` coroutine it runs forever. It resets the +target which instantiates a SynCom object and launches its `start` coroutine. +The two then synchronise by repeatedly transmitting a `_SYN` character. Once +this has been received the link is synchronised and the user task is launched. + +The user task runs forever on the target. On the host it may return if a target +timeout is detected. In this instance the host's `start` task waits for the +optional `fail_delay` before resetting the target and re-synchronising the +interface. The user task, which ran to completion, is re-launched. + +## String Mode + +By default `ujson` is used to serialise data. This can be avoided by sending +strings to the remote platform, which must then interpret the strings as +required by the application. The protocol places some restrictions. The bytes +must not include 0, and they are limited to 7 bits. The latter limitation can +be removed (with small performance penalty) by changing the value of +`_BITS_PER_CH` to 8. The limitations allow for normal UTF8 strings. + +## Timing + +The timing measurements in Limitations above were performed as follows. A logic +analyser was connected to one of the clock signals and the time for one +character (7 bits) to be transferred was measured (note that a bit is +transferred on each edge of the clock). This produced figures for the raw bits +per second throughput of the bitbanged interface. + +The value produced by the test programs (sr_init.py and sr_passive.py) is the +total time to send an object and receive it having been echoed back by the +ESP8266. This includes encoding the object as a string, transmitting it, +decoding and modifying it, followed by similar processing to send it back. +Hence converting the figures to bps will produce a lower figure (on the order +of 656bps at 160MHz). From 0dacf40f44516da871af97783c0127241cba9ae7 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 13 Feb 2021 10:48:48 +0000 Subject: [PATCH 059/305] v3 tutorial: document current_task() method. --- v3/docs/TUTORIAL.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index e57896d..eb9bb0c 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1548,6 +1548,20 @@ async def bar(): print('Task is now cancelled') asyncio.run(bar()) ``` +As of [PR6883](https://github.com/micropython/micropython/pull/6883) the +`current_task()` method is supported. This enables a task to pass itself to +other tasks, enabling them to cancel it. It also facilitates the following +pattern: + +```python +class Foo: + async def run(self): + self.task = asyncio.current_task() + # code omitted + + def cancel(self): + self.task.cancel() +``` ###### [Contents](./TUTORIAL.md#contents) From 6296f21f08e9e74f4b30a12fa2bc76d13ab54a30 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 16 Feb 2021 18:04:12 +0000 Subject: [PATCH 060/305] Tutorial: document ThreadSafeFlag and remove irq_event.py. --- v3/docs/DRIVERS.md | 107 +------------ v3/docs/TUTORIAL.md | 214 ++++++++++++++++---------- v3/primitives/irq_event.py | 42 ----- v3/primitives/tests/irq_event_test.py | 57 ------- 4 files changed, 137 insertions(+), 283 deletions(-) delete mode 100644 v3/primitives/irq_event.py delete mode 100644 v3/primitives/tests/irq_event_test.py diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index d4cb2d1..6c6ae4d 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -11,9 +11,6 @@ events. The asynchronous ADC supports pausing a task until the value read from an ADC goes outside defined bounds. -An IRQ_EVENT class provides a means of interfacing uasyncio to hard or soft -interrupt service routines. - # 1. Contents 1. [Contents](./DRIVERS.md#1-contents) @@ -27,10 +24,9 @@ interrupt service routines. 5. [ADC monitoring](./DRIVERS.md#5-adc-monitoring) Pause until an ADC goes out of bounds 5.1 [AADC class](./DRIVERS.md#51-aadc-class) 5.2 [Design note](./DRIVERS.md#52-design-note) - 6. [IRQ_EVENT](./DRIVERS.md#6-irq_event) Interfacing to interrupt service routines. - 7. [Additional functions](./DRIVERS.md#7-additional-functions) - 7.1 [launch](./DRIVERS.md#71-launch) Run a coro or callback interchangeably - 7.2 [set_global_exception](./DRIVERS.md#72-set_global_exception) Simplify debugging with a global exception handler + 6. [Additional functions](./DRIVERS.md#6-additional-functions) + 6.1 [launch](./DRIVERS.md#61-launch) Run a coro or callback interchangeably + 6.2 [set_global_exception](./DRIVERS.md#62-set_global_exception) Simplify debugging with a global exception handler ###### [Tutorial](./TUTORIAL.md#contents) @@ -342,100 +338,11 @@ this for applications requiring rapid response. ###### [Contents](./DRIVERS.md#1-contents) -# 6. IRQ_EVENT - -Interfacing an interrupt service routine to `uasyncio` requires care. It is -invalid to issue `create_task` or to trigger an `Event` in an ISR as it can -cause a race condition in the scheduler. It is intended that `Event` will -become compatible with soft IRQ's in a future revison of `uasyncio`. See -[iss 6415](https://github.com/micropython/micropython/issues/6415), -[PR 6106](https://github.com/micropython/micropython/pull/6106) and -[iss 5795](https://github.com/micropython/micropython/issues/5795). - -Currently there are two ways of interfacing hard or soft IRQ's with `uasyncio`. -One is to use a busy-wait loop as per the -[Message](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#36-message) -primitive. A more efficient approach is to use this `IRQ_EVENT` class. The API -is a subset of the `Event` class, so if official `Event` becomes thread-safe -it may readily be substituted. The `IRQ_EVENT` class uses uses the `uasyncio` -I/O mechanism to achieve thread-safe operation. - -Unlike `Event` only one task can wait on an `IRQ_EVENT`. - -Constructor: - * This has no args. - -Synchronous Methods: - * `set()` Initiates the event. May be called from a hard or soft ISR. Returns - fast. - * `is_set()` Returns `True` if the irq_event is set. - * `clear()` This does nothing; its purpose is to enable code to be written - compatible with a future thread-safe `Event` class, with the ISR setting then - immediately clearing the event. - -Asynchronous Method: - * `wait` Pause until irq_event is set. The irq_event is cleared. - -A single task waits on the event by issuing `await irq_event.wait()`; execution -pauses until the ISR issues `irq_event.set()`. Execution of the paused task -resumes when it is next scheduled. Under current `uasyncio` (V3.0.0) scheduling -of the paused task does not occur any faster than using busy-wait. In typical -use the ISR services the interrupting device, saving received data, then sets -the irq_event to trigger processing of the received data. - -If interrupts occur faster than `uasyncio` can schedule the paused task, more -than one interrupt may occur before the paused task runs. - -Example usage (assumes a Pyboard with pins X1 and X2 linked): -```python -from machine import Pin -from pyb import LED -import uasyncio as asyncio -import micropython -from primitives.irq_event import IRQ_EVENT - -micropython.alloc_emergency_exception_buf(100) - -driver = Pin(Pin.board.X2, Pin.OUT) -receiver = Pin(Pin.board.X1, Pin.IN) -evt_rx = IRQ_EVENT() # IRQ_EVENT instance for receiving Pin - -def pin_han(pin): # Hard IRQ handler. Typically services a device - evt_rx.set() # then issues this which returns quickly +# 6. Additional functions -receiver.irq(pin_han, Pin.IRQ_FALLING, hard=True) # Set up hard ISR +## 6.1 Launch -async def pulse_gen(pin): - while True: - await asyncio.sleep_ms(500) - pin(not pin()) - -async def red_handler(evt_rx, iterations): - led = LED(1) - for x in range(iterations): - await evt_rx.wait() # Pause until next interrupt - print(x) - led.toggle() - -async def irq_test(iterations): - pg = asyncio.create_task(pulse_gen(driver)) - await red_handler(evt_rx, iterations) - pg.cancel() - -def test(iterations=20): - try: - asyncio.run(irq_test(iterations)) - finally: - asyncio.new_event_loop() -``` - -###### [Contents](./DRIVERS.md#1-contents) - -# 7. Additional functions - -## 7.1 Launch - -Importe as follows: +Import as follows: ```python from primitives import launch ``` @@ -445,7 +352,7 @@ runs it and returns the callback's return value. If a coro is passed, it is converted to a `task` and run asynchronously. The return value is the `task` instance. A usage example is in `primitives/switch.py`. -## 7.2 set_global_exception +## 6.2 set_global_exception Import as follows: ```python diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index eb9bb0c..1782c6e 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -31,10 +31,11 @@ REPL. 3.4 [Semaphore](./TUTORIAL.md#34-semaphore)      3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore) 3.5 [Queue](./TUTORIAL.md#35-queue) - 3.6 [Message](./TUTORIAL.md#36-message) + 3.6 [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) Synchronisation with asynchronous events. 3.7 [Barrier](./TUTORIAL.md#37-barrier) 3.8 [Delay_ms](./TUTORIAL.md#38-delay_ms-class) Software retriggerable delay. - 3.9 [Synchronising to hardware](./TUTORIAL.md#39-synchronising-to-hardware) + 3.9 [Message](./TUTORIAL.md#39-message) + 3.10 [Synchronising to hardware](./TUTORIAL.md#310-synchronising-to-hardware) Debouncing switches and pushbuttons. Taming ADC's. Interfacing interrupts. 4. [Designing classes for asyncio](./TUTORIAL.md#4-designing-classes-for-asyncio) 4.1 [Awaitable classes](./TUTORIAL.md#41-awaitable-classes) @@ -644,8 +645,7 @@ asyncio.run(main()) ``` Constructor: no args. Synchronous Methods: - * `set` Initiates the event. Currently may not be called in an interrupt - context. + * `set` Initiates the event. * `clear` No args. Clears the event. * `is_set` No args. Returns `True` if the event is set. @@ -679,10 +679,9 @@ Solution 1 suffers a proliferation of `Event`s and suffers an inefficient busy-wait where the producer waits on N events. Solution 2 is inefficient with constant creation of tasks. Arguably the `Barrier` class is the best approach. -**NOTE NOT YET SUPPORTED - see Message class** -An Event can also provide a means of communication between a soft interrupt handler -and a task. The handler services the hardware and sets an event which is tested -in slow time by the task. See [PR6106](https://github.com/micropython/micropython/pull/6106). +**WARNING** +`Event` methods must not be called from an interrupt service routine (ISR). The +`Event` class is not thread safe. See [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag). ###### [Contents](./TUTORIAL.md#contents) @@ -876,69 +875,57 @@ asyncio.run(queue_go(4)) ###### [Contents](./TUTORIAL.md#contents) -## 3.6 Message +## 3.6 ThreadSafeFlag -This is an unofficial primitive with no counterpart in CPython asyncio. +This official class provides an efficient means of synchronising a task with a +truly asynchronous event such as a hardware interrupt service routine or code +running in another thread. It operates in a similar way to `Event` with the +following key differences: + * It is thread safe: the `set` event may be called from asynchronous code. + * It is self-clearing. + * Only one task may wait on the flag. -This is similar to the `Event` class. It differs in that: - * `.set()` has an optional data payload. - * `.set()` is capable of being called from a hard or soft interrupt service - routine - a feature not yet available in the more efficient official `Event`. - * It is an awaitable class. +The latter limitation may be addressed by having a task wait on a +`ThreadSafeFlag` before setting an `Event`. Multiple tasks may wait on that +`Event`. -For interfacing to interrupt service routines see also -[the IRQ_EVENT class](./DRIVERS.md#6-irq_event) which is more efficient but -lacks the payload feature. - -Limitation: `Message` is intended for 1:1 operation where a single task waits -on a message from another task or ISR. The receiving task should issue -`.clear`. - -The `.set()` method can accept an optional data value of any type. The task -waiting on the `Message` can retrieve it by means of `.value()`. Note that -`.clear()` will set the value to `None`. One use for this is for the task -setting the `Message` to issue `.set(utime.ticks_ms())`. The task waiting on -the `Message` can determine the latency incurred, for example to perform -compensation for this. - -Like `Event`, `Message` provides a way a task to pause until another flags it -to continue. A `Message` object is instantiated and made accessible to the task -using it: +Synchronous method: + * `set` Triggers the flag. Like issuing `set` then `clear` to an `Event`. +Asynchronous method: + * `wait` Wait for the flag to be set. If the flag is already set then it + returns immediately. +Usage example: triggering from a hard ISR. ```python import uasyncio as asyncio -from primitives.message import Message +from pyb import Timer -async def waiter(msg): - print('Waiting for message') - await msg - res = msg.value() - print('waiter got', res) - msg.clear() +tsf = asyncio.ThreadSafeFlag() -async def main(): - msg = Message() - asyncio.create_task(waiter(msg)) - await asyncio.sleep(1) - msg.set('Hello') # Optional arg - await asyncio.sleep(1) +def cb(_): + tsf.set() -asyncio.run(main()) +async def foo(): + while True: + await tsf.wait() + # Could set an Event here to trigger multiple tasks + print('Triggered') + +tim = Timer(1, freq=1, callback=cb) + +asyncio.run(foo()) ``` -A `Message` can provide a means of communication between an interrupt handler -and a task. The handler services the hardware and issues `.set()` which is -tested in slow time by the task. +The current implementation provides no performance benefits against polling the +hardware. The `ThreadSafeFlag` uses the I/O mechanism. There are plans to +reduce the latency such that I/O is polled every time the scheduler acquires +control. This would provide the highest possible level of performance as +discussed in +[Polling vs Interrupts](./TUTORIAL.md#9-polling-vs-interrupts). -Constructor: - * Optional arg `delay_ms=0` Polling interval. -Synchronous methods: - * `set(data=None)` Trigger the message with optional payload. - * `is_set()` Return `True` if the message is set. - * `clear()` Clears the triggered status and sets payload to `None`. - * `value()` Return the payload. -Asynchronous Method: - * `wait` Pause until message is triggered. You can also `await` the message as - per the above example. +Regardless of performance issues, a key use for `ThreadSafeFlag` is where a +hardware device requires the use of an ISR for a μs level response. Having +serviced the device, it then flags an asynchronous routine, for example to +process data received. ###### [Contents](./TUTORIAL.md#contents) @@ -1117,7 +1104,68 @@ finally: asyncio.new_event_loop() # Clear retained state ``` -## 3.9 Synchronising to hardware +## 3.9 Message + +This is an unofficial primitive with no counterpart in CPython asyncio. It has +largely been superseded by [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag). + +This is similar to the `Event` class. It differs in that: + * `.set()` has an optional data payload. + * `.set()` is capable of being called from a hard or soft interrupt service + routine. + * It is an awaitable class. + +Limitation: `Message` is intended for 1:1 operation where a single task waits +on a message from another task or ISR. The receiving task should issue +`.clear`. + +The `.set()` method can accept an optional data value of any type. The task +waiting on the `Message` can retrieve it by means of `.value()`. Note that +`.clear()` will set the value to `None`. One use for this is for the task +setting the `Message` to issue `.set(utime.ticks_ms())`. The task waiting on +the `Message` can determine the latency incurred, for example to perform +compensation for this. + +Like `Event`, `Message` provides a way a task to pause until another flags it +to continue. A `Message` object is instantiated and made accessible to the task +using it: + +```python +import uasyncio as asyncio +from primitives.message import Message + +async def waiter(msg): + print('Waiting for message') + await msg + res = msg.value() + print('waiter got', res) + msg.clear() + +async def main(): + msg = Message() + asyncio.create_task(waiter(msg)) + await asyncio.sleep(1) + msg.set('Hello') # Optional arg + await asyncio.sleep(1) + +asyncio.run(main()) +``` +A `Message` can provide a means of communication between an interrupt handler +and a task. The handler services the hardware and issues `.set()` which is +tested in slow time by the task. + +Constructor: + * Optional arg `delay_ms=0` Polling interval. +Synchronous methods: + * `set(data=None)` Trigger the message with optional payload. + * `is_set()` Return `True` if the message is set. + * `clear()` Clears the triggered status and sets payload to `None`. + * `value()` Return the payload. +Asynchronous Method: + * `wait` Pause until message is triggered. You can also `await` the message as + per the above example. + +## 3.10 Synchronising to hardware The following hardware-related classes are documented [here](./DRIVERS.md): * `Switch` A debounced switch which can trigger open and close user callbacks. @@ -1126,9 +1174,6 @@ The following hardware-related classes are documented [here](./DRIVERS.md): * `AADC` Asynchronous ADC. A task can pause until the value read from an ADC goes outside defined bounds. Bounds can be absolute or relative to the current value. - * `IRQ_EVENT` A way to interface between hard or soft interrupt service - routines and `uasyncio`. Discusses the hazards of apparently obvious ways such - as issuing `.create_task` or using the `Event` class. ###### [Contents](./TUTORIAL.md#contents) @@ -1625,22 +1670,16 @@ The behaviour is "correct": CPython `asyncio` behaves identically. Ref # 6 Interfacing hardware At heart all interfaces between `uasyncio` and external asynchronous events -rely on polling. Hardware requiring a fast response may use an interrupt. But -the interface between the interrupt service routine (ISR) and a user task will -be polled. For example the ISR might set a global flag with the task awaiting -the outcome polling the flag each time it is scheduled. This is explicit -polling. - -Polling may also be effected implicitly. This is performed by using the -`stream I/O` mechanism which is a system designed for stream devices such as -UARTs and sockets. - -There are hazards involved with approaches to interfacing ISR's which appear to -avoid polling. See [the IRQ_EVENT class](./DRIVERS.md#6-irq_event) for details. -This class is a thread-safe way to implement this interface with efficient -implicit polling. +rely on polling. This is because of the cooperative nature of `uasyncio` +scheduling: the task which is expected to respond to the event can only acquire +control after another task has relinquished it. There are two ways to handle +this. + * Implicit polling: when a task yields and the scheduler acquires control, the + scheduler checks for an event. If it has occurred it schedules a waiting task. + This is the approach used by `ThreadSafeFlag`. + * Explicit polling: a user task does busy-wait polling on the hardware. - At its simplest explicit polling may consist of code like this: +At its simplest explicit polling may consist of code like this: ```python async def poll_my_device(): global my_flag # Set by device ISR @@ -1655,16 +1694,23 @@ In place of a global, an instance variable or an instance of an awaitable class might be used. Explicit polling is discussed further [below](./TUTORIAL.md#62-polling-hardware-with-a-task). -Implicit polling consists of designing the driver to behave like a stream I/O -device such as a socket or UART, using `stream I/O`. This polls devices using -Python's `select.poll` system: because the polling is done in C it is faster -and more efficient than explicit polling. The use of `stream I/O` is discussed +Implicit polling is more efficient and may gain further from planned +improvements to I/O scheduling. Aside from the use of `ThreadSafeFlag` it is +possible to write code which uses the same technique. This is by designing the +driver to behave like a stream I/O device such as a socket or UART, using +`stream I/O`. This polls devices using Python's `select.poll` system: because +polling is done in C it is faster and more efficient than explicit polling. The +use of `stream I/O` is discussed [here](./TUTORIAL.md#63-using-the-stream-mechanism). Owing to its efficiency implicit polling most benefits fast I/O device drivers: streaming drivers can be written for many devices not normally considered as streaming devices [section 6.4](./TUTORIAL.md#64-writing-streaming-device-drivers). +There are hazards involved with approaches to interfacing ISR's which appear to +avoid polling. It is invalid to issue `create_task` or to trigger an `Event` in +an ISR as these can cause a race condition in the scheduler. + ###### [Contents](./TUTORIAL.md#contents) ## 6.1 Timing issues @@ -2589,7 +2635,7 @@ The reason for this is that a cooperative scheduler only schedules tasks when another task has yielded control. Consider a system with a number of concurrent tasks, where the longest any task blocks before yielding to the scheduler is `N`ms. In such a system, even with an ideal scheduler, the worst-case latency -between a hardware event occurring and its handling task beingnscheduled is +between a hardware event occurring and its handling task being scheduled is `N`ms, assuming that the mechanism for detecting the event adds no latency of its own. diff --git a/v3/primitives/irq_event.py b/v3/primitives/irq_event.py deleted file mode 100644 index 8b59fb8..0000000 --- a/v3/primitives/irq_event.py +++ /dev/null @@ -1,42 +0,0 @@ -# irq_event.py Interface between uasyncio and asynchronous events -# A thread-safe class. API is a subset of Event. - -# Copyright (c) 2020 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -import uasyncio as asyncio -import io - -MP_STREAM_POLL_RD = const(1) -MP_STREAM_POLL = const(3) -MP_STREAM_ERROR = const(-1) - -class IRQ_EVENT(io.IOBase): - def __init__(self): - self.state = False # False=unset; True=set - self.sreader = asyncio.StreamReader(self) - - def wait(self): - await self.sreader.read(1) - self.state = False - - def set(self): - self.state = True - - def is_set(self): - return self.state - - def read(self, _): - pass - - def clear(self): - pass # See docs - - def ioctl(self, req, arg): - ret = MP_STREAM_ERROR - if req == MP_STREAM_POLL: - ret = 0 - if arg & MP_STREAM_POLL_RD: - if self.state: - ret |= MP_STREAM_POLL_RD - return ret diff --git a/v3/primitives/tests/irq_event_test.py b/v3/primitives/tests/irq_event_test.py deleted file mode 100644 index fa24f5c..0000000 --- a/v3/primitives/tests/irq_event_test.py +++ /dev/null @@ -1,57 +0,0 @@ -# irq_event_test.py Test for irq_event class -# Run on Pyboard with link between X1 and X2 - -# Copyright (c) 2020 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -# from primitives.tests.irq_event_test import test -# test() - -from machine import Pin -from pyb import LED -import uasyncio as asyncio -import micropython -from primitives.irq_event import IRQ_EVENT - -def printexp(): - print('Test expects a Pyboard with X1 and X2 linked. Expected output:') - print('\x1b[32m') - print('Flashes red LED and prints numbers 0-19') - print('\x1b[39m') - print('Runtime: 20s') - -printexp() - -micropython.alloc_emergency_exception_buf(100) - -driver = Pin(Pin.board.X2, Pin.OUT) -receiver = Pin(Pin.board.X1, Pin.IN) -evt_rx = IRQ_EVENT() - -def pin_han(pin): - evt_rx.set() - -receiver.irq(pin_han, Pin.IRQ_FALLING, hard=True) - -async def pulse_gen(pin): - while True: - await asyncio.sleep_ms(500) - pin(not pin()) - -async def red_handler(evt_rx): - led = LED(1) - for x in range(20): - await evt_rx.wait() - print(x) - led.toggle() - -async def irq_test(): - pg = asyncio.create_task(pulse_gen(driver)) - await red_handler(evt_rx) - pg.cancel() - -def test(): - try: - asyncio.run(irq_test()) - finally: - asyncio.new_event_loop() From c3005163ffbd9bbde192e2ca1f4ee9d78ebe72ce Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 18 Feb 2021 12:23:11 +0000 Subject: [PATCH 061/305] Changes to primitives/Message to use ThreadSafeFlag. --- v3/docs/TUTORIAL.md | 90 ++++++++++++++++++++++++++++--------- v3/primitives/message.py | 95 +++++++++++++++++----------------------- 2 files changed, 110 insertions(+), 75 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 1782c6e..a13c2c6 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -549,6 +549,16 @@ invocation lines alone should be changed. e.g. : from uasyncio import Semaphore, BoundedSemaphore from uasyncio import Queue ``` +##### Note on CPython compatibility + +CPython will throw a `RuntimeError` on first use of a synchronisation primitive +that was instantiated prior to starting the scheduler. By contrast +`MicroPython` allows instantiation in synchronous code executed before the +scheduler is started. Early instantiation can be advantageous in low resource +environments. For example a class might have a large buffer and bound `Event` +instances. Such a class should be instantiated early, before RAM fragmentation +sets in. + The following provides a discussion of the primitives. ###### [Contents](./TUTORIAL.md#contents) @@ -625,15 +635,15 @@ using it: import uasyncio as asyncio from uasyncio import Event -event = Event() -async def waiter(): +async def waiter(event): print('Waiting for event') await event.wait() # Pause here until event is set print('Waiter got event.') event.clear() # Flag caller and enable re-use of the event async def main(): - asyncio.create_task(waiter()) + event = Event() + asyncio.create_task(waiter(event)) await asyncio.sleep(2) print('Setting event') event.set() @@ -915,6 +925,19 @@ tim = Timer(1, freq=1, callback=cb) asyncio.run(foo()) ``` +Another example (posted by [Damien](https://github.com/micropython/micropython/pull/6886#issuecomment-779863757)): +```python +class AsyncPin: + def __init__(self, pin, trigger): + self.pin = pin + self.flag = ThreadSafeFlag() + self.pin.irq(lambda pin: self.flag.set(), trigger, hard=True) + + def wait_edge(self): + return self.flag.wait() +``` +You then call `await async_pin.wait_edge()`. + The current implementation provides no performance benefits against polling the hardware. The `ThreadSafeFlag` uses the I/O mechanism. There are plans to reduce the latency such that I/O is polled every time the scheduler acquires @@ -1106,25 +1129,22 @@ finally: ## 3.9 Message -This is an unofficial primitive with no counterpart in CPython asyncio. It has -largely been superseded by [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag). +This is an unofficial primitive with no counterpart in CPython asyncio. It uses +[ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) to provide an object similar +to `Event` but capable of being set in a hard ISR context. It extends +`ThreadSafeFlag` so that multiple tasks can wait on an ISR. -This is similar to the `Event` class. It differs in that: +It is similar to the `Event` class. It differs in that: * `.set()` has an optional data payload. * `.set()` is capable of being called from a hard or soft interrupt service routine. * It is an awaitable class. - -Limitation: `Message` is intended for 1:1 operation where a single task waits -on a message from another task or ISR. The receiving task should issue -`.clear`. + * The logic of `.clear` differs: it must be called by at least one task which + waits on the `Message`. The `.set()` method can accept an optional data value of any type. The task -waiting on the `Message` can retrieve it by means of `.value()`. Note that -`.clear()` will set the value to `None`. One use for this is for the task -setting the `Message` to issue `.set(utime.ticks_ms())`. The task waiting on -the `Message` can determine the latency incurred, for example to perform -compensation for this. +waiting on the `Message` can retrieve it by means of `.value()` or by awaiting +the `Message` as below. Like `Event`, `Message` provides a way a task to pause until another flags it to continue. A `Message` object is instantiated and made accessible to the task @@ -1136,8 +1156,7 @@ from primitives.message import Message async def waiter(msg): print('Waiting for message') - await msg - res = msg.value() + res = await msg print('waiter got', res) msg.clear() @@ -1155,15 +1174,44 @@ and a task. The handler services the hardware and issues `.set()` which is tested in slow time by the task. Constructor: - * Optional arg `delay_ms=0` Polling interval. + * No args. Synchronous methods: * `set(data=None)` Trigger the message with optional payload. - * `is_set()` Return `True` if the message is set. - * `clear()` Clears the triggered status and sets payload to `None`. + * `is_set()` Returns `True` if the `Message` is set, `False` if `.clear()` has + beein issued. + * `clear()` Clears the triggered status. At least one task waiting on the + message should issue `clear()`. * `value()` Return the payload. Asynchronous Method: * `wait` Pause until message is triggered. You can also `await` the message as - per the above example. + per the examples. + +The following example shows multiple tasks awaiting a `Message`. +```python +from primitives.message import Message +import uasyncio as asyncio + +async def bar(msg, n): + while True: + res = await msg + msg.clear() + print(n, res) + # Pause until other coros waiting on msg have run and before again + # awaiting a message. + await asyncio.sleep_ms(0) + +async def main(): + msg = Message() + for n in range(5): + asyncio.create_task(bar(msg, n)) + k = 0 + while True: + k += 1 + await asyncio.sleep_ms(1000) + msg.set('Hello {}'.format(k)) + +asyncio.run(main()) +``` ## 3.10 Synchronising to hardware diff --git a/v3/primitives/message.py b/v3/primitives/message.py index fc24bb7..ffd6d00 100644 --- a/v3/primitives/message.py +++ b/v3/primitives/message.py @@ -1,70 +1,57 @@ # message.py +# Now uses ThreadSafeFlag for efficiency -# Copyright (c) 2018-2020 Peter Hinch +# Copyright (c) 2018-2021 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file +# Usage: +# from primitives.message import Message + try: import uasyncio as asyncio except ImportError: import asyncio -# Usage: -# from primitives.message import Message # A coro waiting on a message issues await message -# A coro rasing the message issues message.set(payload) -# When all waiting coros have run -# message.clear() should be issued - -# This more efficient version is commented out because Event.set is not ISR -# friendly. TODO If it gets fixed, reinstate this (tested) version and update -# tutorial for 1:n operation. -#class Message(asyncio.Event): - #def __init__(self, _=0): - #self._data = None - #super().__init__() - - #def clear(self): - #self._data = None - #super().clear() - - #def __await__(self): - #await super().wait() - - #__iter__ = __await__ - - #def set(self, data=None): - #self._data = data - #super().set() - - #def value(self): - #return self._data - -# Has an ISR-friendly .set() -class Message(): - def __init__(self, delay_ms=0): - self.delay_ms = delay_ms - self.clear() - - def clear(self): - self._flag = False - self._data = None - - async def wait(self): # CPython comptaibility - while not self._flag: - await asyncio.sleep_ms(self.delay_ms) - - def __await__(self): - while not self._flag: - await asyncio.sleep_ms(self.delay_ms) +# A coro or hard/soft ISR raising the message issues.set(payload) +# .clear() should be issued by at least one waiting task and before +# next event. + +class Message(asyncio.ThreadSafeFlag): + def __init__(self, _=0): # Arg: poll interval. Compatibility with old code. + self._evt = asyncio.Event() + self._data = None # Message + self._state = False # Ensure only one task waits on ThreadSafeFlag + self._is_set = False # For .is_set() + super().__init__() + + def clear(self): # At least one task must call clear when scheduled + self._state = False + self._is_set = False + + def __iter__(self): + yield from self.wait() + return self._data + + async def wait(self): + if self._state: # A task waits on ThreadSafeFlag + await self._evt.wait() # Wait on event + else: # First task to wait + self._state = True + # Ensure other tasks see updated ._state before they wait + await asyncio.sleep_ms(0) + await super().wait() # Wait on ThreadSafeFlag + self._evt.set() + self._evt.clear() + return self._data - __iter__ = __await__ + def set(self, data=None): # Can be called from a hard ISR + self._data = data + self._is_set = True + super().set() def is_set(self): - return self._flag - - def set(self, data=None): - self._flag = True - self._data = data + return self._is_set def value(self): return self._data From ffeb61c055c495b20bd31a1559148cfcef65e212 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 19 Feb 2021 17:23:15 +0000 Subject: [PATCH 062/305] Tutorial: improve section on ThreadSafeFlag. --- v3/docs/TUTORIAL.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index a13c2c6..a391310 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -895,12 +895,9 @@ following key differences: * It is self-clearing. * Only one task may wait on the flag. -The latter limitation may be addressed by having a task wait on a -`ThreadSafeFlag` before setting an `Event`. Multiple tasks may wait on that -`Event`. - Synchronous method: * `set` Triggers the flag. Like issuing `set` then `clear` to an `Event`. + Asynchronous method: * `wait` Wait for the flag to be set. If the flag is already set then it returns immediately. @@ -925,7 +922,7 @@ tim = Timer(1, freq=1, callback=cb) asyncio.run(foo()) ``` -Another example (posted by [Damien](https://github.com/micropython/micropython/pull/6886#issuecomment-779863757)): +Another example ([posted by Damien](https://github.com/micropython/micropython/pull/6886#issuecomment-779863757)): ```python class AsyncPin: def __init__(self, pin, trigger): @@ -939,16 +936,23 @@ class AsyncPin: You then call `await async_pin.wait_edge()`. The current implementation provides no performance benefits against polling the -hardware. The `ThreadSafeFlag` uses the I/O mechanism. There are plans to -reduce the latency such that I/O is polled every time the scheduler acquires -control. This would provide the highest possible level of performance as -discussed in +hardware: other pending tasks may be granted execution first in round-robin +fashion. However the `ThreadSafeFlag` uses the I/O mechanism. There is a plan +to provide a means to reduce the latency such that selected I/O devices are +polled every time the scheduler acquires control. This will provide the highest +possible level of performance as discussed in [Polling vs Interrupts](./TUTORIAL.md#9-polling-vs-interrupts). Regardless of performance issues, a key use for `ThreadSafeFlag` is where a hardware device requires the use of an ISR for a μs level response. Having -serviced the device, it then flags an asynchronous routine, for example to -process data received. +serviced the device, the ISR flags an asynchronous routine, say to process +received data. + +The fact that only one task may wait on a `ThreadSafeFlag` may be addressed by +having the task that waits on the `ThreadSafeFlag` set an `Event`. Multiple +tasks may wait on that `Event`. As an alternative to explicitly coding this, +the [Message class](./TUTORIAL.md#39-message) uses this approach to provide an +`Event`-like object which can be triggered from an ISR. ###### [Contents](./TUTORIAL.md#contents) From 8253a5aef7a96605030dcb125ad1f0cc91788553 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 19 Feb 2021 17:31:05 +0000 Subject: [PATCH 063/305] Tutorial: fix doc formatting. --- v3/docs/TUTORIAL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index a391310..77171a5 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1179,6 +1179,7 @@ tested in slow time by the task. Constructor: * No args. + Synchronous methods: * `set(data=None)` Trigger the message with optional payload. * `is_set()` Returns `True` if the `Message` is set, `False` if `.clear()` has @@ -1186,6 +1187,7 @@ Synchronous methods: * `clear()` Clears the triggered status. At least one task waiting on the message should issue `clear()`. * `value()` Return the payload. + Asynchronous Method: * `wait` Pause until message is triggered. You can also `await` the message as per the examples. From 33b57691e6c83c834f9c7cf48e016668f911616f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 19 Feb 2021 17:37:49 +0000 Subject: [PATCH 064/305] Tutorial: fix doc formatting. --- v3/docs/TUTORIAL.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 77171a5..2dffa1a 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1181,16 +1181,17 @@ Constructor: * No args. Synchronous methods: - * `set(data=None)` Trigger the message with optional payload. + * `set(data=None)` Trigger the `Message` with optional payload (may be any + Python object). * `is_set()` Returns `True` if the `Message` is set, `False` if `.clear()` has - beein issued. + been issued. * `clear()` Clears the triggered status. At least one task waiting on the message should issue `clear()`. * `value()` Return the payload. Asynchronous Method: - * `wait` Pause until message is triggered. You can also `await` the message as - per the examples. + * `wait()` Pause until message is triggered. You can also `await` the message + as per the examples. The following example shows multiple tasks awaiting a `Message`. ```python From 29427902dc8752ff5c9e11943719e0cd64ecbd5f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 19 Feb 2021 17:42:17 +0000 Subject: [PATCH 065/305] Tutorial: improve Message wording. --- v3/docs/TUTORIAL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 2dffa1a..5bb5b7f 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1174,8 +1174,8 @@ async def main(): asyncio.run(main()) ``` A `Message` can provide a means of communication between an interrupt handler -and a task. The handler services the hardware and issues `.set()` which is -tested in slow time by the task. +and a task. The handler services the hardware and issues `.set()` which causes +the waiting task to resume (in relatively slow time). Constructor: * No args. From 7a292283089650514e19a5425f77af216c4f3d06 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 22 Feb 2021 06:34:26 +0000 Subject: [PATCH 066/305] Tutorial: add note re availability of ThreadSafeFlag. --- v3/docs/TUTORIAL.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 5bb5b7f..753c27d 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -954,6 +954,11 @@ tasks may wait on that `Event`. As an alternative to explicitly coding this, the [Message class](./TUTORIAL.md#39-message) uses this approach to provide an `Event`-like object which can be triggered from an ISR. +#### Note + +ThreadSafeFlag is only available in nightly builds. It will be available in +release builds starting with V1.15. + ###### [Contents](./TUTORIAL.md#contents) ## 3.7 Barrier @@ -1220,6 +1225,11 @@ async def main(): asyncio.run(main()) ``` +#### ThreadSafeFlag dependency + +ThreadSafeFlag is only available in nightly builds. It will be available in +release builds starting with V1.15. + ## 3.10 Synchronising to hardware The following hardware-related classes are documented [here](./DRIVERS.md): From ef931a39559cc96ba2cc08fe630ab197b3a20ea0 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 1 Mar 2021 11:03:38 +0000 Subject: [PATCH 067/305] DRIVERS.md: remove outdated refs to IRQ_EVENT. --- v3/docs/DRIVERS.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 6c6ae4d..4d62593 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -40,7 +40,6 @@ Drivers are imported with: from primitives.switch import Switch from primitives.pushbutton import Pushbutton from primitives.aadc import AADC -from primitives.irq_event import IRQ_EVENT ``` There is a test/demo program for the Switch and Pushbutton classes. On import this lists available tests. It assumes a Pyboard with a switch or pushbutton @@ -55,12 +54,6 @@ is run as follows: from primitives.tests.adctest import test test() ``` -The test for the `IRQ_EVENT` class requires a Pyboard with pins X1 and X2 -linked. It is run as follows: -```python -from primitives.tests.irq_event_test import test -test() -``` ###### [Contents](./DRIVERS.md#1-contents) From 34c7d0c66a5f80ef79df0cea8eca13973b60810e Mon Sep 17 00:00:00 2001 From: John Maximilian <2e0byo@gmail.com> Date: Sat, 27 Mar 2021 20:19:11 +0000 Subject: [PATCH 068/305] make long/double fn settable --- v3/primitives/pushbutton.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index abe438c..f1eaa1b 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -41,10 +41,18 @@ def release_func(self, func, args=()): def double_func(self, func, args=()): self._df = func self._da = args + if self._df: + self._dd = Delay_ms(self._ddto) + else: + self._dd = False def long_func(self, func, args=()): self._lf = func self._la = args + if self._lf: # Instantiate timers if funcs exist + self._ld = Delay_ms(self._lf, self._la) + else: + self._lf = False # Current non-debounced logical button state: True == pressed def rawstate(self): @@ -61,10 +69,6 @@ def _ddto(self): # Doubleclick timeout: no doubleclick occurred launch(self._ff, self._fa) async def buttoncheck(self): - if self._lf: # Instantiate timers if funcs exist - self._ld = Delay_ms(self._lf, self._la) - if self._df: - self._dd = Delay_ms(self._ddto) while True: state = self.rawstate() # State has changed: act on it now. From 4f2aac0bb3ef497ab1a5d95e27e32270ea894606 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 29 Mar 2021 08:34:10 +0100 Subject: [PATCH 069/305] TUTORIAL.md Clarify ThreadSafeFlag example. --- v3/docs/TUTORIAL.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 753c27d..3944c2c 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -922,18 +922,33 @@ tim = Timer(1, freq=1, callback=cb) asyncio.run(foo()) ``` -Another example ([posted by Damien](https://github.com/micropython/micropython/pull/6886#issuecomment-779863757)): +An example [based on one posted by Damien](https://github.com/micropython/micropython/pull/6886#issuecomment-779863757) +Link pins X1 and X2 to test. ```python +from machine import Pin, Timer +import uasyncio as asyncio + class AsyncPin: def __init__(self, pin, trigger): self.pin = pin - self.flag = ThreadSafeFlag() + self.flag = asyncio.ThreadSafeFlag() self.pin.irq(lambda pin: self.flag.set(), trigger, hard=True) - def wait_edge(self): - return self.flag.wait() + async def wait_edge(self): + await self.flag.wait() + +async def foo(): + pin_in = Pin('X1', Pin.IN) + async_pin = AsyncPin(pin_in, Pin.IRQ_RISING) + pin_out = Pin('X2', Pin.OUT) # Toggle pin to test + t = Timer(-1, period=500, callback=lambda _: pin_out(not pin_out())) + await asyncio.sleep(0) + while True: + await async_pin.wait_edge() + print('Got edge.') + +asyncio.run(foo()) ``` -You then call `await async_pin.wait_edge()`. The current implementation provides no performance benefits against polling the hardware: other pending tasks may be granted execution first in round-robin From 07eaa3548e242b13da83c5d7a269a95229fa6882 Mon Sep 17 00:00:00 2001 From: John Maximilian <2e0byo@gmail.com> Date: Tue, 30 Mar 2021 23:05:50 +0100 Subject: [PATCH 070/305] 2nd shot --- v3/primitives/pushbutton.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index f1eaa1b..008e217 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -38,21 +38,19 @@ def release_func(self, func, args=()): self._ff = func self._fa = args - def double_func(self, func, args=()): + def double_func(self, func=False, args=()): self._df = func self._da = args - if self._df: - self._dd = Delay_ms(self._ddto) - else: - self._dd = False + if self._dd: + self._dd.stop() + self._dd = Delay_ms(self._ddto) if func else False - def long_func(self, func, args=()): + def long_func(self, func=False, args=()): self._lf = func self._la = args - if self._lf: # Instantiate timers if funcs exist - self._ld = Delay_ms(self._lf, self._la) - else: - self._lf = False + if self._ld: + self._ld.stop() + self._ld = Delay_ms(self._lf, self._la) if func else False # Current non-debounced logical button state: True == pressed def rawstate(self): From bdc5eeb7e794f61b23383c569268f04f4af63010 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 31 Mar 2021 11:36:00 +0100 Subject: [PATCH 071/305] Improvements to delay_ms.py --- v3/docs/TUTORIAL.md | 28 +++++++++++ v3/primitives/delay_ms.py | 98 +++++++++++++++++++-------------------- 2 files changed, 76 insertions(+), 50 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 3944c2c..331231d 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1123,6 +1123,8 @@ Methods: 5. `rvalue` No argument. If a timeout has occurred and a callback has run, returns the return value of the callback. If a coroutine was passed, returns the `Task` instance. This allows the `Task` to be cancelled or awaited. + 6. `wait` One or more tasks may wait on a `Delay_ms` instance. Execution will + proceed when the instance has timed out. In this example a `Delay_ms` instance is created with the default duration of 1s. It is repeatedly triggered for 5 secs, preventing the callback from @@ -1150,6 +1152,32 @@ try: finally: asyncio.new_event_loop() # Clear retained state ``` +This example illustrates multiple tasks waiting on a `Delay_ms`. No callback is +used. +```python +import uasyncio as asyncio +from primitives.delay_ms import Delay_ms + +async def foo(n, d): + await d.wait() + print('Done in foo no.', n) + +async def my_app(): + d = Delay_ms() + for n in range(4): + asyncio.create_task(foo(n, d)) + d.trigger(3000) + print('Waiting on d') + await d.wait() + print('Done in my_app.') + await asyncio.sleep(1) + print('Test complete.') + +try: + asyncio.run(my_app()) +finally: + _ = asyncio.new_event_loop() # Clear retained state +``` ## 3.9 Message diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py index 7424335..66094db 100644 --- a/v3/primitives/delay_ms.py +++ b/v3/primitives/delay_ms.py @@ -1,69 +1,67 @@ -# delay_ms.py +# delay_ms.py Now uses ThreadSafeFlag and has extra .wait() API +# Usage: +# from primitives.delay_ms import Delay_ms -# Copyright (c) 2018-2020 Peter Hinch +# Copyright (c) 2018-2021 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -# Rewritten for uasyncio V3. Allows stop time to be brought forwards. import uasyncio as asyncio from utime import ticks_add, ticks_diff, ticks_ms -from micropython import schedule from . import launch -# Usage: -# from primitives.delay_ms import Delay_ms class Delay_ms: - verbose = False # verbose and can_alloc retained to avoid breaking code. - def __init__(self, func=None, args=(), can_alloc=True, duration=1000): + + class DummyTimer: # Stand-in for the timer class. Can be cancelled. + def cancel(self): + pass + _fake = DummyTimer() + + def __init__(self, func=None, args=(), duration=1000): self._func = func self._args = args - self._duration = duration # Default duration - self._tstop = None # Stop time (ms). None signifies not running. - self._tsave = None # Temporary storage for stop time - self._ktask = None # timer task - self._retrn = None # Return value of launched callable - self._do_trig = self._trig # Avoid allocation in .trigger + self._durn = duration # Default duration + self._retn = None # Return value of launched callable + self._tend = None # Stop time (absolute ms). + self._busy = False + self._trig = asyncio.ThreadSafeFlag() + self._tout = asyncio.Event() # Timeout event + self.wait = self._tout.wait # Allow: await wait_ms.wait() + self._ttask = self._fake # Timer task + asyncio.create_task(self._run()) - def stop(self): - if self._ktask is not None: - self._ktask.cancel() + async def _run(self): + while True: + await self._trig.wait() # Await a trigger + self._ttask.cancel() # Cancel and replace + await asyncio.sleep_ms(0) + dt = max(ticks_diff(self._tend, ticks_ms()), 0) # Beware already elapsed. + self._ttask = asyncio.create_task(self._timer(dt)) - def trigger(self, duration=0): # Update end time - now = ticks_ms() - if duration <= 0: # Use default set by constructor - duration = self._duration - self._retrn = None - is_running = self() - tstop = self._tstop # Current stop time - # Retriggering normally just updates ._tstop for ._timer - self._tstop = ticks_add(now, duration) - # Identify special case where we are bringing the end time forward - can = is_running and duration < ticks_diff(tstop, now) - if not is_running or can: - schedule(self._do_trig, can) + async def _timer(self, dt): + await asyncio.sleep_ms(dt) + self._tout.set() # Only gets here if not cancelled. + self._tout.clear() + self._busy = False + if self._func is not None: + self._retn = launch(self._func, self._args) - def _trig(self, can): - if can: - self._ktask.cancel() - self._ktask = asyncio.create_task(self._timer(can)) +# API + # trigger may be called from hard ISR. + def trigger(self, duration=0): # Update absolute end time, 0-> ctor default + self._tend = ticks_add(ticks_ms(), duration if duration > 0 else self._durn) + self._retn = None # Default in case cancelled. + self._busy = True + self._trig.set() + + def stop(self): + self._ttask.cancel() + self._ttask = self._fake + self._busy = False def __call__(self): # Current running status - return self._tstop is not None + return self._busy running = __call__ def rvalue(self): - return self._retrn - - async def _timer(self, restart): - if restart: # Restore cached end time - self._tstop = self._tsave - try: - twait = ticks_diff(self._tstop, ticks_ms()) - while twait > 0: # Must loop here: might be retriggered - await asyncio.sleep_ms(twait) - twait = ticks_diff(self._tstop, ticks_ms()) - if self._func is not None: # Timed out: execute callback - self._retrn = launch(self._func, self._args) - finally: - self._tsave = self._tstop # Save in case we restart. - self._tstop = None # timer is stopped + return self._retn From 0c86db0582b0aec096b5fd7cc2ac61c75d18965a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 1 Apr 2021 18:35:20 +0100 Subject: [PATCH 072/305] Switch, Pushbutton and Delay_ms: can change callbacks at runtime. --- v3/docs/DRIVERS.md | 30 ++++++++++++++------------ v3/docs/TUTORIAL.md | 2 ++ v3/primitives/delay_ms.py | 4 ++++ v3/primitives/pushbutton.py | 29 ++++++++++++++----------- v3/primitives/tests/switches.py | 38 +++++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 27 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 4d62593..79d7fb6 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -32,7 +32,8 @@ goes outside defined bounds. # 2. Installation and usage -The drivers are in the primitives package. To install copy the `primitives` +The drivers require a daily build of firmware or a release build >=1.15. The +drivers are in the primitives package. To install copy the `primitives` directory and its contents to the target hardware. Drivers are imported with: @@ -158,20 +159,21 @@ Constructor arguments: Methods: - 1. `press_func` Args: `func` (mandatory) a `callable` to run on button push. - `args` a tuple of arguments for the `callable` (default `()`). - 2. `release_func` Args: `func` (mandatory) a `callable` to run on button - release. `args` a tuple of arguments for the `callable` (default `()`). - 3. `long_func` Args: `func` (mandatory) a `callable` to run on long button - push. `args` a tuple of arguments for the `callable` (default `()`). - 4. `double_func` Args: `func` (mandatory) a `callable` to run on double - push. `args` a tuple of arguments for the `callable` (default `()`). + 1. `press_func` Args: `func=False` a `callable` to run on button push, + `args=()` a tuple of arguments for the `callable`. + 2. `release_func` Args: `func=False` a `callable` to run on button release, + `args=()` a tuple of arguments for the `callable`. + 3. `long_func` Args: `func=False` a `callable` to run on long button push, + `args=()` a tuple of arguments for the `callable`. + 4. `double_func` Args: `func=False` a `callable` to run on double push, + `args=()` a tuple of arguments for the `callable`. 5. `__call__` Call syntax e.g. `mybutton()` Returns the logical debounced state of the button (`True` corresponds to pressed). 6. `rawstate()` Returns the logical instantaneous state of the button. There is probably no reason to use this. -Methods 1 - 4 should be called before starting the scheduler. +Methods 1 - 4 may be called at any time. If `False` is passed for a callable, +any existing callback will be disabled. Class attributes: 1. `debounce_ms` Debounce time in ms. Default 50. @@ -188,12 +190,12 @@ def toggle(led): led.toggle() async def my_app(): + pin = Pin('X1', Pin.IN, Pin.PULL_UP) # Pushbutton to gnd + red = LED(1) + pb = Pushbutton(pin) + pb.press_func(toggle, (red,)) # Note how function and args are passed await asyncio.sleep(60) # Dummy -pin = Pin('X1', Pin.IN, Pin.PULL_UP) # Pushbutton to gnd -red = LED(1) -pb = Pushbutton(pin) -pb.press_func(toggle, (red,)) # Note how function and args are passed asyncio.run(my_app()) # Run main application code ``` diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 331231d..73bd87d 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1125,6 +1125,8 @@ Methods: the `Task` instance. This allows the `Task` to be cancelled or awaited. 6. `wait` One or more tasks may wait on a `Delay_ms` instance. Execution will proceed when the instance has timed out. + 7. `callback` args `func=None`, `args=()`. Allows the callable and its args to + be assigned, reassigned or disabled at run time. In this example a `Delay_ms` instance is created with the default duration of 1s. It is repeatedly triggered for 5 secs, preventing the callback from diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py index 66094db..6fd11fb 100644 --- a/v3/primitives/delay_ms.py +++ b/v3/primitives/delay_ms.py @@ -65,3 +65,7 @@ def __call__(self): # Current running status def rvalue(self): return self._retn + + def callback(self, func=None, args=()): + self._func = func + self._args = args diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index 008e217..1e2a616 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -1,6 +1,6 @@ # pushbutton.py -# Copyright (c) 2018-2020 Peter Hinch +# Copyright (c) 2018-2021 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file import uasyncio as asyncio @@ -23,34 +23,37 @@ def __init__(self, pin, suppress=False, sense=None): self._tf = False self._ff = False self._df = False - self._lf = False self._ld = False # Delay_ms instance for long press self._dd = False # Ditto for doubleclick self.sense = pin.value() if sense is None else sense # Convert from electrical to logical value self.state = self.rawstate() # Initial state asyncio.create_task(self.buttoncheck()) # Thread runs forever - def press_func(self, func, args=()): + def press_func(self, func=False, args=()): self._tf = func self._ta = args - def release_func(self, func, args=()): + def release_func(self, func=False, args=()): self._ff = func self._fa = args def double_func(self, func=False, args=()): self._df = func self._da = args - if self._dd: - self._dd.stop() - self._dd = Delay_ms(self._ddto) if func else False + if func: # If double timer already in place, leave it + if not self._dd: + self._dd = Delay_ms(self._ddto) + else: + self._dd = False # Clearing down double func def long_func(self, func=False, args=()): - self._lf = func - self._la = args - if self._ld: - self._ld.stop() - self._ld = Delay_ms(self._lf, self._la) if func else False + if func: + if self._ld: + self._ld.callback(func, args) + else: + self._ld = Delay_ms(func, args) + else: + self._ld = False # Current non-debounced logical button state: True == pressed def rawstate(self): @@ -75,7 +78,7 @@ async def buttoncheck(self): if state: # Button pressed: launch pressed func if self._tf: launch(self._tf, self._ta) - if self._lf: # There's a long func: start long press delay + if self._ld: # There's a long func: start long press delay self._ld.trigger(Pushbutton.long_press_ms) if self._df: if self._dd(): # Second click: timer running diff --git a/v3/primitives/tests/switches.py b/v3/primitives/tests/switches.py index 026ca30..c55711a 100644 --- a/v3/primitives/tests/switches.py +++ b/v3/primitives/tests/switches.py @@ -26,6 +26,7 @@ test_swcb Switch with callback test_btn Pushutton launching coros test_btncb Pushbutton launching callbacks +btn_dynamic Change coros launched at runtime. ''' print(tests) @@ -141,3 +142,40 @@ def test_btncb(): pb.double_func(toggle, (yellow,)) pb.long_func(toggle, (blue,)) run() + +# Test for the Pushbutton class where callback coros change dynamically +def setup(pb, press, release, dbl, lng, t=1000): + s = ''' +Functions are changed: +LED's pulse for 2 seconds +press pulses blue +release pulses red +double click pulses green +long pulses yellow +''' + pb.press_func(pulse, (press, t)) + pb.release_func(pulse, (release, t)) + pb.double_func(pulse, (dbl, t)) + if lng is not None: + pb.long_func(pulse, (lng, t)) + print(s) + +def btn_dynamic(): + s = ''' +press pulses red +release pulses green +double click pulses yellow +long press changes button functions. +''' + print('Test of pushbutton scheduling coroutines.') + print(helptext) + print(s) + pin = Pin('X1', Pin.IN, Pin.PULL_UP) + red = LED(1) + green = LED(2) + yellow = LED(3) + blue = LED(4) + pb = Pushbutton(pin) + setup(pb, red, green, yellow, None) + pb.long_func(setup, (pb, blue, red, green, yellow, 2000)) + run() From 6e93221a85934fa0638bbaa5c8307fbeb777041f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 6 Apr 2021 08:00:23 +0100 Subject: [PATCH 073/305] TUTORIAL.md Clarify consequence of Event.clear. --- v3/docs/TUTORIAL.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 73bd87d..28eeebd 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -669,7 +669,12 @@ be queued for execution. Note that the synchronous sequence event.set() event.clear() ``` -will cause any tasks waiting on the event to resume in round-robin order. +will cause any tasks waiting on the event to resume in round-robin order. In +general the waiting task should clear the event, as in the `waiter` example +above. This caters for the case where the waiting task has not reached the +event at the time when it is triggered. In this instance, by the time the task +reaches the event, the task will find it clear and will pause. This can lead to +non-deterministic behaviour if timing is marginal. The `Event` class is an efficient and effective way to synchronise tasks, but firmware applications often have multiple tasks running `while True:` loops. From 088d3900ea306b0a496cdfff01c5352b2494dc4b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 22 Apr 2021 09:34:52 +0100 Subject: [PATCH 074/305] TUTORIAL.md Adjust firmware dependency notes after V1.15 realease. --- v3/docs/TUTORIAL.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 28eeebd..3149ead 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -892,6 +892,8 @@ asyncio.run(queue_go(4)) ## 3.6 ThreadSafeFlag +This requires firmware V1.15 or later. + This official class provides an efficient means of synchronising a task with a truly asynchronous event such as a hardware interrupt service routine or code running in another thread. It operates in a similar way to `Event` with the @@ -974,11 +976,6 @@ tasks may wait on that `Event`. As an alternative to explicitly coding this, the [Message class](./TUTORIAL.md#39-message) uses this approach to provide an `Event`-like object which can be triggered from an ISR. -#### Note - -ThreadSafeFlag is only available in nightly builds. It will be available in -release builds starting with V1.15. - ###### [Contents](./TUTORIAL.md#contents) ## 3.7 Barrier @@ -1188,6 +1185,8 @@ finally: ## 3.9 Message +This requires firmware V1.15 or later. + This is an unofficial primitive with no counterpart in CPython asyncio. It uses [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) to provide an object similar to `Event` but capable of being set in a hard ISR context. It extends @@ -1275,11 +1274,6 @@ async def main(): asyncio.run(main()) ``` -#### ThreadSafeFlag dependency - -ThreadSafeFlag is only available in nightly builds. It will be available in -release builds starting with V1.15. - ## 3.10 Synchronising to hardware The following hardware-related classes are documented [here](./DRIVERS.md): From ef8be12dc9d31c21c6b74c07359797f8c49afffd Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 25 Jun 2021 10:55:19 +0100 Subject: [PATCH 075/305] TUTORIAL.md Add note to retaining state section 7.2 --- v3/docs/TUTORIAL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 3149ead..4b154d4 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2222,7 +2222,6 @@ scheduler is running. If a `uasyncio` application terminates, state is retained. Embedded code seldom terminates, but in testing it is useful to re-run a script without the need for a soft reset. This may be done as follows: - ```python import uasyncio as asyncio @@ -2237,6 +2236,8 @@ def test(): finally: asyncio.new_event_loop() # Clear retained state ``` +It should be noted that clearing retained state is not a panacea. Re-running +complex applications may require state to be retained. ###### [Contents](./TUTORIAL.md#contents) From b7da62f71ea41c197bf419c0653d0013f0c2794f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 30 Jun 2021 10:24:07 +0100 Subject: [PATCH 076/305] Add primitives/encoder. --- v3/docs/DRIVERS.md | 76 ++++++++++++++++++++++++----- v3/primitives/encoder.py | 49 +++++++++++++++++++ v3/primitives/tests/encoder_test.py | 31 ++++++++++++ 3 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 v3/primitives/encoder.py create mode 100644 v3/primitives/tests/encoder_test.py diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 79d7fb6..c85435a 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -1,12 +1,15 @@ # 0. Introduction -Drivers for switches and pushbuttons are provided, plus a retriggerable delay -class. The switch and button drivers support debouncing. The switch driver -provides for running a callback or launching a coroutine (coro) on contact -closure and/or opening. +Drivers for switches and pushbuttons are provided. Switch and button drivers +support debouncing. The switch driver provides for running a callback or +launching a coroutine (coro) on contact closure and/or opening. The pushbutton +driver extends this to support long-press and double-click events. -The pushbutton driver extends this to support long-press and double-click -events. +An `Encoder` class is provided to support rotary control knobs based on +quadrature encoder switches. This is not intended for high throughput encoders +as used in CNC machines where +[an interrupt based solution](https://github.com/peterhinch/micropython-samples#47-rotary-incremental-encoder) +is required. The asynchronous ADC supports pausing a task until the value read from an ADC goes outside defined bounds. @@ -24,9 +27,11 @@ goes outside defined bounds. 5. [ADC monitoring](./DRIVERS.md#5-adc-monitoring) Pause until an ADC goes out of bounds 5.1 [AADC class](./DRIVERS.md#51-aadc-class) 5.2 [Design note](./DRIVERS.md#52-design-note) - 6. [Additional functions](./DRIVERS.md#6-additional-functions) - 6.1 [launch](./DRIVERS.md#61-launch) Run a coro or callback interchangeably - 6.2 [set_global_exception](./DRIVERS.md#62-set_global_exception) Simplify debugging with a global exception handler + 6. [Quadrature encoders](./DRIVERS.md#6-quadrature-encoders) + 6.1 [Encoder class](./DRIVERS.md#61-encoder-class) + 7. [Additional functions](./DRIVERS.md#7-additional-functions) + 7.1 [launch](./DRIVERS.md#71-launch) Run a coro or callback interchangeably + 7.2 [set_global_exception](./DRIVERS.md#72-set_global_exception) Simplify debugging with a global exception handler ###### [Tutorial](./TUTORIAL.md#contents) @@ -333,9 +338,56 @@ this for applications requiring rapid response. ###### [Contents](./DRIVERS.md#1-contents) -# 6. Additional functions +# 6. Quadrature encoders + +The `Encoder` class is an asynchronous driver for control knobs based on +quadrature encoder switches such as +[this Adafruit product](https://www.adafruit.com/product/377). This is not +intended for high throughput encoders such as those used in CNC machines where +[an interrupt based solution](https://github.com/peterhinch/micropython-samples#47-rotary-incremental-encoder) +is required. This is because the driver works by polling the switches. The +latency between successive readings of the switch state will depend on the +behaviour of other tasks in the application, but if changes occur rapidly it is +likely that transitions will be missed. + +In the context of a rotary dial this is usually not a problem, firstly because +changes occur at a relatively low rate and secondly because there is usually +some form of feedback to the user. A single missed increment on a CNC machine +is a fail. In a user interface it usually is not. + +The API uses a callback which occurs whenever the value changes. Alternatively +the `Encoder` may be queried to retrieve the current position. + +A high throughput solution can be used with rotary dials but there is a +difference in the way contact bounce (or vibration induced jitter) are handled. +The high throughput solution results in +-1 count jitter with the callback +repeatedly occurring. This driver uses hysteresis to ensure that transitions +due to contact bounce are ignored. + +## 6.1 Encoder class + +Constructor arguments: + 1. `pin_x` Initialised `machine.Pin` instances for the switch. Should be set + as `Pin.IN` and have pullups. + 2. `pin_y` Ditto. + 3. `v=0` Initial value. + 4. `vmin=None` By default the `value` of the encoder can vary without limit. + Optionally maximum and/or minimum limits can be set. + 5. `vmax=None` + 6. `callback=lambda *_ : None` Optional callback function. The callback + receives two args, `v` being the encoder's current value and `fwd` being + `True` if the value has incremented of `False` if it decremented. Further args + may be appended by the following. + 7. `args=()` An optional tuple of args for the callback. + +Synchronous method: + * `value` No args. Returns an integer being the `Encoder` current value. -## 6.1 Launch +###### [Contents](./DRIVERS.md#1-contents) + +# 7. Additional functions + +## 7.1 Launch Import as follows: ```python @@ -347,7 +399,7 @@ runs it and returns the callback's return value. If a coro is passed, it is converted to a `task` and run asynchronously. The return value is the `task` instance. A usage example is in `primitives/switch.py`. -## 6.2 set_global_exception +## 7.2 set_global_exception Import as follows: ```python diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py new file mode 100644 index 0000000..dddc303 --- /dev/null +++ b/v3/primitives/encoder.py @@ -0,0 +1,49 @@ +# encoder.py Asynchronous driver for incremental quadrature encoder. + +# Copyright (c) 2021 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +# This driver is intended for encoder-based control knobs. It is not +# suitable for NC machine applications. Please see the docs. + +import uasyncio as asyncio + +class Encoder: + def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, + callback=lambda a, b : None, args=()): + self._v = v + asyncio.create_task(self._run(pin_x, pin_y, vmin, vmax, + callback, args)) + + def _run(self, pin_x, pin_y, vmin, vmax, callback, args): + xp = pin_x() # Prior levels + yp = pin_y() + pf = None # Prior direction + while True: + await asyncio.sleep_ms(0) + x = pin_x() # Current levels + y = pin_y() + if xp == x: + if yp == y: + continue # No change, nothing to do + fwd = x ^ y ^ 1 # y changed + else: + fwd = x ^ y # x changed + pv = self._v # Cache prior value + nv = pv + (1 if fwd else -1) # New value + if vmin is not None: + nv = max(vmin, nv) + if vmax is not None: + nv = min(vmax, nv) + if nv != pv: # Change + rev = (pf is not None) and (pf != fwd) + if not rev: + callback(nv, fwd, *args) + self._v = nv + + pf = fwd # Update prior state + xp = x + yp = y + + def value(self): + return self._v diff --git a/v3/primitives/tests/encoder_test.py b/v3/primitives/tests/encoder_test.py new file mode 100644 index 0000000..e5aa7c2 --- /dev/null +++ b/v3/primitives/tests/encoder_test.py @@ -0,0 +1,31 @@ +# encoder_test.py Test for asynchronous driver for incremental quadrature encoder. + +# Copyright (c) 2021 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +from machine import Pin +import uasyncio as asyncio +from primitives.encoder import Encoder + + +px = Pin(33, Pin.IN) +py = Pin(25, Pin.IN) + +def cb(pos, fwd): + print(pos, fwd) + +async def main(): + while True: + await asyncio.sleep(1) + +def test(): + print('Running encoder test. Press ctrl-c to teminate.') + enc = Encoder(px, py, v=0, vmin=0, vmax=100, callback=cb) + try: + asyncio.run(main()) + except KeyboardInterrupt: + print('Interrupted') + finally: + asyncio.new_event_loop() + +test() From 8c273d6634f3d099ddcfb7ea8e42b5450fa85160 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 30 Jun 2021 10:36:37 +0100 Subject: [PATCH 077/305] TUTORIAL: add ref to Encoder class. --- v3/docs/TUTORIAL.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 4b154d4..245b658 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -36,7 +36,7 @@ REPL. 3.8 [Delay_ms](./TUTORIAL.md#38-delay_ms-class) Software retriggerable delay. 3.9 [Message](./TUTORIAL.md#39-message) 3.10 [Synchronising to hardware](./TUTORIAL.md#310-synchronising-to-hardware) - Debouncing switches and pushbuttons. Taming ADC's. Interfacing interrupts. + Debouncing switches, pushbuttons and encoder knobs. Taming ADC's. 4. [Designing classes for asyncio](./TUTORIAL.md#4-designing-classes-for-asyncio) 4.1 [Awaitable classes](./TUTORIAL.md#41-awaitable-classes)      4.1.1 [Use in context managers](./TUTORIAL.md#411-use-in-context-managers) @@ -1280,6 +1280,8 @@ The following hardware-related classes are documented [here](./DRIVERS.md): * `Switch` A debounced switch which can trigger open and close user callbacks. * `Pushbutton` Debounced pushbutton with callbacks for pressed, released, long press or double-press. + * `Encoder` An asynchronous interface for control knobs with switch contacts + configured as a quadrature encoder. * `AADC` Asynchronous ADC. A task can pause until the value read from an ADC goes outside defined bounds. Bounds can be absolute or relative to the current value. From 3661ee977bd0252260dc84b2e8dcb698faf83a1a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 1 Jul 2021 18:53:03 +0100 Subject: [PATCH 078/305] primitives/encoder uses interrupts. --- v3/docs/DRIVERS.md | 44 ++++++++++--------- v3/primitives/encoder.py | 67 ++++++++++++++++------------- v3/primitives/tests/encoder_test.py | 4 +- 3 files changed, 63 insertions(+), 52 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index c85435a..f7420b6 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -340,29 +340,28 @@ this for applications requiring rapid response. # 6. Quadrature encoders +This is a work in progress. Changes may occur. + The `Encoder` class is an asynchronous driver for control knobs based on quadrature encoder switches such as -[this Adafruit product](https://www.adafruit.com/product/377). This is not -intended for high throughput encoders such as those used in CNC machines where -[an interrupt based solution](https://github.com/peterhinch/micropython-samples#47-rotary-incremental-encoder) -is required. This is because the driver works by polling the switches. The -latency between successive readings of the switch state will depend on the -behaviour of other tasks in the application, but if changes occur rapidly it is -likely that transitions will be missed. +[this Adafruit product](https://www.adafruit.com/product/377). The driver is +not intended for applications such as CNC machines where +[a solution such as this one](https://github.com/peterhinch/micropython-samples#47-rotary-incremental-encoder) +is required. Drivers for NC machines must never miss an edge. Contact bounce or +vibration induced jitter can cause transitions to occur at a high rate; these +must be tracked. -In the context of a rotary dial this is usually not a problem, firstly because -changes occur at a relatively low rate and secondly because there is usually -some form of feedback to the user. A single missed increment on a CNC machine -is a fail. In a user interface it usually is not. +This driver runs the user supplied callback in an `asyncio` context, so it runs +only when other tasks have yielded to the scheduler. This ensures that the +callback can run safely. The driver allows limits to be assigned to the control +so that a dial running from (say) 0 to 100 may be implemented. If limits are +used, encoder values no longer represent absolute angles. -The API uses a callback which occurs whenever the value changes. Alternatively -the `Encoder` may be queried to retrieve the current position. +The callback only runs if a change in position has occurred. -A high throughput solution can be used with rotary dials but there is a -difference in the way contact bounce (or vibration induced jitter) are handled. -The high throughput solution results in +-1 count jitter with the callback -repeatedly occurring. This driver uses hysteresis to ensure that transitions -due to contact bounce are ignored. +A consequence of the callback running in an `asyncio` context is that, by the +time it runs, the encoder's position may have changed by more than one +increment. ## 6.1 Encoder class @@ -375,14 +374,17 @@ Constructor arguments: Optionally maximum and/or minimum limits can be set. 5. `vmax=None` 6. `callback=lambda *_ : None` Optional callback function. The callback - receives two args, `v` being the encoder's current value and `fwd` being - `True` if the value has incremented of `False` if it decremented. Further args - may be appended by the following. + receives two args, `v` being the encoder's current value and `delta` being + the signed difference between the current value and the previous one. Further + args may be appended by the following. 7. `args=()` An optional tuple of args for the callback. Synchronous method: * `value` No args. Returns an integer being the `Encoder` current value. +Class variable: + * `LATENCY=50` This sets a minumum period (in ms) between callback runs. + ###### [Contents](./DRIVERS.md#1-contents) # 7. Additional functions diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index dddc303..7b8f100 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -7,43 +7,52 @@ # suitable for NC machine applications. Please see the docs. import uasyncio as asyncio +from machine import Pin class Encoder: + LATENCY = 50 + def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, callback=lambda a, b : None, args=()): + self._pin_x = pin_x + self._pin_y = pin_y self._v = v - asyncio.create_task(self._run(pin_x, pin_y, vmin, vmax, - callback, args)) + self._tsf = asyncio.ThreadSafeFlag() + try: + xirq = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._x_cb, hard=True) + yirq = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._y_cb, hard=True) + except TypeError: + xirq = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._x_cb) + yirq = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._y_cb) + asyncio.create_task(self._run(vmin, vmax, callback, args)) + + + # Hardware IRQ's + def _x_cb(self, pin): + fwd = pin() ^ self._pin_y() + self._v += 1 if fwd else -1 + self._tsf.set() - def _run(self, pin_x, pin_y, vmin, vmax, callback, args): - xp = pin_x() # Prior levels - yp = pin_y() - pf = None # Prior direction + def _y_cb(self, pin): + fwd = pin() ^ self._pin_x() ^ 1 + self._v += 1 if fwd else -1 + self._tsf.set() + + async def _run(self, vmin, vmax, cb, args): + pv = self._v # Prior value while True: - await asyncio.sleep_ms(0) - x = pin_x() # Current levels - y = pin_y() - if xp == x: - if yp == y: - continue # No change, nothing to do - fwd = x ^ y ^ 1 # y changed - else: - fwd = x ^ y # x changed - pv = self._v # Cache prior value - nv = pv + (1 if fwd else -1) # New value - if vmin is not None: - nv = max(vmin, nv) + await self._tsf.wait() + cv = self._v # Current value if vmax is not None: - nv = min(vmax, nv) - if nv != pv: # Change - rev = (pf is not None) and (pf != fwd) - if not rev: - callback(nv, fwd, *args) - self._v = nv - - pf = fwd # Update prior state - xp = x - yp = y + cv = min(cv, vmax) + if vmin is not None: + cv = max(cv, vmin) + self._v = cv + #print(cv, pv) + if cv != pv: + cb(cv, cv - pv, *args) # User CB in uasyncio context + pv = cv + await asyncio.sleep_ms(self.LATENCY) def value(self): return self._v diff --git a/v3/primitives/tests/encoder_test.py b/v3/primitives/tests/encoder_test.py index e5aa7c2..78a6ad6 100644 --- a/v3/primitives/tests/encoder_test.py +++ b/v3/primitives/tests/encoder_test.py @@ -11,8 +11,8 @@ px = Pin(33, Pin.IN) py = Pin(25, Pin.IN) -def cb(pos, fwd): - print(pos, fwd) +def cb(pos, delta): + print(pos, delta) async def main(): while True: From db211994a575be4948b3b1e6fd502e2bbf3d1cad Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 4 Jul 2021 12:07:45 +0100 Subject: [PATCH 079/305] Encoder primitive now has div arg. --- v3/docs/DRIVERS.md | 50 ++++++++++++++++++++++++++-------- v3/primitives/encoder.py | 58 +++++++++++++++++++++++++--------------- 2 files changed, 75 insertions(+), 33 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index f7420b6..1c5483f 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -353,15 +353,22 @@ must be tracked. This driver runs the user supplied callback in an `asyncio` context, so it runs only when other tasks have yielded to the scheduler. This ensures that the -callback can run safely. The driver allows limits to be assigned to the control -so that a dial running from (say) 0 to 100 may be implemented. If limits are -used, encoder values no longer represent absolute angles. +callback can run safely, even if it triggers complex application behaviour. -The callback only runs if a change in position has occurred. +The `Encoder` can be instantiated in such a way that its effective resolution +can be reduced. A virtual encoder with lower resolution can be useful in some +applications. -A consequence of the callback running in an `asyncio` context is that, by the -time it runs, the encoder's position may have changed by more than one -increment. +The driver allows limits to be assigned to the virtual encoder's value so that +a dial running from (say) 0 to 100 may be implemented. If limits arenused, +encoder values no longer represent absolute angles, as the user might continue +to rotate the dial when it is "stuck" at an endstop. + +The callback only runs if a change in position of the virtual encoder has +occurred. In consequence of the callback running in an `asyncio` context, by +the time it is scheduled, the encoder's position may have changed by more than +one increment. The callback receives two args, the absolute value of the +virtual encoder and the signed change since the previous callback run. ## 6.1 Encoder class @@ -372,18 +379,39 @@ Constructor arguments: 3. `v=0` Initial value. 4. `vmin=None` By default the `value` of the encoder can vary without limit. Optionally maximum and/or minimum limits can be set. - 5. `vmax=None` - 6. `callback=lambda *_ : None` Optional callback function. The callback + 5. `vmax=None` As above. If `vmin` and/or `vmax` are specified, a `ValueError` + will be thrown if the initial value `v` does not conform with the limits. + 6. `div=1` A value > 1 causes the motion rate of the encoder to be divided + down, to produce a virtual encoder with lower resolution. This was found usefl + in some applications with the Adafruit encoder. + 7. `callback=lambda a, b : None` Optional callback function. The callback receives two args, `v` being the encoder's current value and `delta` being the signed difference between the current value and the previous one. Further args may be appended by the following. - 7. `args=()` An optional tuple of args for the callback. + 8. `args=()` An optional tuple of args for the callback. Synchronous method: * `value` No args. Returns an integer being the `Encoder` current value. Class variable: - * `LATENCY=50` This sets a minumum period (in ms) between callback runs. + * `delay=100` After motion is detected the driver waits for `delay` ms before + reading the current position. This was found useful with the Adafruit encoder + which has mechanical detents, which span multiple increments or decrements. A + delay gives time for motion to stop, enabling just one call to the callback. + +#### Note + +The driver works by maintaining an internal value `._v` which uses hardware +interrupts to track the absolute position of the physical encoder. In theory +this should be precise, but on ESP32 with the Adafruit encoder it is not. + +Currently under investigation: it may be a consequence of ESP32's use of soft +IRQ's. + +This is probably of little practical consequence as encoder knobs are usually +used in systems where there is user feedback. In a practical application +([ugui](https://github.com/peterhinch/micropython-micro-gui)) I can see no +evidence of the missed pulses. ###### [Contents](./DRIVERS.md#1-contents) diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index 7b8f100..8c206be 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -3,29 +3,32 @@ # Copyright (c) 2021 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -# This driver is intended for encoder-based control knobs. It is not -# suitable for NC machine applications. Please see the docs. +# This driver is intended for encoder-based control knobs. It is +# unsuitable for NC machine applications. Please see the docs. import uasyncio as asyncio from machine import Pin class Encoder: - LATENCY = 50 + delay = 100 # Pause (ms) for motion to stop - def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, + def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, div=1, callback=lambda a, b : None, args=()): self._pin_x = pin_x self._pin_y = pin_y - self._v = v + self._v = 0 # Hardware value always starts at 0 + self._cv = v # Current (divided) value + if ((vmin is not None) and v < min) or ((vmax is not None) and v > vmax): + raise ValueError('Incompatible args: must have vmin <= v <= vmax') self._tsf = asyncio.ThreadSafeFlag() + trig = Pin.IRQ_RISING | Pin.IRQ_FALLING try: - xirq = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._x_cb, hard=True) - yirq = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._y_cb, hard=True) - except TypeError: - xirq = pin_x.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._x_cb) - yirq = pin_y.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=self._y_cb) - asyncio.create_task(self._run(vmin, vmax, callback, args)) - + xirq = pin_x.irq(trigger=trig, handler=self._x_cb, hard=True) + yirq = pin_y.irq(trigger=trig, handler=self._y_cb, hard=True) + except TypeError: # hard arg is unsupported on some hosts + xirq = pin_x.irq(trigger=trig, handler=self._x_cb) + yirq = pin_y.irq(trigger=trig, handler=self._y_cb) + asyncio.create_task(self._run(vmin, vmax, div, callback, args)) # Hardware IRQ's def _x_cb(self, pin): @@ -38,21 +41,32 @@ def _y_cb(self, pin): self._v += 1 if fwd else -1 self._tsf.set() - async def _run(self, vmin, vmax, cb, args): - pv = self._v # Prior value + async def _run(self, vmin, vmax, div, cb, args): + pv = self._v # Prior hardware value + cv = self._cv # Current divided value as passed to callback + pcv = cv # Prior divided value passed to callback + mod = 0 + delay = self.delay while True: await self._tsf.wait() - cv = self._v # Current value + await asyncio.sleep_ms(delay) # Wait for motion to stop + new = self._v # Sample hardware (atomic read) + a = new - pv # Hardware change + # Ensure symmetrical bahaviour for + and - values + q, r = divmod(abs(a), div) + if a < 0: + r = -r + q = -q + pv = new - r # Hardware value when local value was updated + cv += q if vmax is not None: cv = min(cv, vmax) if vmin is not None: cv = max(cv, vmin) - self._v = cv - #print(cv, pv) - if cv != pv: - cb(cv, cv - pv, *args) # User CB in uasyncio context - pv = cv - await asyncio.sleep_ms(self.LATENCY) + self._cv = cv # For value() + if cv != pcv: + cb(cv, cv - pcv, *args) # User CB in uasyncio context + pcv = cv def value(self): - return self._v + return self._cv From c85001ff956f106c9ca51b16bc628a90bc048540 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 4 Jul 2021 12:13:59 +0100 Subject: [PATCH 080/305] Encoder primitive now has div arg. --- v3/docs/DRIVERS.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 1c5483f..00e9485 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -360,7 +360,7 @@ can be reduced. A virtual encoder with lower resolution can be useful in some applications. The driver allows limits to be assigned to the virtual encoder's value so that -a dial running from (say) 0 to 100 may be implemented. If limits arenused, +a dial running from (say) 0 to 100 may be implemented. If limits are used, encoder values no longer represent absolute angles, as the user might continue to rotate the dial when it is "stuck" at an endstop. @@ -388,7 +388,7 @@ Constructor arguments: receives two args, `v` being the encoder's current value and `delta` being the signed difference between the current value and the previous one. Further args may be appended by the following. - 8. `args=()` An optional tuple of args for the callback. + 8. `args=()` An optional tuple of positionl args for the callback. Synchronous method: * `value` No args. Returns an integer being the `Encoder` current value. @@ -397,20 +397,21 @@ Class variable: * `delay=100` After motion is detected the driver waits for `delay` ms before reading the current position. This was found useful with the Adafruit encoder which has mechanical detents, which span multiple increments or decrements. A - delay gives time for motion to stop, enabling just one call to the callback. + delay gives time for motion to stop enabling just one call to the callback. #### Note The driver works by maintaining an internal value `._v` which uses hardware interrupts to track the absolute position of the physical encoder. In theory -this should be precise, but on ESP32 with the Adafruit encoder it is not. +this should be precise, but on ESP32 with the Adafruit encoder it is not: +returning the dial to a given detent shows a small "drift" in position. Currently under investigation: it may be a consequence of ESP32's use of soft IRQ's. This is probably of little practical consequence as encoder knobs are usually used in systems where there is user feedback. In a practical application -([ugui](https://github.com/peterhinch/micropython-micro-gui)) I can see no +([micro-gui](https://github.com/peterhinch/micropython-micro-gui)) I can see no evidence of the missed pulses. ###### [Contents](./DRIVERS.md#1-contents) From 1341eac52a7fa9525413a46abbcbc6b6b3404947 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 5 Jul 2021 11:35:13 +0100 Subject: [PATCH 081/305] DRIVERS.md Improve Encoder description. --- v3/docs/DRIVERS.md | 59 ++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 00e9485..07acb84 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -340,8 +340,6 @@ this for applications requiring rapid response. # 6. Quadrature encoders -This is a work in progress. Changes may occur. - The `Encoder` class is an asynchronous driver for control knobs based on quadrature encoder switches such as [this Adafruit product](https://www.adafruit.com/product/377). The driver is @@ -349,11 +347,14 @@ not intended for applications such as CNC machines where [a solution such as this one](https://github.com/peterhinch/micropython-samples#47-rotary-incremental-encoder) is required. Drivers for NC machines must never miss an edge. Contact bounce or vibration induced jitter can cause transitions to occur at a high rate; these -must be tracked. +must be tracked. Consequently callbacks occur in an interrupt context with the +associated concurrency issues. -This driver runs the user supplied callback in an `asyncio` context, so it runs -only when other tasks have yielded to the scheduler. This ensures that the -callback can run safely, even if it triggers complex application behaviour. +This driver runs the user supplied callback in an `asyncio` context, so that +the callback runs only when other tasks have yielded to the scheduler. This +ensures that the callback runs with the same rules as apply to any `uasyncio` +task. This offers safety, even if the task triggers complex application +behaviour. The `Encoder` can be instantiated in such a way that its effective resolution can be reduced. A virtual encoder with lower resolution can be useful in some @@ -361,14 +362,15 @@ applications. The driver allows limits to be assigned to the virtual encoder's value so that a dial running from (say) 0 to 100 may be implemented. If limits are used, -encoder values no longer represent absolute angles, as the user might continue -to rotate the dial when it is "stuck" at an endstop. +encoder values no longer approximate absolute angles: the user might continue +to rotate the dial when its value is "stuck" at an endstop. The callback only runs if a change in position of the virtual encoder has occurred. In consequence of the callback running in an `asyncio` context, by the time it is scheduled, the encoder's position may have changed by more than one increment. The callback receives two args, the absolute value of the -virtual encoder and the signed change since the previous callback run. +virtual encoder at the time it was triggered and the signed change in this +value since the previous time the callback ran. ## 6.1 Encoder class @@ -385,34 +387,41 @@ Constructor arguments: down, to produce a virtual encoder with lower resolution. This was found usefl in some applications with the Adafruit encoder. 7. `callback=lambda a, b : None` Optional callback function. The callback - receives two args, `v` being the encoder's current value and `delta` being - the signed difference between the current value and the previous one. Further - args may be appended by the following. + receives two integer args, `v` being the virtual encoder's current value and + `delta` being the signed difference between the current value and the previous + one. Further args may be appended by the following. 8. `args=()` An optional tuple of positionl args for the callback. Synchronous method: - * `value` No args. Returns an integer being the `Encoder` current value. + * `value` No args. Returns an integer being the virtual encoder's current + value. Class variable: * `delay=100` After motion is detected the driver waits for `delay` ms before reading the current position. This was found useful with the Adafruit encoder which has mechanical detents, which span multiple increments or decrements. A - delay gives time for motion to stop enabling just one call to the callback. + delay gives time for motion to stop in the event of a single click movement. + If this occurs the delay ensures just one call to the callback. With no delay + a single click typically gives rise to two callbacks, the second of which can + come as a surprise in visual applications. -#### Note +#### Note on accuracy The driver works by maintaining an internal value `._v` which uses hardware interrupts to track the absolute position of the physical encoder. In theory -this should be precise, but on ESP32 with the Adafruit encoder it is not: -returning the dial to a given detent shows a small "drift" in position. - -Currently under investigation: it may be a consequence of ESP32's use of soft -IRQ's. - -This is probably of little practical consequence as encoder knobs are usually -used in systems where there is user feedback. In a practical application -([micro-gui](https://github.com/peterhinch/micropython-micro-gui)) I can see no -evidence of the missed pulses. +this should be precise with jitter caused by contact bounce being tracked. With +the Adafruit encoder it is imprecise: returning the dial to a given detent +after repeated movements shows a gradual "drift" in position. This occurs on +hosts with hard or soft IRQ's. I attempted to investigate this with various +hardware and software techniques and suspect there may be mechanical issues in +the device. Possibly pulses may occasionally missed with direction-dependent +probability. Unlike optical encoders these low cost controls make no claim to +absolute accuracy. + +This is of little practical consequence as encoder knobs are usually used in +systems where there is user feedback. In a practical application +([micro-gui](https://github.com/peterhinch/micropython-micro-gui)) there is no +obvious evidence of the missed pulses. ###### [Contents](./DRIVERS.md#1-contents) From 05579e5da91369802327a3a1b84f7b771f2f56aa Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 29 Aug 2021 09:28:32 +0100 Subject: [PATCH 082/305] Simplify aledflash demo. --- v3/as_demos/aledflash.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/v3/as_demos/aledflash.py b/v3/as_demos/aledflash.py index 2d478c4..3d961b5 100644 --- a/v3/as_demos/aledflash.py +++ b/v3/as_demos/aledflash.py @@ -7,9 +7,6 @@ import pyb import uasyncio as asyncio -async def killer(duration): - await asyncio.sleep(duration) - async def toggle(objLED, time_ms): while True: await asyncio.sleep_ms(time_ms) @@ -23,7 +20,7 @@ async def main(duration): for x, led in enumerate(leds): # Create a task for each LED t = int((0.2 + x/2) * 1000) asyncio.create_task(toggle(leds[x], t)) - asyncio.run(killer(duration)) + await asyncio.sleep(duration) def test(duration=10): try: From 519d1dfd24cdc0e1f7edc1a9ded1abbeac1a04e8 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 12 Sep 2021 06:25:25 +0100 Subject: [PATCH 083/305] Tutorial: add note re ThreadSafeFlag. --- v3/docs/TUTORIAL.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 245b658..d88f2aa 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -909,7 +909,12 @@ Asynchronous method: * `wait` Wait for the flag to be set. If the flag is already set then it returns immediately. -Usage example: triggering from a hard ISR. +Typical usage is having a `uasyncio` task wait on a hard ISR. Only one task +should wait on a `ThreadSafeFlag`. The hard ISR services the interrupting +device, sets the `ThreadSafeFlag`, and quits. A single task waits on the flag. +This design conforms with the self-clearing behaviour of the `ThreadSafeFlag`. +Each interrupting device has its own `ThreadSafeFlag` instance and its own +waiting task. ```python import uasyncio as asyncio from pyb import Timer @@ -967,8 +972,8 @@ possible level of performance as discussed in Regardless of performance issues, a key use for `ThreadSafeFlag` is where a hardware device requires the use of an ISR for a μs level response. Having -serviced the device, the ISR flags an asynchronous routine, say to process -received data. +serviced the device, the ISR flags an asynchronous routine, typically +processing received data. The fact that only one task may wait on a `ThreadSafeFlag` may be addressed by having the task that waits on the `ThreadSafeFlag` set an `Event`. Multiple From 3c8817d9ead33bcd8399d0935ffb24dd7bcd6e71 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 17 Sep 2021 08:41:06 +0100 Subject: [PATCH 084/305] I2C.md Fix broken links. --- v3/docs/I2C.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/v3/docs/I2C.md b/v3/docs/I2C.md index 35c47bd..96f147c 100644 --- a/v3/docs/I2C.md +++ b/v3/docs/I2C.md @@ -26,7 +26,7 @@ If the `Responder` (typically an ESP8266) crashes the resultant I2C failure is detected by the `Initiator` which can issue a hardware reboot to the `Responder` enabling the link to recover. This can occur transparently to the application and is covered in detail -[in section 5.3](./README.md#53-responder-crash-detection). +[in section 5.3](./I2C.md#53-responder-crash-detection). ## Changes @@ -42,20 +42,20 @@ V0.1 Initial release. # Contents - 1. [Files](./README.md#1-files) - 2. [Wiring](./README.md#2-wiring) - 3. [Design](./README.md#3-design) - 4. [API](./README.md#4-api) - 4.1 [Channel class](./README.md#41-channel-class) - 4.2 [Initiator class](./README.md#42-initiator-class) - 4.2.1 [Configuration](./README.md#421-configuration) Fine-tuning the interface. - 4.2.2 [Optional coroutines](./README.md#422-optional-coroutines) - 4.3 [Responder class](./README.md#43-responder-class) - 5. [Limitations](./README.md#5-limitations) - 5.1 [Blocking](./README.md#51-blocking) - 5.2 [Buffering and RAM usage](./README.md#52-buffering-and-ram-usage) - 5.3 [Responder crash detection](./README.md#53-responder-crash-detection) - 6. [Hacker notes](./README.md#6-hacker-notes) For anyone wanting to hack on + 1. [Files](./I2C.md#1-files) + 2. [Wiring](./I2C.md#2-wiring) + 3. [Design](./I2C.md#3-design) + 4. [API](./I2C.md#4-api) + 4.1 [Channel class](./I2C.md#41-channel-class) + 4.2 [Initiator class](./I2C.md#42-initiator-class) + 4.2.1 [Configuration](./I2C.md#421-configuration) Fine-tuning the interface. + 4.2.2 [Optional coroutines](./I2C.md#422-optional-coroutines) + 4.3 [Responder class](./I2C.md#43-responder-class) + 5. [Limitations](./I2C.md#5-limitations) + 5.1 [Blocking](./I2C.md#51-blocking) + 5.2 [Buffering and RAM usage](./I2C.md#52-buffering-and-ram-usage) + 5.3 [Responder crash detection](./I2C.md#53-responder-crash-detection) + 6. [Hacker notes](./I2C.md#6-hacker-notes) For anyone wanting to hack on the code. # 1. Files @@ -107,7 +107,7 @@ machine.Pin.board.EN_3V3.value(1) ``` This also enables the I2C pullups on the X side. -###### [Contents](./README.md#contents) +###### [Contents](./I2C.md#contents) # 3. Design @@ -142,7 +142,7 @@ starts to run once each end has determined that its counterpart is ready. The design assumes exclusive use of the I2C interface. Hard or soft I2C may be used. -###### [Contents](./README.md#contents) +###### [Contents](./I2C.md#contents) # 4. API @@ -244,7 +244,7 @@ finally: chan.close() # for subsequent runs ``` -###### [Contents](./README.md#contents) +###### [Contents](./I2C.md#contents) ## 4.1 Channel class @@ -267,7 +267,7 @@ Coroutine: 4. `reset=None` Optional tuple defining a reset pin (see below). 5. `verbose=True` If `True` causes debug messages to be output. 6. `cr_go=False` Optional coroutine to run at startup. See - [4.2.2](./README.md#422-optional-coroutines). + [4.2.2](./I2C.md#422-optional-coroutines). 7. `go_args=()` Optional tuple of args for above coro. 8. `cr_fail=False` Optional coro to run on ESP8266 fail or reboot. 9. `f_args=()` Optional tuple of args for above. @@ -290,12 +290,12 @@ If the `Initiator` has no `reset` tuple and the `Responder` times out, an 2. `rxbufsize=200` Size of receive buffer. This should exceed the maximum message length. -See [Section 4.2.1](./README.md#421-configuration). +See [Section 4.2.1](./I2C.md#421-configuration). ##### Instance variables: The `Initiator` maintains instance variables which may be used to measure its -peformance. See [Section 4.2.1](./README.md#421-configuration). +peformance. See [Section 4.2.1](./I2C.md#421-configuration). ##### Coroutine: 1. `reboot` If a `reset` tuple was provided, reboot the `Responder`. @@ -353,7 +353,7 @@ from as_drivers.i2c.asi2c_i import Initiator chan = Initiator(i2c, syn, ack, rst, verbose, self._go, (), self._fail) ``` -###### [Contents](./README.md#contents) +###### [Contents](./I2C.md#contents) ## 4.3 Responder class @@ -372,7 +372,7 @@ chan = Initiator(i2c, syn, ack, rst, verbose, self._go, (), self._fail) 2. `rxbufsize=200` Size of receive buffer. This should exceed the maximum message length. Consider reducing this in ESP8266 applications to save RAM. -###### [Contents](./README.md#contents) +###### [Contents](./I2C.md#contents) # 5. Limitations @@ -428,7 +428,7 @@ This also has implications when testing. If a `Responder` application is interrupted with `ctrl-c` the ISR will continue to run. To test crash detection issue a soft or hard reset to the `Responder`. -###### [Contents](./README.md#contents) +###### [Contents](./I2C.md#contents) # 6. Hacker notes From 6619c5dc8e92be84d55b55ba3f9b337c555c4eed Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 21 Sep 2021 08:04:43 +0100 Subject: [PATCH 085/305] Add uasyncio monitor v3/as_demos/monitor. --- v3/as_demos/monitor/README.md | 209 ++++++++++++++++++++++++++++ v3/as_demos/monitor/monitor.jpg | Bin 0 -> 50663 bytes v3/as_demos/monitor/monitor.py | 97 +++++++++++++ v3/as_demos/monitor/monitor_pico.py | 55 ++++++++ v3/as_demos/monitor/monitor_test.py | 55 ++++++++ v3/as_demos/monitor/quick_test.py | 31 +++++ 6 files changed, 447 insertions(+) create mode 100644 v3/as_demos/monitor/README.md create mode 100644 v3/as_demos/monitor/monitor.jpg create mode 100644 v3/as_demos/monitor/monitor.py create mode 100644 v3/as_demos/monitor/monitor_pico.py create mode 100644 v3/as_demos/monitor/monitor_test.py create mode 100644 v3/as_demos/monitor/quick_test.py diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md new file mode 100644 index 0000000..4445c95 --- /dev/null +++ b/v3/as_demos/monitor/README.md @@ -0,0 +1,209 @@ +# 1. A uasyncio monitor + +This library provides a means of examining the behaviour of a running +`uasyncio` system. The device under test is linked to a Raspberry Pi Pico. The +latter displays the behaviour of the host by pin changes and/or optional print +statements. Communication with the Pico is uni-directional via a UART so only a +single GPIO pin is used - at last a use for the ESP8266 transmit-only UART(1). + +A logic analyser or scope provides an insight into the way an asynchronous +application is working. + +Where an application runs multiple concurrent tasks it can be difficult to +locate a task which is hogging CPU time. Long blocking periods can also result +from several tasks each of which can block for a period. If, on occasion, these +are scheduled in succession, the times can add. The monitor issues a trigger +when the blocking period exceeds a threshold. With a logic analyser the system +state at the time of the transient event may be examined. + +The following image shows the `quick_test.py` code being monitored at the point +when a task hogs the CPU. The top line 00 shows the "hog detect" trigger. Line +02 shows the fast running `hog_detect` task which cannot run at the time of the +trigger. Lines 01 and 03 show the `foo` and `bar` tasks. +![Image](/.monitor.jpg) + +## 1.1 Pre-requisites + +The device being monitored must run firmware V1.17 or later. The `uasyncio` +version should be V3 (as included in the firmware). + +## 1.2 Usage + +Example script `quick_test.py` provides a usage example. + +An application to be monitored typically has the following setup code: +```python +from monitor import monitor, hog_detect, set_uart +set_uart(2) # Define device under test UART no. +``` + +Coroutines to be monitored are prefixed with the `@monitor` decorator: +```python +@monitor(2, 3) +async def my_coro(): + # code +``` +The decorator args are as follows: + 1. A unique `ident` for the code being monitored. Determines the pin number on + the Pico. See [Pico Pin mapping](./README.md#3-pico-pin-mapping). + 2. An optional arg defining the maximum number of concurrent instances of the + task to be independently monitored (default 1). + +Whenever the code runs, a pin on the Pico will go high, and when the code +terminates it will go low. This enables the behaviour of the system to be +viewed on a logic analyser or via console output on the Pico. This behavior +works whether the code terminates normally, is cancelled or has a timeout. + +In the example above, when `my_coro` starts, the pin defined by `ident==2` +(GPIO 4) will go high. When it ends, the pin will go low. If, while it is +running, a second instance of `my_coro` is launched, the next pin (GPIO 5) will +go high. Pins will go low when the relevant instance terminates, is cancelled, +or times out. If more instances are started than were specified to the +decorator, a warning will be printed on the host. All excess instances will be +associated with the final pin (`pins[ident + max_instances - 1]`) which will +only go low when all instances associated with that pin have terminated. + +Consequently if `max_instances=1` and multiple instances are launched, a +warning will appear on the host; the pin will go high when the first instance +starts and will not go low until all have ended. + +## 1.3 Detecting CPU hogging + +A common cause of problems in asynchronous code is the case where a task blocks +for a period, hogging the CPU, stalling the scheduler and preventing other +tasks from running. Determining the task responsible can be difficult. + +The pin state only indicates that the task is running. A pin state of 1 does +not imply CPU hogging. Thus +```python +@monitor(3) +async def long_time(): + await asyncio.sleep(30) +``` +will cause the pin to go high for 30s, even though the task is consuming no +resources for that period. + +To provide a clue about CPU hogging, a `hog_detect` coroutine is provided. This +has `ident=0` and, if used, is monitored on GPIO 2. It loops, yielding to the +scheduler. It will therefore be scheduled in round-robin fashion at speed. If +long gaps appear in the pulses on GPIO 2, other tasks are hogging the CPU. +Usage of this is optional. To use, issue +```python +import uasyncio as asyncio +from monitor import monitor, hog_detect +# code omitted +asyncio.create_task(hog_detect()) +# code omitted +``` +To aid in detecting the gaps in execution, the Pico code implements a timer. +This is retriggered by activity on `ident=0`. If it times out, a brief high +going pulse is produced on pin 28, along with the console message "Hog". The +pulse can be used to trigger a scope or logic analyser. The duration of the +timer may be adjusted - see [section 4](./README.md~4-the-pico-code). + +# 2. Monitoring synchronous code + +In general there are easier ways to debug synchronous code. However in the +context of a monitored asynchronous application there may be a need to view the +timing of synchronous code. Functions and methods may be monitored either in +the declaration via a decorator or when called via a context manager. + +## 2.1 The mon_func decorator + +This works as per the asynchronous decorator, but without the `max_instances` +arg. This will activate the GPIO associated with ident 20 for the duration of +every call to `sync_func()`: +```python +@mon_func(20) +def sync_func(): + pass +``` + +## 2.2 The mon_call context manager + +This may be used to monitor a function only when called from specific points in +the code. +```python +def another_sync_func(): + pass + +with mon_call(22): + another_sync_func() +``` + +It is advisable not to use the context manager with a function having the +`mon_func` decorator. The pin and report behaviour is confusing. + +# 3. Pico Pin mapping + +The Pico GPIO numbers start at 2 to allow for UART(0) and also have a gap where +GPIO's are used for particular purposes. This is the mapping between `ident` +GPIO no. and Pico PCB pin, with the pins for the timer and the UART link also +identified: + +| ident | GPIO | pin | +|:-----:|:----:|:----:| +| uart | 1 | 2 | +| 0 | 2 | 4 | +| 1 | 3 | 5 | +| 2 | 4 | 6 | +| 3 | 5 | 7 | +| 4 | 6 | 9 | +| 5 | 7 | 10 | +| 6 | 8 | 11 | +| 7 | 9 | 12 | +| 8 | 10 | 14 | +| 9 | 11 | 15 | +| 10 | 12 | 16 | +| 11 | 13 | 17 | +| 12 | 14 | 19 | +| 13 | 15 | 20 | +| 14 | 16 | 21 | +| 15 | 17 | 22 | +| 16 | 18 | 24 | +| 17 | 19 | 25 | +| 18 | 20 | 26 | +| 19 | 21 | 27 | +| 20 | 22 | 29 | +| 21 | 26 | 31 | +| 22 | 27 | 32 | +| timer | 28 | 34 | + +The host's UART `txd` pin should be connected to Pico GPIO 1 (pin 2). There +must be a link between `Gnd` pins on the host and Pico. + +# 4. The Pico code + +Monitoring of the UART with default behaviour is started as follows: +```python +from monitor_pico import run +run() +``` +By default the Pico does not produce console output and the timer has a period +of 100ms - pin 28 will pulse if ident 0 is inactive for over 100ms. These +behaviours can be modified by the following `run` args: + 1. `period=100` Define the timer period in ms. + 2. `verbose=()` Determines which `ident` values should produce console output. + +Thus to run such that idents 4 and 7 produce console output, with hogging +reported if blocking is for more than 60ms, issue +```python +from monitor_pico import run +run(60, (4, 7)) +``` + +# 5. Design notes + +The use of decorators is intended to ease debugging: they are readily turned on +and off by commenting out. + +The Pico was chosen for extremely low cost. It has plenty of GPIO pins and no +underlying OS to introduce timing uncertainties. + +Symbols transmitted by the UART are printable ASCII characters to ease +debugging. A single byte protocol simplifies and speeds the Pico code. + +The baudrate of 1Mbps was chosen to minimise latency (10μs per character is +fast in the context of uasyncio). It also ensures that tasks like `hog_detect`, +which can be scheduled at a high rate, can't overflow the UART buffer. The +1Mbps rate seems widely supported. diff --git a/v3/as_demos/monitor/monitor.jpg b/v3/as_demos/monitor/monitor.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a5e71e7923070e90eef71d08ca757a6a562b446 GIT binary patch literal 50663 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 o)01`7fM= 0 and ident + num <= 23: + try: + for x in range(ident, ident + num): + _available.remove(x) + except KeyError: + raise ValueError(f'Monitor error - ident {x:02} already allocated.') + else: + raise ValueError(f'Monitor error - ident {ident:02} out of range.') + + +def monitor(n, max_instances=1): + def decorator(coro): + # This code runs before asyncio.run() + _validate(n, max_instances) + instance = 0 + async def wrapped_coro(*args, **kwargs): + # realtime + nonlocal instance + d = 0x40 + n + min(instance, max_instances - 1) + v = bytes(chr(d), 'utf8') + instance += 1 + if instance > max_instances: + print(f'Monitor {n:02} max_instances reached') + uart.write(v) + try: + res = await coro(*args, **kwargs) + except asyncio.CancelledError: + raise + finally: + d |= 0x20 + v = bytes(chr(d), 'utf8') + uart.write(v) + instance -= 1 + return res + return wrapped_coro + return decorator + +# Optionally run this to show up periods of blocking behaviour +@monitor(0) +async def _do_nowt(): + await asyncio.sleep_ms(0) + +async def hog_detect(): + while True: + await _do_nowt() + +# Monitor a synchronous function definition +def mon_func(n): + def decorator(func): + _validate(n) + dstart = 0x40 + n + vstart = bytes(chr(dstart), 'utf8') + dend = 0x60 + n + vend = bytes(chr(dend), 'utf8') + def wrapped_func(*args, **kwargs): + uart.write(vstart) + res = func(*args, **kwargs) + uart.write(vend) + return res + return wrapped_func + return decorator + + +# Monitor a synchronous function call +class mon_call: + def __init__(self, n): + _validate(n) + self.n = n + self.dstart = 0x40 + n + self.vstart = bytes(chr(self.dstart), 'utf8') + self.dend = 0x60 + n + self.vend = bytes(chr(self.dend), 'utf8') + + def __enter__(self): + uart.write(self.vstart) + return self + + def __exit__(self, type, value, traceback): + uart.write(self.vend) + return False # Don't silence exceptions diff --git a/v3/as_demos/monitor/monitor_pico.py b/v3/as_demos/monitor/monitor_pico.py new file mode 100644 index 0000000..2b6a4da --- /dev/null +++ b/v3/as_demos/monitor/monitor_pico.py @@ -0,0 +1,55 @@ +# monitor_pico.py +# Runs on a Raspberry Pico board to receive data from monitor.py + +# Copyright (c) 2021 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +# UART gets a single ASCII byte defining the pin number and whether +# to increment (uppercase) or decrement (lowercase) the use count. +# Pin goes high if use count > 0 else low. +# incoming numbers are 0..22 which map onto 23 GPIO pins + +from machine import UART, Pin, Timer + +# Valid GPIO pins +# GP0,1 are UART 0 so pins are 2..22, 26..27 +PIN_NOS = list(range(2,23)) + list(range(26, 28)) +uart = UART(0, 1_000_000) # rx on GP1 + +pin_t = Pin(28, Pin.OUT) +def _cb(_): + pin_t(1) + print('Hog') + pin_t(0) + +tim = Timer() +t_ms = 100 +# Index is incoming ID +# contents [Pin, instance_count, verbose] +pins = [] +for pin_no in PIN_NOS: + pins.append([Pin(pin_no, Pin.OUT), 0, False]) + +def run(period=100, verbose=[]): + global t_ms + t_ms = period + for x in verbose: + pins[x][2] = True + while True: + while not uart.any(): + pass + x = ord(uart.read(1)) + #print('got', chr(x)) gets CcAa + if not 0x40 <= x <= 0x7f: # Get an initial 0 + continue + if x == 0x40: + tim.init(period=t_ms, mode=Timer.ONE_SHOT, callback=_cb) + i = x & 0x1f # Key: 0x40 (ord('@')) is pin ID 0 + d = -1 if x & 0x20 else 1 + pins[i][1] += d + if pins[i][1]: # Count > 0 turn pin on + pins[i][0](1) + else: + pins[i][0](0) + if pins[i][2]: + print(f'ident {i} count {pins[i][1]}') diff --git a/v3/as_demos/monitor/monitor_test.py b/v3/as_demos/monitor/monitor_test.py new file mode 100644 index 0000000..8dc207e --- /dev/null +++ b/v3/as_demos/monitor/monitor_test.py @@ -0,0 +1,55 @@ +# monitor_test.py + +import uasyncio as asyncio +from monitor import monitor, mon_func, mon_call, set_uart + +set_uart(2) # Define interface to use + +@monitor(1, 2) +async def foo(t): + await asyncio.sleep_ms(t) + return t * 2 + +@monitor(3) +async def bar(t): + await asyncio.sleep_ms(t) + return t * 2 + +@monitor(4) +async def forever(): + while True: + await asyncio.sleep(1) + +class Foo: + def __init__(self): + pass + @monitor(5, 1) + async def rats(self): + await asyncio.sleep(1) + print('rats ran') + +@mon_func(20) +def sync_func(): + pass + +def another_sync_func(): + pass + +async def main(): + sync_func() + with mon_call(22): + another_sync_func() + while True: + myfoo = Foo() + asyncio.create_task(myfoo.rats()) + ft = asyncio.create_task(foo(1000)) + bt = asyncio.create_task(bar(200)) + print('bar', await bt) + ft.cancel() + print('got', await foo(2000)) + try: + await asyncio.wait_for(forever(), 3) + except asyncio.TimeoutError: # Mandatory error trapping + print('got timeout') # Caller sees TimeoutError + +asyncio.run(main()) diff --git a/v3/as_demos/monitor/quick_test.py b/v3/as_demos/monitor/quick_test.py new file mode 100644 index 0000000..c1984cd --- /dev/null +++ b/v3/as_demos/monitor/quick_test.py @@ -0,0 +1,31 @@ +# quick_test.py + +import uasyncio as asyncio +import time +from monitor import monitor, hog_detect, set_uart + +set_uart(2) # Define interface to use + +@monitor(1) +async def foo(t): + await asyncio.sleep_ms(t) + +@monitor(2) +async def hog(): + await asyncio.sleep(5) + time.sleep_ms(500) + +@monitor(3) +async def bar(t): + await asyncio.sleep_ms(t) + + +async def main(): + asyncio.create_task(hog_detect()) + asyncio.create_task(hog()) # Will hog for 500ms after 5 secs + while True: + ft = asyncio.create_task(foo(100)) + await bar(150) + await asyncio.sleep_ms(50) + +asyncio.run(main()) From 7f3ffe150c51d6af25c38e39bea9264c81b678a9 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 21 Sep 2021 08:07:06 +0100 Subject: [PATCH 086/305] Add uasyncio monitor v3/as_demos/monitor. --- v3/as_demos/monitor/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 4445c95..7f0e392 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -20,7 +20,7 @@ The following image shows the `quick_test.py` code being monitored at the point when a task hogs the CPU. The top line 00 shows the "hog detect" trigger. Line 02 shows the fast running `hog_detect` task which cannot run at the time of the trigger. Lines 01 and 03 show the `foo` and `bar` tasks. -![Image](/.monitor.jpg) +![Image](./monitor.jpg) ## 1.1 Pre-requisites From 4967b686090557e26375dfc07ecfd5c004e43d32 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 21 Sep 2021 09:51:58 +0100 Subject: [PATCH 087/305] V3/README.md Move V2 porting guide to end. --- v3/README.md | 46 ++++++++++++++++++++--------------- v3/as_demos/monitor/README.md | 3 +++ 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/v3/README.md b/v3/README.md index f11aafe..cfde5a6 100644 --- a/v3/README.md +++ b/v3/README.md @@ -31,6 +31,12 @@ This [lightweight scheduler](./docs/SCHEDULE.md) enables tasks to be scheduled at future times. These can be assigned in a flexible way: a task might run at 4.10am on Monday and Friday if there's no "r" in the month. +### A monitor + +This [monitor](./as_demos/monitor/README.md) enables a running `uasyncio` +application to be monitored using a Pi Pico, ideally with a scope or logic +analyser. + ### Asynchronous device drivers These device drivers are intended as examples of asynchronous code which are @@ -80,6 +86,26 @@ supported. The `Future` class is not supported, nor are the `event_loop` methods `call_soon`, `call_later`, `call_at`. +## 2.1 Outstanding issues with V3 + +V3 is still a work in progress. The following is a list of issues which I hope +will be addressed in due course. + +### 2.1.1 Fast I/O scheduling + +There is currently no support for this: I/O is scheduled in round robin fashion +with other tasks. There are situations where this is too slow, for example in +I2S applications and ones involving multiple fast I/O streams, e.g. from UARTs. +In these applications there is still a use case for the `fast_io` V2 variant. + +### 2.1.2 Synchronisation primitives + +These CPython primitives are outstanding: + * `Semaphore`. + * `BoundedSemaphore`. + * `Condition`. + * `Queue`. + # 3. Porting applications from V2 Many applications using the coding style advocated in the V2 tutorial will work @@ -182,23 +208,3 @@ New versions are provided in this repository. Classes: * `Delay_ms` Software retriggerable monostable (watchdog-like object). * `Switch` Debounced switch with close and open callbacks. * `Pushbutton` Pushbutton with double-click and long press callbacks. - -# 4. Outstanding issues with V3 - -V3 is still a work in progress. The following is a list of issues which I hope -will be addressed in due course. - -## 4.1 Fast I/O scheduling - -There is currently no support for this: I/O is scheduled in round robin fashion -with other tasks. There are situations where this is too slow, for example in -I2S applications and ones involving multiple fast I/O streams, e.g. from UARTs. -In these applications there is still a use case for the `fast_io` V2 variant. - -## 4.2 Synchronisation primitives - -These CPython primitives are outstanding: - * `Semaphore`. - * `BoundedSemaphore`. - * `Condition`. - * `Queue`. diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 7f0e392..8610aaf 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -207,3 +207,6 @@ The baudrate of 1Mbps was chosen to minimise latency (10μs per character is fast in the context of uasyncio). It also ensures that tasks like `hog_detect`, which can be scheduled at a high rate, can't overflow the UART buffer. The 1Mbps rate seems widely supported. + +This project was inspired by +[this GitHub thread](https://github.com/micropython/micropython/issues/7456). From 47944b2543689407780bf03b326ddcf636ba6e75 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 21 Sep 2021 12:37:30 +0100 Subject: [PATCH 088/305] Provide monitor.init. --- v3/as_demos/monitor/README.md | 7 +++++-- v3/as_demos/monitor/monitor.py | 3 +++ v3/as_demos/monitor/monitor_pico.py | 5 ++++- v3/as_demos/monitor/monitor_test.py | 3 ++- v3/as_demos/monitor/quick_test.py | 3 ++- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 8610aaf..1a997da 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -33,10 +33,13 @@ Example script `quick_test.py` provides a usage example. An application to be monitored typically has the following setup code: ```python -from monitor import monitor, hog_detect, set_uart +from monitor import monitor, monitor_init, hog_detect, set_uart set_uart(2) # Define device under test UART no. ``` - +On application start it should issue +```python +monitor_init() +``` Coroutines to be monitored are prefixed with the `@monitor` decorator: ```python @monitor(2, 3) diff --git a/v3/as_demos/monitor/monitor.py b/v3/as_demos/monitor/monitor.py index 81c7285..7d0ccc6 100644 --- a/v3/as_demos/monitor/monitor.py +++ b/v3/as_demos/monitor/monitor.py @@ -52,6 +52,9 @@ async def wrapped_coro(*args, **kwargs): return wrapped_coro return decorator +def monitor_init(): + uart.write(b'z') + # Optionally run this to show up periods of blocking behaviour @monitor(0) async def _do_nowt(): diff --git a/v3/as_demos/monitor/monitor_pico.py b/v3/as_demos/monitor/monitor_pico.py index 2b6a4da..edfe0e7 100644 --- a/v3/as_demos/monitor/monitor_pico.py +++ b/v3/as_demos/monitor/monitor_pico.py @@ -39,9 +39,12 @@ def run(period=100, verbose=[]): while not uart.any(): pass x = ord(uart.read(1)) - #print('got', chr(x)) gets CcAa if not 0x40 <= x <= 0x7f: # Get an initial 0 continue + if x == 0x7a: # Init: program under test has restarted + for w in range(len(pins)): + pins[w][1] = 0 + continue if x == 0x40: tim.init(period=t_ms, mode=Timer.ONE_SHOT, callback=_cb) i = x & 0x1f # Key: 0x40 (ord('@')) is pin ID 0 diff --git a/v3/as_demos/monitor/monitor_test.py b/v3/as_demos/monitor/monitor_test.py index 8dc207e..f351cc4 100644 --- a/v3/as_demos/monitor/monitor_test.py +++ b/v3/as_demos/monitor/monitor_test.py @@ -1,7 +1,7 @@ # monitor_test.py import uasyncio as asyncio -from monitor import monitor, mon_func, mon_call, set_uart +from monitor import monitor, monitor_init, mon_func, mon_call, set_uart set_uart(2) # Define interface to use @@ -36,6 +36,7 @@ def another_sync_func(): pass async def main(): + monitor_init() sync_func() with mon_call(22): another_sync_func() diff --git a/v3/as_demos/monitor/quick_test.py b/v3/as_demos/monitor/quick_test.py index c1984cd..855cd32 100644 --- a/v3/as_demos/monitor/quick_test.py +++ b/v3/as_demos/monitor/quick_test.py @@ -2,7 +2,7 @@ import uasyncio as asyncio import time -from monitor import monitor, hog_detect, set_uart +from monitor import monitor, monitor_init, hog_detect, set_uart set_uart(2) # Define interface to use @@ -21,6 +21,7 @@ async def bar(t): async def main(): + monitor_init() asyncio.create_task(hog_detect()) asyncio.create_task(hog()) # Will hog for 500ms after 5 secs while True: From e44472579834029d6c8695479151d8c36629fef5 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 24 Sep 2021 14:56:01 +0100 Subject: [PATCH 089/305] monitor: Reduce latency, adapt for future SPI option. --- v3/as_demos/monitor/README.md | 16 ++++++++- v3/as_demos/monitor/monitor.py | 26 ++++++++------ v3/as_demos/monitor/monitor_pico.py | 55 +++++++++++++++++------------ v3/as_demos/monitor/quick_test.py | 8 +++-- 4 files changed, 69 insertions(+), 36 deletions(-) diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 1a997da..96fffd2 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -187,6 +187,7 @@ of 100ms - pin 28 will pulse if ident 0 is inactive for over 100ms. These behaviours can be modified by the following `run` args: 1. `period=100` Define the timer period in ms. 2. `verbose=()` Determines which `ident` values should produce console output. + 3. `device="uart"` Provides for future use of other interfaces. Thus to run such that idents 4 and 7 produce console output, with hogging reported if blocking is for more than 60ms, issue @@ -195,7 +196,12 @@ from monitor_pico import run run(60, (4, 7)) ``` -# 5. Design notes +# 5. Performance and design notes + +The latency between a monitored coroutine starting to run and the Pico pin +going high is about 20μs. This isn't as absurd as it sounds: theoretically the +latency could be negative as the effect of the decorator is to send the +character before the coroutine starts. The use of decorators is intended to ease debugging: they are readily turned on and off by commenting out. @@ -213,3 +219,11 @@ which can be scheduled at a high rate, can't overflow the UART buffer. The This project was inspired by [this GitHub thread](https://github.com/micropython/micropython/issues/7456). + +# 6. Work in progress + +It is intended to add an option for SPI communication; `monitor.py` has a +`set_device` method which can be passed an instance of an initialised SPI +object. The Pico `run` method will be able to take a `device="spi"` arg which +will expect an SPI connection on pins 0 (sck) and 1 (data). This requires a +limited implementation of an SPI slave using the PIO, which I will do soon. diff --git a/v3/as_demos/monitor/monitor.py b/v3/as_demos/monitor/monitor.py index 7d0ccc6..606f8eb 100644 --- a/v3/as_demos/monitor/monitor.py +++ b/v3/as_demos/monitor/monitor.py @@ -7,10 +7,16 @@ import uasyncio as asyncio from machine import UART -uart = None +device = None def set_uart(n): # Monitored app defines interface - global uart - uart = UART(n, 1_000_000) + global device + device = UART(n, 1_000_000) + +# For future use with SPI +# Pass initialised instance of some device +def set_device(dev): + global device + device = dev _available = set(range(0, 23)) # Valid idents are 0..22 @@ -38,7 +44,7 @@ async def wrapped_coro(*args, **kwargs): instance += 1 if instance > max_instances: print(f'Monitor {n:02} max_instances reached') - uart.write(v) + device.write(v) try: res = await coro(*args, **kwargs) except asyncio.CancelledError: @@ -46,14 +52,14 @@ async def wrapped_coro(*args, **kwargs): finally: d |= 0x20 v = bytes(chr(d), 'utf8') - uart.write(v) + device.write(v) instance -= 1 return res return wrapped_coro return decorator def monitor_init(): - uart.write(b'z') + device.write(b'z') # Optionally run this to show up periods of blocking behaviour @monitor(0) @@ -73,9 +79,9 @@ def decorator(func): dend = 0x60 + n vend = bytes(chr(dend), 'utf8') def wrapped_func(*args, **kwargs): - uart.write(vstart) + device.write(vstart) res = func(*args, **kwargs) - uart.write(vend) + device.write(vend) return res return wrapped_func return decorator @@ -92,9 +98,9 @@ def __init__(self, n): self.vend = bytes(chr(self.dend), 'utf8') def __enter__(self): - uart.write(self.vstart) + device.write(self.vstart) return self def __exit__(self, type, value, traceback): - uart.write(self.vend) + device.write(self.vend) return False # Don't silence exceptions diff --git a/v3/as_demos/monitor/monitor_pico.py b/v3/as_demos/monitor/monitor_pico.py index edfe0e7..ad4c363 100644 --- a/v3/as_demos/monitor/monitor_pico.py +++ b/v3/as_demos/monitor/monitor_pico.py @@ -9,12 +9,13 @@ # Pin goes high if use count > 0 else low. # incoming numbers are 0..22 which map onto 23 GPIO pins -from machine import UART, Pin, Timer +from machine import UART, Pin, Timer, freq + +freq(250_000_000) # Valid GPIO pins # GP0,1 are UART 0 so pins are 2..22, 26..27 PIN_NOS = list(range(2,23)) + list(range(26, 28)) -uart = UART(0, 1_000_000) # rx on GP1 pin_t = Pin(28, Pin.OUT) def _cb(_): @@ -30,29 +31,37 @@ def _cb(_): for pin_no in PIN_NOS: pins.append([Pin(pin_no, Pin.OUT), 0, False]) -def run(period=100, verbose=[]): +# native reduced latency to 10μs but killed the hog detector: timer never timed out. +# Also locked up Pico so ctrl-c did not interrupt. +#@micropython.native +def run(period=100, verbose=[], device="uart"): global t_ms t_ms = period for x in verbose: pins[x][2] = True + # Provide for future devices. Must support a blocking read. + if device == "uart": + uart = UART(0, 1_000_000) # rx on GPIO 1 + def read(): + while not uart.any(): + pass + return ord(uart.read(1)) + while True: - while not uart.any(): - pass - x = ord(uart.read(1)) - if not 0x40 <= x <= 0x7f: # Get an initial 0 - continue - if x == 0x7a: # Init: program under test has restarted - for w in range(len(pins)): - pins[w][1] = 0 - continue - if x == 0x40: - tim.init(period=t_ms, mode=Timer.ONE_SHOT, callback=_cb) - i = x & 0x1f # Key: 0x40 (ord('@')) is pin ID 0 - d = -1 if x & 0x20 else 1 - pins[i][1] += d - if pins[i][1]: # Count > 0 turn pin on - pins[i][0](1) - else: - pins[i][0](0) - if pins[i][2]: - print(f'ident {i} count {pins[i][1]}') + if x := read(): # Get an initial 0 on UART + if x == 0x7a: # Init: program under test has restarted + for pin in pins: + pin[1] = 0 + continue + if x == 0x40: # Retrigger hog detector. + tim.init(period=t_ms, mode=Timer.ONE_SHOT, callback=_cb) + p = pins[x & 0x1f] # Key: 0x40 (ord('@')) is pin ID 0 + if x & 0x20: # Going down + p[1] -= 1 + if not p[1]: # Instance count is zero + p[0](0) + else: + p[0](1) + p[1] += 1 + if p[2]: + print(f'ident {i} count {p[1]}') diff --git a/v3/as_demos/monitor/quick_test.py b/v3/as_demos/monitor/quick_test.py index 855cd32..74178b6 100644 --- a/v3/as_demos/monitor/quick_test.py +++ b/v3/as_demos/monitor/quick_test.py @@ -2,12 +2,15 @@ import uasyncio as asyncio import time +from machine import Pin from monitor import monitor, monitor_init, hog_detect, set_uart set_uart(2) # Define interface to use @monitor(1) -async def foo(t): +async def foo(t, pin): + pin(1) # Measure latency + pin(0) await asyncio.sleep_ms(t) @monitor(2) @@ -22,10 +25,11 @@ async def bar(t): async def main(): monitor_init() + test_pin = Pin('X6', Pin.OUT) asyncio.create_task(hog_detect()) asyncio.create_task(hog()) # Will hog for 500ms after 5 secs while True: - ft = asyncio.create_task(foo(100)) + asyncio.create_task(foo(100, test_pin)) await bar(150) await asyncio.sleep_ms(50) From 3e8a1e8f22b04a027c2798e1dbf41e77578ed6b2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 24 Sep 2021 15:15:08 +0100 Subject: [PATCH 090/305] Tutorial: update and correct section on sockets. --- v3/docs/TUTORIAL.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index d88f2aa..2fdb312 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2352,8 +2352,8 @@ The second approach to socket programming is to use nonblocking sockets. This adds complexity but is necessary in some applications, notably where connectivity is via WiFi (see below). -At the time of writing (March 2019) support for TLS on nonblocking sockets is -under development. Its exact status is unknown (to me). +Support for TLS on nonblocking sockets is platform dependent. It works on ESP32 +and Pyboard D. It does not work on ESP8266. The use of nonblocking sockets requires some attention to detail. If a nonblocking read is performed, because of server latency, there is no guarantee @@ -2364,14 +2364,6 @@ Hence asynchronous read and write methods need to iteratively perform the nonblocking operation until the required data has been read or written. In practice a timeout is likely to be required to cope with server outages. -A further complication is that the ESP32 port had issues which required rather -unpleasant hacks for error-free operation. I have not tested whether this is -still the case. - -The file [sock_nonblock.py](./sock_nonblock.py) illustrates the sort of -techniques required. It is not a working demo, and solutions are likely to be -application dependent. - ### 7.6.1 WiFi issues The `uasyncio` stream mechanism is not good at detecting WiFi outages. I have From 58a2bf2ba5bedffa1edc95a686cf0d43f14bb633 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 27 Sep 2021 13:11:18 +0100 Subject: [PATCH 091/305] monitor: add SPI interface support. --- v3/as_demos/monitor/README.md | 162 ++++++++++++++++------------ v3/as_demos/monitor/monitor.py | 55 ++++++---- v3/as_demos/monitor/monitor_pico.py | 56 ++++++++-- v3/as_demos/monitor/monitor_test.py | 4 +- v3/as_demos/monitor/quick_test.py | 8 +- v3/as_demos/rate.py | 7 +- 6 files changed, 195 insertions(+), 97 deletions(-) diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 96fffd2..9f7da9c 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -3,40 +3,70 @@ This library provides a means of examining the behaviour of a running `uasyncio` system. The device under test is linked to a Raspberry Pi Pico. The latter displays the behaviour of the host by pin changes and/or optional print -statements. Communication with the Pico is uni-directional via a UART so only a -single GPIO pin is used - at last a use for the ESP8266 transmit-only UART(1). +statements. A logic analyser or scope provides an insight into the way an +asynchronous application is working. -A logic analyser or scope provides an insight into the way an asynchronous -application is working. +Communication with the Pico may be by UART or SPI, and is uni-directional from +system under test to Pico. If a UART is used only one GPIO pin is used; at last +a use for the ESP8266 transmit-only UART(1). SPI requires three - mosi, sck and +cs/. Where an application runs multiple concurrent tasks it can be difficult to locate a task which is hogging CPU time. Long blocking periods can also result from several tasks each of which can block for a period. If, on occasion, these are scheduled in succession, the times can add. The monitor issues a trigger -when the blocking period exceeds a threshold. With a logic analyser the system -state at the time of the transient event may be examined. +pulse when the blocking period exceeds a threshold. With a logic analyser the +system state at the time of the transient event may be examined. The following image shows the `quick_test.py` code being monitored at the point when a task hogs the CPU. The top line 00 shows the "hog detect" trigger. Line 02 shows the fast running `hog_detect` task which cannot run at the time of the -trigger. Lines 01 and 03 show the `foo` and `bar` tasks. +trigger because another task is hogging the CPU. Lines 01 and 03 show the `foo` +and `bar` tasks. ![Image](./monitor.jpg) +### Breaking changes to support SPI + +The `set_uart` method is replaced by `set_device`. Pin mappings on the Pico +have changed. + ## 1.1 Pre-requisites The device being monitored must run firmware V1.17 or later. The `uasyncio` -version should be V3 (as included in the firmware). +version should be V3 (included in the firmware). ## 1.2 Usage -Example script `quick_test.py` provides a usage example. +Example script `quick_test.py` provides a usage example. It may be adapted to +use a UART or SPI interface: see commented-out code. + +### 1.2.1 Interface selection set_device() -An application to be monitored typically has the following setup code: +An application to be monitored needs setup code to initialise the interface. +This comprises a call to `monitor.set_device` with an initialised UART or SPI +device. The Pico must be set up to match the interface chosen on the host: see +[section 4](./README.md#4-the-pico-code). + +In the case of a UART an initialised UART with 1MHz baudrate is passed: +```python +from machine import UART +from monitor import monitor, monitor_init, hog_detect, set_device +set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz. +``` +In the case of SPI initialised SPI and cs/ Pin instances are passed: ```python -from monitor import monitor, monitor_init, hog_detect, set_uart -set_uart(2) # Define device under test UART no. +from machine import Pin, SPI +from monitor import monitor, monitor_init, hog_detect, set_device +set_device(SPI(2, baudrate=5_000_000), Pin('X6', Pin.OUT)) # Device under test SPI ``` -On application start it should issue +The SPI instance must have default args; the one exception being baudrate which +may be any value. I have tested up to 30MHz but there is no benefit in running +above 1MHz. Hard or soft SPI may be used. It should be possible to share the +bus with other devices, although I haven't tested this. + +### 1.2.2 Monitoring + +On startup, after defining the interface, an application should issue: ```python monitor_init() ``` @@ -52,14 +82,14 @@ The decorator args are as follows: 2. An optional arg defining the maximum number of concurrent instances of the task to be independently monitored (default 1). -Whenever the code runs, a pin on the Pico will go high, and when the code +Whenever the coroutine runs, a pin on the Pico will go high, and when the code terminates it will go low. This enables the behaviour of the system to be viewed on a logic analyser or via console output on the Pico. This behavior works whether the code terminates normally, is cancelled or has a timeout. In the example above, when `my_coro` starts, the pin defined by `ident==2` -(GPIO 4) will go high. When it ends, the pin will go low. If, while it is -running, a second instance of `my_coro` is launched, the next pin (GPIO 5) will +(GPIO 5) will go high. When it ends, the pin will go low. If, while it is +running, a second instance of `my_coro` is launched, the next pin (GPIO 6) will go high. Pins will go low when the relevant instance terminates, is cancelled, or times out. If more instances are started than were specified to the decorator, a warning will be printed on the host. All excess instances will be @@ -87,9 +117,9 @@ will cause the pin to go high for 30s, even though the task is consuming no resources for that period. To provide a clue about CPU hogging, a `hog_detect` coroutine is provided. This -has `ident=0` and, if used, is monitored on GPIO 2. It loops, yielding to the +has `ident=0` and, if used, is monitored on GPIO 3. It loops, yielding to the scheduler. It will therefore be scheduled in round-robin fashion at speed. If -long gaps appear in the pulses on GPIO 2, other tasks are hogging the CPU. +long gaps appear in the pulses on GPIO 3, other tasks are hogging the CPU. Usage of this is optional. To use, issue ```python import uasyncio as asyncio @@ -139,45 +169,51 @@ It is advisable not to use the context manager with a function having the # 3. Pico Pin mapping -The Pico GPIO numbers start at 2 to allow for UART(0) and also have a gap where -GPIO's are used for particular purposes. This is the mapping between `ident` -GPIO no. and Pico PCB pin, with the pins for the timer and the UART link also +The Pico GPIO numbers used by idents start at 3 and have a gap where the Pico +uses GPIO's for particular purposes. This is the mapping between `ident` GPIO +no. and Pico PCB pin. Pins for the timer and the UART/SPI link are also identified: -| ident | GPIO | pin | -|:-----:|:----:|:----:| -| uart | 1 | 2 | -| 0 | 2 | 4 | -| 1 | 3 | 5 | -| 2 | 4 | 6 | -| 3 | 5 | 7 | -| 4 | 6 | 9 | -| 5 | 7 | 10 | -| 6 | 8 | 11 | -| 7 | 9 | 12 | -| 8 | 10 | 14 | -| 9 | 11 | 15 | -| 10 | 12 | 16 | -| 11 | 13 | 17 | -| 12 | 14 | 19 | -| 13 | 15 | 20 | -| 14 | 16 | 21 | -| 15 | 17 | 22 | -| 16 | 18 | 24 | -| 17 | 19 | 25 | -| 18 | 20 | 26 | -| 19 | 21 | 27 | -| 20 | 22 | 29 | -| 21 | 26 | 31 | -| 22 | 27 | 32 | -| timer | 28 | 34 | - -The host's UART `txd` pin should be connected to Pico GPIO 1 (pin 2). There -must be a link between `Gnd` pins on the host and Pico. +| ident | GPIO | pin | +|:-------:|:----:|:----:| +| nc/mosi | 0 | 1 | +| rxd/sck | 1 | 2 | +| nc/cs/ | 2 | 4 | +| 0 | 3 | 5 | +| 1 | 4 | 6 | +| 2 | 5 | 7 | +| 3 | 6 | 9 | +| 4 | 7 | 10 | +| 5 | 8 | 11 | +| 6 | 9 | 12 | +| 7 | 10 | 14 | +| 8 | 11 | 15 | +| 9 | 12 | 16 | +| 10 | 13 | 17 | +| 11 | 14 | 19 | +| 12 | 15 | 20 | +| 13 | 16 | 21 | +| 14 | 17 | 22 | +| 15 | 18 | 24 | +| 16 | 19 | 25 | +| 17 | 20 | 26 | +| 18 | 21 | 27 | +| 19 | 22 | 29 | +| 20 | 26 | 31 | +| 21 | 27 | 32 | +| timer | 28 | 34 | + +For a UART interface the host's UART `txd` pin should be connected to Pico GPIO +1 (pin 2). + +For SPI the host's `mosi` goes to GPIO 0 (pin 1), and `sck` to GPIO 1 (pin 2). +The host's CS Pin is connected to GPIO 2 (pin 4). + +There must be a link between `Gnd` pins on the host and Pico. # 4. The Pico code -Monitoring of the UART with default behaviour is started as follows: +Monitoring via the UART with default behaviour is started as follows: ```python from monitor_pico import run run() @@ -185,9 +221,9 @@ run() By default the Pico does not produce console output and the timer has a period of 100ms - pin 28 will pulse if ident 0 is inactive for over 100ms. These behaviours can be modified by the following `run` args: - 1. `period=100` Define the timer period in ms. + 1. `period=100` Define the hog_detect timer period in ms. 2. `verbose=()` Determines which `ident` values should produce console output. - 3. `device="uart"` Provides for future use of other interfaces. + 3. `device="uart"` Set to "spi" for an SPI interface. Thus to run such that idents 4 and 7 produce console output, with hogging reported if blocking is for more than 60ms, issue @@ -198,10 +234,12 @@ run(60, (4, 7)) # 5. Performance and design notes -The latency between a monitored coroutine starting to run and the Pico pin -going high is about 20μs. This isn't as absurd as it sounds: theoretically the -latency could be negative as the effect of the decorator is to send the -character before the coroutine starts. +Using a UART the latency between a monitored coroutine starting to run and the +Pico pin going high is about 23μs. With SPI I measured -12μs. This isn't as +absurd as it sounds: a negative latency is the effect of the decorator which +sends the character before the coroutine starts. These values are small in the +context of `uasyncio`: scheduling delays are on the order of 150μs or greater +depending on the platform. See `quick_test.py` for a way to measure latency. The use of decorators is intended to ease debugging: they are readily turned on and off by commenting out. @@ -219,11 +257,3 @@ which can be scheduled at a high rate, can't overflow the UART buffer. The This project was inspired by [this GitHub thread](https://github.com/micropython/micropython/issues/7456). - -# 6. Work in progress - -It is intended to add an option for SPI communication; `monitor.py` has a -`set_device` method which can be passed an instance of an initialised SPI -object. The Pico `run` method will be able to take a `device="spi"` arg which -will expect an SPI connection on pins 0 (sck) and 1 (data). This requires a -limited implementation of an SPI slave using the PIO, which I will do soon. diff --git a/v3/as_demos/monitor/monitor.py b/v3/as_demos/monitor/monitor.py index 606f8eb..7cc0032 100644 --- a/v3/as_demos/monitor/monitor.py +++ b/v3/as_demos/monitor/monitor.py @@ -5,23 +5,37 @@ # Released under the MIT License (MIT) - see LICENSE file import uasyncio as asyncio -from machine import UART +from machine import UART, SPI, Pin -device = None -def set_uart(n): # Monitored app defines interface - global device - device = UART(n, 1_000_000) +_write = lambda _ : print('Must run set_device') +_dummy = lambda : None # If UART do nothing. -# For future use with SPI -# Pass initialised instance of some device -def set_device(dev): - global device - device = dev +# For UART pass initialised UART. Baudrate must be 1_000_000. +# For SPI pass initialised instance SPI. Can be any baudrate, but +# must be default in other respects. +def set_device(dev, cspin=None): + global _write + global _dummy + if isinstance(dev, UART) and cspin is None: # UART + _write = dev.write + elif isinstance(dev, SPI) and isinstance(cspin, Pin): + cspin(1) + def spiwrite(data): + cspin(0) + dev.write(data) + cspin(1) + _write = spiwrite + def clear_sm(): # Set Pico SM to its initial state + cspin(1) + dev.write(b'\0') # SM is now waiting for CS low. + _dummy = clear_sm + else: + print('set_device: invalid args.') -_available = set(range(0, 23)) # Valid idents are 0..22 +_available = set(range(0, 22)) # Valid idents are 0..21 def _validate(ident, num=1): - if ident >= 0 and ident + num <= 23: + if ident >= 0 and ident + num < 22: try: for x in range(ident, ident + num): _available.remove(x) @@ -44,7 +58,7 @@ async def wrapped_coro(*args, **kwargs): instance += 1 if instance > max_instances: print(f'Monitor {n:02} max_instances reached') - device.write(v) + _write(v) try: res = await coro(*args, **kwargs) except asyncio.CancelledError: @@ -52,14 +66,17 @@ async def wrapped_coro(*args, **kwargs): finally: d |= 0x20 v = bytes(chr(d), 'utf8') - device.write(v) + _write(v) instance -= 1 return res return wrapped_coro return decorator +# If SPI, clears the state machine in case prior test resulted in the DUT +# crashing. It does this by sending a byte with CS\ False (high). def monitor_init(): - device.write(b'z') + _dummy() # Does nothing if UART + _write(b'z') # Optionally run this to show up periods of blocking behaviour @monitor(0) @@ -79,9 +96,9 @@ def decorator(func): dend = 0x60 + n vend = bytes(chr(dend), 'utf8') def wrapped_func(*args, **kwargs): - device.write(vstart) + _write(vstart) res = func(*args, **kwargs) - device.write(vend) + _write(vend) return res return wrapped_func return decorator @@ -98,9 +115,9 @@ def __init__(self, n): self.vend = bytes(chr(self.dend), 'utf8') def __enter__(self): - device.write(self.vstart) + _write(self.vstart) return self def __exit__(self, type, value, traceback): - device.write(self.vend) + _write(self.vend) return False # Don't silence exceptions diff --git a/v3/as_demos/monitor/monitor_pico.py b/v3/as_demos/monitor/monitor_pico.py index ad4c363..728dc9f 100644 --- a/v3/as_demos/monitor/monitor_pico.py +++ b/v3/as_demos/monitor/monitor_pico.py @@ -4,18 +4,55 @@ # Copyright (c) 2021 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -# UART gets a single ASCII byte defining the pin number and whether +# Device gets a single ASCII byte defining the pin number and whether # to increment (uppercase) or decrement (lowercase) the use count. # Pin goes high if use count > 0 else low. -# incoming numbers are 0..22 which map onto 23 GPIO pins +# incoming numbers are 0..21 which map onto 22 GPIO pins +import rp2 from machine import UART, Pin, Timer, freq freq(250_000_000) +# ****** SPI support ****** +@rp2.asm_pio(autopush=True, in_shiftdir=rp2.PIO.SHIFT_LEFT, push_thresh=8) +def spi_in(): + label("escape") + set(x, 0) + mov(isr, x) # Zero after DUT crash + wrap_target() + wait(1, pins, 2) # CS/ False + wait(0, pins, 2) # CS/ True + set(x, 7) + label("bit") + wait(0, pins, 1) + wait(1, pins, 1) + in_(pins, 1) + jmp(pin, "escape") # DUT crashed. On restart it sends a char with CS high. + jmp(x_dec, "bit") # Post decrement + wrap() + + +class PIOSPI: + + def __init__(self): + self._sm = rp2.StateMachine(0, spi_in, + in_shiftdir=rp2.PIO.SHIFT_LEFT, + push_thresh=8, in_base=Pin(0), + jmp_pin=Pin(2, Pin.IN, Pin.PULL_UP)) + self._sm.active(1) + + # Blocking read of 1 char. Returns ord(ch). If DUT crashes, worst case + # is where CS is left low. SM will hang until user restarts. On restart + # the app + def read(self): + return self._sm.get() & 0xff + +# ****** Define pins ****** + # Valid GPIO pins -# GP0,1 are UART 0 so pins are 2..22, 26..27 -PIN_NOS = list(range(2,23)) + list(range(26, 28)) +# GPIO 0,1,2 are for interface so pins are 3..22, 26..27 +PIN_NOS = list(range(3, 23)) + list(range(26, 28)) pin_t = Pin(28, Pin.OUT) def _cb(_): @@ -31,6 +68,7 @@ def _cb(_): for pin_no in PIN_NOS: pins.append([Pin(pin_no, Pin.OUT), 0, False]) +# ****** Monitor ****** # native reduced latency to 10μs but killed the hog detector: timer never timed out. # Also locked up Pico so ctrl-c did not interrupt. #@micropython.native @@ -39,13 +77,19 @@ def run(period=100, verbose=[], device="uart"): t_ms = period for x in verbose: pins[x][2] = True - # Provide for future devices. Must support a blocking read. + # A device must support a blocking read. if device == "uart": uart = UART(0, 1_000_000) # rx on GPIO 1 def read(): - while not uart.any(): + while not uart.any(): # Prevent UART timeouts pass return ord(uart.read(1)) + elif device == "spi": + pio = PIOSPI() + def read(): + return pio.read() + else: + raise ValueError("Unsupported device:", device) while True: if x := read(): # Get an initial 0 on UART diff --git a/v3/as_demos/monitor/monitor_test.py b/v3/as_demos/monitor/monitor_test.py index f351cc4..6a01a7e 100644 --- a/v3/as_demos/monitor/monitor_test.py +++ b/v3/as_demos/monitor/monitor_test.py @@ -1,9 +1,9 @@ # monitor_test.py import uasyncio as asyncio -from monitor import monitor, monitor_init, mon_func, mon_call, set_uart +from monitor import monitor, monitor_init, mon_func, mon_call, set_device -set_uart(2) # Define interface to use +set_device(UART(2, 1_000_000)) # UART must be 1MHz @monitor(1, 2) async def foo(t): diff --git a/v3/as_demos/monitor/quick_test.py b/v3/as_demos/monitor/quick_test.py index 74178b6..22b132a 100644 --- a/v3/as_demos/monitor/quick_test.py +++ b/v3/as_demos/monitor/quick_test.py @@ -2,10 +2,12 @@ import uasyncio as asyncio import time -from machine import Pin -from monitor import monitor, monitor_init, hog_detect, set_uart +from machine import Pin, UART, SPI +from monitor import monitor, monitor_init, hog_detect, set_device -set_uart(2) # Define interface to use +# Define interface to use +set_device(UART(2, 1_000_000)) # UART must be 1MHz +#set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz @monitor(1) async def foo(t, pin): diff --git a/v3/as_demos/rate.py b/v3/as_demos/rate.py index 3e0c301..fecd759 100644 --- a/v3/as_demos/rate.py +++ b/v3/as_demos/rate.py @@ -7,6 +7,12 @@ # 100 minimal coros are scheduled at an interval of 195μs on uasyncio V3 # Compares with ~156μs on official uasyncio V2. +# Results for 100 coros on other platforms at standard clock rate: +# Pyboard D SF2W 124μs +# Pico 481μs +# ESP32 920μs +# ESP8266 1495μs (could not run 500 or 1000 coros) + import uasyncio as asyncio num_coros = (100, 200, 500, 1000) @@ -43,4 +49,3 @@ async def report(): n, int(iterations[x]/duration), int(duration*1000000/iterations[x]))) asyncio.run(report()) - From 057ae6609512a432baf59e897dcc886f0b815340 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 28 Sep 2021 08:14:06 +0100 Subject: [PATCH 092/305] monitor: add Pico comms messages. --- v3/as_demos/monitor/README.md | 18 +++++++++++++----- v3/as_demos/monitor/monitor_pico.py | 4 +++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 9f7da9c..4b77d3f 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -28,7 +28,8 @@ and `bar` tasks. ### Breaking changes to support SPI The `set_uart` method is replaced by `set_device`. Pin mappings on the Pico -have changed. +have changed. Barring bug fixes or user suggestions I consider this project to +be complete. ## 1.1 Pre-requisites @@ -134,6 +135,9 @@ going pulse is produced on pin 28, along with the console message "Hog". The pulse can be used to trigger a scope or logic analyser. The duration of the timer may be adjusted - see [section 4](./README.md~4-the-pico-code). +Note that hog detection will be triggered if the host application terminates. +The Pico cannot determine the reason why the `hog_detect` task has stopped. + # 2. Monitoring synchronous code In general there are easier ways to debug synchronous code. However in the @@ -218,12 +222,16 @@ Monitoring via the UART with default behaviour is started as follows: from monitor_pico import run run() ``` -By default the Pico does not produce console output and the timer has a period -of 100ms - pin 28 will pulse if ident 0 is inactive for over 100ms. These -behaviours can be modified by the following `run` args: +By default the Pico does not produce console output when tasks start and end. +The timer has a period of 100ms - pin 28 will pulse if ident 0 is inactive for +over 100ms. These behaviours can be modified by the following `run` args: 1. `period=100` Define the hog_detect timer period in ms. - 2. `verbose=()` Determines which `ident` values should produce console output. + 2. `verbose=()` A list or tuple of `ident` values which should produce console + output. 3. `device="uart"` Set to "spi" for an SPI interface. + 4. `vb=True` By default the Pico issues console messages reporting on initial + communication status, repeated each time the application under test restarts. + Set `False` to disable these messages. Thus to run such that idents 4 and 7 produce console output, with hogging reported if blocking is for more than 60ms, issue diff --git a/v3/as_demos/monitor/monitor_pico.py b/v3/as_demos/monitor/monitor_pico.py index 728dc9f..2a1b65d 100644 --- a/v3/as_demos/monitor/monitor_pico.py +++ b/v3/as_demos/monitor/monitor_pico.py @@ -72,7 +72,7 @@ def _cb(_): # native reduced latency to 10μs but killed the hog detector: timer never timed out. # Also locked up Pico so ctrl-c did not interrupt. #@micropython.native -def run(period=100, verbose=[], device="uart"): +def run(period=100, verbose=(), device="uart", vb=True): global t_ms t_ms = period for x in verbose: @@ -91,9 +91,11 @@ def read(): else: raise ValueError("Unsupported device:", device) + vb and print('Awaiting communication') while True: if x := read(): # Get an initial 0 on UART if x == 0x7a: # Init: program under test has restarted + vb and print('Got communication.') for pin in pins: pin[1] = 0 continue From 928974460109f47f1918bd57028056fb43f20e34 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 30 Sep 2021 10:34:20 +0100 Subject: [PATCH 093/305] monitor_pico.py: improve hog detection. --- v3/as_demos/monitor/README.md | 56 +++++++++++++++++++++----- v3/as_demos/monitor/monitor_pico.py | 62 +++++++++++++++++++++++------ v3/as_demos/monitor/monitor_test.py | 3 ++ v3/as_demos/monitor/quick_test.py | 16 ++++++-- 4 files changed, 110 insertions(+), 27 deletions(-) diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 4b77d3f..2085904 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -4,12 +4,13 @@ This library provides a means of examining the behaviour of a running `uasyncio` system. The device under test is linked to a Raspberry Pi Pico. The latter displays the behaviour of the host by pin changes and/or optional print statements. A logic analyser or scope provides an insight into the way an -asynchronous application is working. +asynchronous application is working, although valuable informtion can be +gleaned without such tools. Communication with the Pico may be by UART or SPI, and is uni-directional from system under test to Pico. If a UART is used only one GPIO pin is used; at last -a use for the ESP8266 transmit-only UART(1). SPI requires three - mosi, sck and -cs/. +a use for the ESP8266 transmit-only UART(1). SPI requires three - `mosi`, `sck` +and `cs/`. Where an application runs multiple concurrent tasks it can be difficult to locate a task which is hogging CPU time. Long blocking periods can also result @@ -25,11 +26,14 @@ trigger because another task is hogging the CPU. Lines 01 and 03 show the `foo` and `bar` tasks. ![Image](./monitor.jpg) -### Breaking changes to support SPI +### Status -The `set_uart` method is replaced by `set_device`. Pin mappings on the Pico -have changed. Barring bug fixes or user suggestions I consider this project to -be complete. +30th Sep 2021 Pico code has improved hog detection. + +27th Sep 2021 SPI support added. The `set_uart` method is replaced by +`set_device`. Pin mappings on the Pico changed. + +21st Sep 2021 Initial release. ## 1.1 Pre-requisites @@ -133,10 +137,8 @@ To aid in detecting the gaps in execution, the Pico code implements a timer. This is retriggered by activity on `ident=0`. If it times out, a brief high going pulse is produced on pin 28, along with the console message "Hog". The pulse can be used to trigger a scope or logic analyser. The duration of the -timer may be adjusted - see [section 4](./README.md~4-the-pico-code). - -Note that hog detection will be triggered if the host application terminates. -The Pico cannot determine the reason why the `hog_detect` task has stopped. +timer may be adjusted. Other modes of hog detection are also supported. See +[section 4](./README.md~4-the-pico-code). # 2. Monitoring synchronous code @@ -239,6 +241,38 @@ reported if blocking is for more than 60ms, issue from monitor_pico import run run(60, (4, 7)) ``` +Hog reporting is as follows. If ident 0 is inactive for more than the specified +time, "Timeout" is issued. If ident 0 occurs after this, "Hog Nms" is issued +where N is the duration of the outage. If the outage is longer than the prior +maximum, "Max hog Nms" is also issued. + +This means that if the application under test terminates, throws an exception +or crashes, "Timeout" will be issued. + +## 4.1 Advanced hog detection + +The detection of rare instances of high latency is a key requirement and other +modes are available. There are two aims: providing information to users lacking +test equipment and enhancing the ability to detect infrequent cases. Modes +affect the timing of the trigger pulse and the frequency of reports. + +Modes are invoked by passing a 2-tuple as the `period` arg. + * `period[0]` The period (ms): outages shorter than this time will be ignored. + * `period[1]` is the mode: constants `SOON`, `LATE` and `MAX` are exported. + +The mode has the following effect on the trigger pulse: + * `SOON` Default behaviour: pulse occurs early at time `period[0]` ms after + the last trigger. + * `LATE` Pulse occurs when the outage ends. + * `MAX` Pulse occurs when the outage ends and its duration exceeds the prior + maximum. + +The mode also affects reporting. The effect of mode is as follows: + * `SOON` Default behaviour as described in section 4. + * `LATE` As above, but no "Timeout" message: reporting occurs at the end of an + outage only. + * `MAX` Report at end of outage but only when prior maximum exceeded. This + ensures worst-case is not missed. # 5. Performance and design notes diff --git a/v3/as_demos/monitor/monitor_pico.py b/v3/as_demos/monitor/monitor_pico.py index 2a1b65d..b16aeed 100644 --- a/v3/as_demos/monitor/monitor_pico.py +++ b/v3/as_demos/monitor/monitor_pico.py @@ -11,6 +11,7 @@ import rp2 from machine import UART, Pin, Timer, freq +from time import ticks_ms, ticks_diff freq(250_000_000) @@ -54,27 +55,43 @@ def read(self): # GPIO 0,1,2 are for interface so pins are 3..22, 26..27 PIN_NOS = list(range(3, 23)) + list(range(26, 28)) +# Index is incoming ID +# contents [Pin, instance_count, verbose] +pins = [] +for pin_no in PIN_NOS: + pins.append([Pin(pin_no, Pin.OUT), 0, False]) + +# ****** Timing ***** + pin_t = Pin(28, Pin.OUT) def _cb(_): pin_t(1) - print('Hog') + print("Timeout.") pin_t(0) tim = Timer() -t_ms = 100 -# Index is incoming ID -# contents [Pin, instance_count, verbose] -pins = [] -for pin_no in PIN_NOS: - pins.append([Pin(pin_no, Pin.OUT), 0, False]) # ****** Monitor ****** + +SOON = const(0) +LATE = const(1) +MAX = const(2) +# Modes. Pulses and reports only occur if an outage exceeds the threshold. +# SOON: pulse early when timer times out. Report at outage end. +# LATE: pulse when outage ends. Report at outage end. +# MAX: pulse when outage exceeds prior maximum. Report only in that instance. + # native reduced latency to 10μs but killed the hog detector: timer never timed out. # Also locked up Pico so ctrl-c did not interrupt. #@micropython.native def run(period=100, verbose=(), device="uart", vb=True): - global t_ms - t_ms = period + if isinstance(period, int): + t_ms = period + mode = SOON + else: + t_ms, mode = period + if mode not in (SOON, LATE, MAX): + raise ValueError('Invalid mode.') for x in verbose: pins[x][2] = True # A device must support a blocking read. @@ -92,15 +109,36 @@ def read(): raise ValueError("Unsupported device:", device) vb and print('Awaiting communication') + h_max = 0 # Max hog duration (ms) + h_start = 0 # Absolute hog start time while True: if x := read(): # Get an initial 0 on UART if x == 0x7a: # Init: program under test has restarted vb and print('Got communication.') + h_max = 0 # Restart timing + h_start = 0 for pin in pins: - pin[1] = 0 + pin[1] = 0 # Clear instance counters continue - if x == 0x40: # Retrigger hog detector. - tim.init(period=t_ms, mode=Timer.ONE_SHOT, callback=_cb) + if x == 0x40: # hog_detect task has started. + t = ticks_ms() # Arrival time + if mode == SOON: # Pulse on absence of activity + tim.init(period=t_ms, mode=Timer.ONE_SHOT, callback=_cb) + if h_start: # There was a prior trigger + dt = ticks_diff(t, h_start) + if dt > t_ms: # Delay exceeds threshold + if mode != MAX: + print(f"Hog {dt}ms") + if mode == LATE: + pin_t(1) + pin_t(0) + if dt > h_max: + h_max = dt + print(f"Max hog {dt}ms") + if mode == MAX: + pin_t(1) + pin_t(0) + h_start = t p = pins[x & 0x1f] # Key: 0x40 (ord('@')) is pin ID 0 if x & 0x20: # Going down p[1] -= 1 diff --git a/v3/as_demos/monitor/monitor_test.py b/v3/as_demos/monitor/monitor_test.py index 6a01a7e..7e1c400 100644 --- a/v3/as_demos/monitor/monitor_test.py +++ b/v3/as_demos/monitor/monitor_test.py @@ -1,5 +1,8 @@ # monitor_test.py +# Copyright (c) 2021 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + import uasyncio as asyncio from monitor import monitor, monitor_init, mon_func, mon_call, set_device diff --git a/v3/as_demos/monitor/quick_test.py b/v3/as_demos/monitor/quick_test.py index 22b132a..5e1a34c 100644 --- a/v3/as_demos/monitor/quick_test.py +++ b/v3/as_demos/monitor/quick_test.py @@ -1,5 +1,8 @@ # quick_test.py +# Copyright (c) 2021 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + import uasyncio as asyncio import time from machine import Pin, UART, SPI @@ -17,8 +20,9 @@ async def foo(t, pin): @monitor(2) async def hog(): - await asyncio.sleep(5) - time.sleep_ms(500) + while True: + await asyncio.sleep(5) + time.sleep_ms(500) @monitor(3) async def bar(t): @@ -27,7 +31,8 @@ async def bar(t): async def main(): monitor_init() - test_pin = Pin('X6', Pin.OUT) + # test_pin = Pin('X6', Pin.OUT) + test_pin = lambda _ : None # If you don't want to measure latency asyncio.create_task(hog_detect()) asyncio.create_task(hog()) # Will hog for 500ms after 5 secs while True: @@ -35,4 +40,7 @@ async def main(): await bar(150) await asyncio.sleep_ms(50) -asyncio.run(main()) +try: + asyncio.run(main()) +finally: + asyncio.new_event_loop() From 01820e81204eaa14deae6eb052d4b6be6f5f8b27 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 2 Oct 2021 16:32:26 +0100 Subject: [PATCH 094/305] monitor: Add trigger function. Improve README. --- v3/as_demos/monitor/README.md | 24 ++++++++++++++++++++---- v3/as_demos/monitor/monitor.jpg | Bin 50663 -> 90535 bytes v3/as_demos/monitor/monitor.py | 8 ++++++++ v3/as_demos/monitor/monitor_gc.jpg | Bin 0 -> 74292 bytes v3/as_demos/monitor/quick_test.py | 8 ++++---- 5 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 v3/as_demos/monitor/monitor_gc.jpg diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 2085904..8864dce 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -21,13 +21,21 @@ system state at the time of the transient event may be examined. The following image shows the `quick_test.py` code being monitored at the point when a task hogs the CPU. The top line 00 shows the "hog detect" trigger. Line -02 shows the fast running `hog_detect` task which cannot run at the time of the -trigger because another task is hogging the CPU. Lines 01 and 03 show the `foo` -and `bar` tasks. +01 shows the fast running `hog_detect` task which cannot run at the time of the +trigger because another task is hogging the CPU. Lines 02 and 04 show the `foo` +and `bar` tasks. Line 03 shows the `hog` task and line 05 is a trigger issued +by `hog()` when it starts monopolising the CPU. The Pico issues the "hog +detect" trigger 100ms after hogging starts. ![Image](./monitor.jpg) +The following image shows brief (<4ms) hogging while `quick_test.py` ran. The +likely cause is garbage collection on the Pyboard D host. +![Image](./monitor_gc.jpg) + ### Status +2nd Oct 2021 Add trigger function. + 30th Sep 2021 Pico code has improved hog detection. 27th Sep 2021 SPI support added. The `set_uart` method is replaced by @@ -145,7 +153,9 @@ timer may be adjusted. Other modes of hog detection are also supported. See In general there are easier ways to debug synchronous code. However in the context of a monitored asynchronous application there may be a need to view the timing of synchronous code. Functions and methods may be monitored either in -the declaration via a decorator or when called via a context manager. +the declaration via a decorator or when called via a context manager. Timing +markers may be inserted in code: a call to `monitor.trigger` will cause a Pico +pin to pulse. ## 2.1 The mon_func decorator @@ -173,6 +183,12 @@ with mon_call(22): It is advisable not to use the context manager with a function having the `mon_func` decorator. The pin and report behaviour is confusing. +## 2.3 The trigger timing marker + +A call to `monitor.trigger(n)` may be inserted anywhere in synchronous or +asynchronous code. When this runs, a brief (~80μs) pulse will occur on the Pico +pin with ident `n`. + # 3. Pico Pin mapping The Pico GPIO numbers used by idents start at 3 and have a gap where the Pico diff --git a/v3/as_demos/monitor/monitor.jpg b/v3/as_demos/monitor/monitor.jpg index 8a5e71e7923070e90eef71d08ca757a6a562b446..6a7f20a79b0b7c511df1cb803bbc993bdc2af52d 100644 GIT binary patch literal 90535 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

N(#hG(?d=Y`oG^5}J<7@yc8J94yR# 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 NDRb}Kzw-W?{11D$j647U literal 50663 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 literal 0 HcmV?d00001 diff --git a/v3/as_demos/monitor/quick_test.py b/v3/as_demos/monitor/quick_test.py index 5e1a34c..fa393a4 100644 --- a/v3/as_demos/monitor/quick_test.py +++ b/v3/as_demos/monitor/quick_test.py @@ -6,7 +6,7 @@ import uasyncio as asyncio import time from machine import Pin, UART, SPI -from monitor import monitor, monitor_init, hog_detect, set_device +from monitor import monitor, monitor_init, hog_detect, set_device, trigger # Define interface to use set_device(UART(2, 1_000_000)) # UART must be 1MHz @@ -20,9 +20,9 @@ async def foo(t, pin): @monitor(2) async def hog(): - while True: - await asyncio.sleep(5) - time.sleep_ms(500) + await asyncio.sleep(5) + trigger(4) # Hog start + time.sleep_ms(500) @monitor(3) async def bar(t): From cdba061a08b1b38dd3cc5a976e746259a3811c01 Mon Sep 17 00:00:00 2001 From: algestam Date: Mon, 4 Oct 2021 23:27:59 +0200 Subject: [PATCH 095/305] apoll.py Minor doc fix --- v2/apoll.py | 2 +- v3/as_demos/apoll.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/v2/apoll.py b/v2/apoll.py index eeff59a..9639a2c 100644 --- a/v2/apoll.py +++ b/v2/apoll.py @@ -1,4 +1,4 @@ -# Demonstration of a device driver using a coroutine to poll a dvice. +# Demonstration of a device driver using a coroutine to poll a device. # Runs on Pyboard: displays results from the onboard accelerometer. # Uses crude filtering to discard noisy data. diff --git a/v3/as_demos/apoll.py b/v3/as_demos/apoll.py index 54d218f..40c4233 100644 --- a/v3/as_demos/apoll.py +++ b/v3/as_demos/apoll.py @@ -1,4 +1,4 @@ -# Demonstration of a device driver using a coroutine to poll a dvice. +# Demonstration of a device driver using a coroutine to poll a device. # Runs on Pyboard: displays results from the onboard accelerometer. # Uses crude filtering to discard noisy data. From d6b1e05d8222466c0422582640a58bdd93f44bae Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 6 Oct 2021 12:03:51 +0100 Subject: [PATCH 096/305] monitor: New release with multiple changes. --- v3/as_demos/monitor/README.md | 224 ++++++++++++++---- v3/as_demos/monitor/monitor.py | 66 ++++-- v3/as_demos/monitor/monitor_gc.jpg | Bin 74292 -> 68891 bytes v3/as_demos/monitor/monitor_test.py | 59 ----- v3/as_demos/monitor/tests/full_test.jpg | Bin 0 -> 69423 bytes v3/as_demos/monitor/tests/full_test.py | 47 ++++ v3/as_demos/monitor/tests/latency.jpg | Bin 0 -> 76931 bytes v3/as_demos/monitor/tests/latency.py | 36 +++ v3/as_demos/monitor/{ => tests}/quick_test.py | 30 ++- v3/as_demos/monitor/tests/syn_test.jpg | Bin 0 -> 78274 bytes v3/as_demos/monitor/tests/syn_test.py | 55 +++++ 11 files changed, 367 insertions(+), 150 deletions(-) delete mode 100644 v3/as_demos/monitor/monitor_test.py create mode 100644 v3/as_demos/monitor/tests/full_test.jpg create mode 100644 v3/as_demos/monitor/tests/full_test.py create mode 100644 v3/as_demos/monitor/tests/latency.jpg create mode 100644 v3/as_demos/monitor/tests/latency.py rename v3/as_demos/monitor/{ => tests}/quick_test.py (51%) create mode 100644 v3/as_demos/monitor/tests/syn_test.jpg create mode 100644 v3/as_demos/monitor/tests/syn_test.py diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 8864dce..63dd2ff 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -4,20 +4,19 @@ This library provides a means of examining the behaviour of a running `uasyncio` system. The device under test is linked to a Raspberry Pi Pico. The latter displays the behaviour of the host by pin changes and/or optional print statements. A logic analyser or scope provides an insight into the way an -asynchronous application is working, although valuable informtion can be -gleaned without such tools. +asynchronous application is working; valuable informtion can also be gleaned at +the Pico command line. Communication with the Pico may be by UART or SPI, and is uni-directional from -system under test to Pico. If a UART is used only one GPIO pin is used; at last -a use for the ESP8266 transmit-only UART(1). SPI requires three - `mosi`, `sck` -and `cs/`. +system under test to Pico. If a UART is used only one GPIO pin is used. SPI +requires three - `mosi`, `sck` and `cs/`. Where an application runs multiple concurrent tasks it can be difficult to -locate a task which is hogging CPU time. Long blocking periods can also result -from several tasks each of which can block for a period. If, on occasion, these -are scheduled in succession, the times can add. The monitor issues a trigger -pulse when the blocking period exceeds a threshold. With a logic analyser the -system state at the time of the transient event may be examined. +identify a task which is hogging CPU time. Long blocking periods can also occur +when several tasks each block for a period. If, on occasion, these are +scheduled in succession, the times will add. The monitor issues a trigger pulse +when the blocking period exceeds a threshold. With a logic analyser the system +state at the time of the transient event may be examined. The following image shows the `quick_test.py` code being monitored at the point when a task hogs the CPU. The top line 00 shows the "hog detect" trigger. Line @@ -29,27 +28,45 @@ detect" trigger 100ms after hogging starts. ![Image](./monitor.jpg) The following image shows brief (<4ms) hogging while `quick_test.py` ran. The -likely cause is garbage collection on the Pyboard D host. +likely cause is garbage collection on the Pyboard D host. The monitor was able +to demostrate that this never exceeded 5ms. ![Image](./monitor_gc.jpg) ### Status -2nd Oct 2021 Add trigger function. - -30th Sep 2021 Pico code has improved hog detection. - -27th Sep 2021 SPI support added. The `set_uart` method is replaced by -`set_device`. Pin mappings on the Pico changed. - -21st Sep 2021 Initial release. +4th Oct 2021 Please regard this as "all new". Many functions have been renamed, +error checking has been improved and code made more efficient. ## 1.1 Pre-requisites The device being monitored must run firmware V1.17 or later. The `uasyncio` -version should be V3 (included in the firmware). +version should be V3 (included in the firmware). The file `monitor.py` should +be copied to the target, and `monitor_pico` to the Pico. ## 1.2 Usage +A minimal example of a UART-monitored application looks like this: +```python +import uasyncio as asyncio +from machine import UART # Using a UART for monitoring +import monitor +monitor.set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz. + +@monitor.asyn(1) # Assign ident 1 to foo (GPIO 4) +async def foo(): + await asyncio.sleep_ms(100) + +async def main(): + monitor.init() # Initialise Pico state at the start of every run + while True: + await foo() # Pico GPIO4 will go high for duration + await asyncio.sleep_ms(100) + +try: + asyncio.run(main()) +finally: + asyncio.new_event_loop() +``` Example script `quick_test.py` provides a usage example. It may be adapted to use a UART or SPI interface: see commented-out code. @@ -63,14 +80,14 @@ device. The Pico must be set up to match the interface chosen on the host: see In the case of a UART an initialised UART with 1MHz baudrate is passed: ```python from machine import UART -from monitor import monitor, monitor_init, hog_detect, set_device -set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz. +import monitor +monitor.set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz. ``` In the case of SPI initialised SPI and cs/ Pin instances are passed: ```python from machine import Pin, SPI -from monitor import monitor, monitor_init, hog_detect, set_device -set_device(SPI(2, baudrate=5_000_000), Pin('X6', Pin.OUT)) # Device under test SPI +import monitor +monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X6', Pin.OUT)) # Device under test SPI ``` The SPI instance must have default args; the one exception being baudrate which may be any value. I have tested up to 30MHz but there is no benefit in running @@ -81,18 +98,19 @@ bus with other devices, although I haven't tested this. On startup, after defining the interface, an application should issue: ```python -monitor_init() +monitor.init() ``` -Coroutines to be monitored are prefixed with the `@monitor` decorator: +Coroutines to be monitored are prefixed with the `@monitor.asyn` decorator: ```python -@monitor(2, 3) +@monitor.asyn(2, 3) async def my_coro(): # code ``` -The decorator args are as follows: - 1. A unique `ident` for the code being monitored. Determines the pin number on - the Pico. See [Pico Pin mapping](./README.md#3-pico-pin-mapping). - 2. An optional arg defining the maximum number of concurrent instances of the +The decorator positional args are as follows: + 1. `n` A unique `ident` in range `0 <= ident <= 21` for the code being + monitored. Determines the pin number on the Pico. See + [Pico Pin mapping](./README.md#3-pico-pin-mapping). + 2. `max_instances=1` Defines the maximum number of concurrent instances of the task to be independently monitored (default 1). Whenever the coroutine runs, a pin on the Pico will go high, and when the code @@ -122,7 +140,7 @@ tasks from running. Determining the task responsible can be difficult. The pin state only indicates that the task is running. A pin state of 1 does not imply CPU hogging. Thus ```python -@monitor(3) +@monitor.asyn(3) async def long_time(): await asyncio.sleep(30) ``` @@ -136,9 +154,9 @@ long gaps appear in the pulses on GPIO 3, other tasks are hogging the CPU. Usage of this is optional. To use, issue ```python import uasyncio as asyncio -from monitor import monitor, hog_detect +import monitor # code omitted -asyncio.create_task(hog_detect()) +asyncio.create_task(monitor.hog_detect()) # code omitted ``` To aid in detecting the gaps in execution, the Pico code implements a timer. @@ -148,35 +166,53 @@ pulse can be used to trigger a scope or logic analyser. The duration of the timer may be adjusted. Other modes of hog detection are also supported. See [section 4](./README.md~4-the-pico-code). +## 1.4 Validation of idents + +Re-using idents would lead to confusing behaviour. A `ValueError` is thrown if +an ident is out of range or is assigned to more than one coroutine. + # 2. Monitoring synchronous code -In general there are easier ways to debug synchronous code. However in the -context of a monitored asynchronous application there may be a need to view the -timing of synchronous code. Functions and methods may be monitored either in -the declaration via a decorator or when called via a context manager. Timing -markers may be inserted in code: a call to `monitor.trigger` will cause a Pico -pin to pulse. +In the context of an asynchronous application there may be a need to view the +timing of synchronous code, or simply to create a trigger pulse at a known +point in the code. The following are provided: + * A `sync` decorator for synchronous functions or methods: like `async` it + monitors every call to the function. + * A `trigger` function which issues a brief pulse on the Pico. + * A `mon_call` context manager enables function monitoring to be restricted to + specific calls. + +Idents used by `trigger` or `mon_call` must be reserved: this is because these +may occur in a looping construct. This enables the validation to protect +against inadvertent multiple usage of an ident. The `monitor.reserve()` +function can reserve one or more idents: +```python +monitor.reserve(4, 9, 10) +``` -## 2.1 The mon_func decorator +## 2.1 The sync decorator -This works as per the asynchronous decorator, but without the `max_instances` -arg. This will activate the GPIO associated with ident 20 for the duration of -every call to `sync_func()`: +This works as per the `@async` decorator, but with no `max_instances` arg. This +will activate GPIO 26 (associated with ident 20) for the duration of every call +to `sync_func()`: ```python -@mon_func(20) +@monitor.sync(20) def sync_func(): pass ``` +Note that the ident must not be reserved. ## 2.2 The mon_call context manager This may be used to monitor a function only when called from specific points in the code. ```python +monitor.reserve(22) + def another_sync_func(): pass -with mon_call(22): +with monitor.mon_call(22): another_sync_func() ``` @@ -187,7 +223,13 @@ It is advisable not to use the context manager with a function having the A call to `monitor.trigger(n)` may be inserted anywhere in synchronous or asynchronous code. When this runs, a brief (~80μs) pulse will occur on the Pico -pin with ident `n`. +pin with ident `n`. As per `mon_call`, ident `n` must be reserved. +```python +monitor.reserve(10) + +def foo(): + monitor.trigger(10) # Pulse ident 10, GPIO 13 +``` # 3. Pico Pin mapping @@ -290,20 +332,61 @@ The mode also affects reporting. The effect of mode is as follows: * `MAX` Report at end of outage but only when prior maximum exceeded. This ensures worst-case is not missed. -# 5. Performance and design notes +Running the following produce instructive console output: +```python +from monitor_pico import run, MAX +run((1, MAX)) +``` + +# 5. Test and demo scripts + +`quick_test.py` Primarily tests deliberate CPU hogging. Discussed in section 1. + +`full_test.py` Tests task timeout and cancellation, also the handling of +multiple task instances. If the Pico is run with `run((1, MAX))` it reveals +the maximum time the host hogs the CPU. On a Pyboard D I measured 5ms. + +The sequence here is a trigger is issued on ident 4. The task on ident 1 is +started, but times out after 100ms. 100ms later, five instances of the task on +ident 1 are started, at 100ms intervals. They are then cancelled at 100ms +intervals. Because 3 idents are allocated for multiple instances, these show up +on idents 1, 2, and 3 with ident 3 representing 3 instances. Ident 3 therefore +only goes low when the last of these three instances is cancelled. + +![Image](./tests/full_test.jpg) + +`latency.py` Measures latency between the start of a monitored task and the +Pico pin going high. The sequence below is first the task pulses a pin (ident +6). Then the Pico pin monitoring the task goes high (ident 1 after ~20μs). Then +the trigger on ident 2 occurs 112μs after the pin pulse. + +![Image](./tests/latency.jpg) + +`syn_test.py` Demonstrates two instances of a bound method along with the ways +of monitoring synchronous code. The trigger on ident 5 marks the start of the +sequence. The `foo1.pause` method on ident 1 starts and runs `foo1.wait1` on +ident 3. 100ms after this ends, `foo`.wait2` on ident 4 is triggered. 100ms +after this ends, `foo1.pause` on ident 1 ends. The second instance of `.pause` +(`foo2.pause`) on ident 2 repeats this sequence shifted by 50ms. The 10ms gaps +in `hog_detect` show the periods of deliberate CPU hogging. + +![Image](./tests/syn_test.jpg) + +# 6. Performance and design notes Using a UART the latency between a monitored coroutine starting to run and the Pico pin going high is about 23μs. With SPI I measured -12μs. This isn't as absurd as it sounds: a negative latency is the effect of the decorator which sends the character before the coroutine starts. These values are small in the context of `uasyncio`: scheduling delays are on the order of 150μs or greater -depending on the platform. See `quick_test.py` for a way to measure latency. +depending on the platform. See `tests/latency.py` for a way to measure latency. -The use of decorators is intended to ease debugging: they are readily turned on -and off by commenting out. +The use of decorators eases debugging: they are readily turned on and off by +commenting out. The Pico was chosen for extremely low cost. It has plenty of GPIO pins and no -underlying OS to introduce timing uncertainties. +underlying OS to introduce timing uncertainties. The PIO enables a simple SPI +slave. Symbols transmitted by the UART are printable ASCII characters to ease debugging. A single byte protocol simplifies and speeds the Pico code. @@ -313,5 +396,42 @@ fast in the context of uasyncio). It also ensures that tasks like `hog_detect`, which can be scheduled at a high rate, can't overflow the UART buffer. The 1Mbps rate seems widely supported. +## 6.1 How it works + +This is for anyone wanting to modify the code. Each ident is associated with +two bytes, `0x40 + ident` and `0x60 + ident`. These are upper and lower case +printable ASCII characters (aside from ident 0 which is `@` and the backtick +character). When an ident becomes active (e.g. at the start of a coroutine), +uppercase is transmitted, when it becomes inactive lowercase is sent. + +The Pico maintains a list `pins` indexed by `ident`. Each entry is a 3-list +comprising: + * The `Pin` object associated with that ident. + * An instance counter. + * A `verbose` boolean defaulting `False`. + +When a character arrives, the `ident` value is recovered. If it is uppercase +the pin goes high and the instance count is incremented. If it is lowercase the +instance count is decremented: if it becomes 0 the pin goes low. + +The `init` function on the host sends `b"z"` to the Pico. This clears down the +instance counters (the program under test may have previously failed, leaving +instance counters non-zero). The Pico also clears variables used to measure +hogging. In the case of SPI communication, before sending the `b"z"`, a 0 +character is sent with `cs/` high. The Pico implements a basic SPI slave using +the PIO. This may have been left in an invalid state by a crashing host. It is +designed to reset to a known state if it receives a character with `cs/` high. + +The ident `@` (0x40) is assumed to be used by the `hog_detect()` function. When +the Pico receives it, processing occurs to aid in hog detection and creating a +trigger on GPIO28. Behaviour depends on the mode passed to the `run()` command. +In the following, `thresh` is the time passed to `run()` in `period[0]`. + * `SOON` This retriggers a timer with period `thresh`. Timeout causes a + trigger. + * `LATE` Trigger occurs if the period since the last `@` exceeds `thresh`. + * `MAX` Trigger occurs if period exceeds `thresh` and also exceeds the prior + maximum. + This project was inspired by [this GitHub thread](https://github.com/micropython/micropython/issues/7456). + diff --git a/v3/as_demos/monitor/monitor.py b/v3/as_demos/monitor/monitor.py index 33785e8..611d4f2 100644 --- a/v3/as_demos/monitor/monitor.py +++ b/v3/as_demos/monitor/monitor.py @@ -7,8 +7,14 @@ import uasyncio as asyncio from machine import UART, SPI, Pin from time import sleep_us +from sys import exit -_write = lambda _ : print('Must run set_device') +# Quit with an error message rather than throw. +def _quit(s): + print("Monitor " + s) + exit(0) + +_write = lambda _ : _quit("must run set_device") _dummy = lambda : None # If UART do nothing. # For UART pass initialised UART. Baudrate must be 1_000_000. @@ -28,12 +34,15 @@ def spiwrite(data): _write = spiwrite def clear_sm(): # Set Pico SM to its initial state cspin(1) - dev.write(b'\0') # SM is now waiting for CS low. + dev.write(b"\0") # SM is now waiting for CS low. _dummy = clear_sm else: - print('set_device: invalid args.') + _quit("set_device: invalid args.") +# Justification for validation even when decorating a method +# /mnt/qnap2/data/Projects/Python/AssortedTechniques/decorators _available = set(range(0, 22)) # Valid idents are 0..21 +_reserved = set() # Idents reserved for synchronous monitoring def _validate(ident, num=1): if ident >= 0 and ident + num < 22: @@ -41,12 +50,23 @@ def _validate(ident, num=1): for x in range(ident, ident + num): _available.remove(x) except KeyError: - raise ValueError(f'Monitor error - ident {x:02} already allocated.') + _quit(f"error - ident {x:02} already allocated.") else: - raise ValueError(f'Monitor error - ident {ident:02} out of range.') + _quit(f"error - ident {ident:02} out of range.") + +# Reserve ID's to be used for synchronous monitoring +def reserve(*ids): + for ident in ids: + _validate(ident) + _reserved.add(ident) +# Check whether a synchronous ident was reserved +def _check(ident): + if ident not in _reserved: + _quit(f"error: synchronous ident {ident:02} was not reserved.") -def monitor(n, max_instances=1): +# asynchronous monitor +def asyn(n, max_instances=1): def decorator(coro): # This code runs before asyncio.run() _validate(n, max_instances) @@ -55,10 +75,10 @@ async def wrapped_coro(*args, **kwargs): # realtime nonlocal instance d = 0x40 + n + min(instance, max_instances - 1) - v = bytes(chr(d), 'utf8') + v = int.to_bytes(d, 1, "big") instance += 1 - if instance > max_instances: - print(f'Monitor {n:02} max_instances reached') + if instance > max_instances: # Warning only + print(f"Monitor {n:02} max_instances reached") _write(v) try: res = await coro(*args, **kwargs) @@ -66,7 +86,7 @@ async def wrapped_coro(*args, **kwargs): raise finally: d |= 0x20 - v = bytes(chr(d), 'utf8') + v = int.to_bytes(d, 1, "big") _write(v) instance -= 1 return res @@ -75,12 +95,12 @@ async def wrapped_coro(*args, **kwargs): # If SPI, clears the state machine in case prior test resulted in the DUT # crashing. It does this by sending a byte with CS\ False (high). -def monitor_init(): +def init(): _dummy() # Does nothing if UART - _write(b'z') + _write(b"z") # Clear Pico's instance counters etc. # Optionally run this to show up periods of blocking behaviour -@monitor(0) +@asyn(0) async def _do_nowt(): await asyncio.sleep_ms(0) @@ -89,13 +109,13 @@ async def hog_detect(): await _do_nowt() # Monitor a synchronous function definition -def mon_func(n): +def sync(n): def decorator(func): _validate(n) dstart = 0x40 + n - vstart = bytes(chr(dstart), 'utf8') + vstart = int.to_bytes(dstart, 1, "big") dend = 0x60 + n - vend = bytes(chr(dend), 'utf8') + vend = int.to_bytes(dend, 1, "big") def wrapped_func(*args, **kwargs): _write(vstart) res = func(*args, **kwargs) @@ -104,16 +124,16 @@ def wrapped_func(*args, **kwargs): return wrapped_func return decorator - +# Runtime monitoring: can't validate because code may be looping. # Monitor a synchronous function call class mon_call: def __init__(self, n): - _validate(n) + _check(n) self.n = n self.dstart = 0x40 + n - self.vstart = bytes(chr(self.dstart), 'utf8') + self.vstart = int.to_bytes(self.dstart, 1, "big") self.dend = 0x60 + n - self.vend = bytes(chr(self.dend), 'utf8') + self.vend = int.to_bytes(self.dend, 1, "big") def __enter__(self): _write(self.vstart) @@ -125,7 +145,7 @@ def __exit__(self, type, value, traceback): # Cause pico ident n to produce a brief (~80μs) pulse def trigger(n): - _validate(n) - _write(bytes(chr(0x40 + n), 'utf8')) + _check(n) + _write(int.to_bytes(0x40 + n, 1, "big")) sleep_us(20) - _write(bytes(chr(0x60 + n), 'utf8')) + _write(int.to_bytes(0x60 + n, 1, "big")) diff --git a/v3/as_demos/monitor/monitor_gc.jpg b/v3/as_demos/monitor/monitor_gc.jpg index 89697e2f2532a4678ceb7dbb4f3031ef8d639119..3e667657af6d8f57afe2ba105e96ae3fcbbd6fba 100644 GIT binary patch literal 68891 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 literal 74292 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 diff --git a/v3/as_demos/monitor/monitor_test.py b/v3/as_demos/monitor/monitor_test.py deleted file mode 100644 index 7e1c400..0000000 --- a/v3/as_demos/monitor/monitor_test.py +++ /dev/null @@ -1,59 +0,0 @@ -# monitor_test.py - -# Copyright (c) 2021 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -import uasyncio as asyncio -from monitor import monitor, monitor_init, mon_func, mon_call, set_device - -set_device(UART(2, 1_000_000)) # UART must be 1MHz - -@monitor(1, 2) -async def foo(t): - await asyncio.sleep_ms(t) - return t * 2 - -@monitor(3) -async def bar(t): - await asyncio.sleep_ms(t) - return t * 2 - -@monitor(4) -async def forever(): - while True: - await asyncio.sleep(1) - -class Foo: - def __init__(self): - pass - @monitor(5, 1) - async def rats(self): - await asyncio.sleep(1) - print('rats ran') - -@mon_func(20) -def sync_func(): - pass - -def another_sync_func(): - pass - -async def main(): - monitor_init() - sync_func() - with mon_call(22): - another_sync_func() - while True: - myfoo = Foo() - asyncio.create_task(myfoo.rats()) - ft = asyncio.create_task(foo(1000)) - bt = asyncio.create_task(bar(200)) - print('bar', await bt) - ft.cancel() - print('got', await foo(2000)) - try: - await asyncio.wait_for(forever(), 3) - except asyncio.TimeoutError: # Mandatory error trapping - print('got timeout') # Caller sees TimeoutError - -asyncio.run(main()) diff --git a/v3/as_demos/monitor/tests/full_test.jpg b/v3/as_demos/monitor/tests/full_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..95ed14a9618cddc92d5f64de9c9697746115f335 GIT binary patch literal 69423 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 E0bEcmMF0Q* literal 0 HcmV?d00001 diff --git a/v3/as_demos/monitor/tests/full_test.py b/v3/as_demos/monitor/tests/full_test.py new file mode 100644 index 0000000..45ac9e3 --- /dev/null +++ b/v3/as_demos/monitor/tests/full_test.py @@ -0,0 +1,47 @@ +# full_test.py + +# Copyright (c) 2021 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +# Tests monitoring of timeout, task cancellation and multiple instances. + +import uasyncio as asyncio +from machine import Pin, UART, SPI +import monitor + +monitor.reserve(4) +# Define interface to use +monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz +#monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz + +@monitor.asyn(1, 3) +async def forever(): + while True: + await asyncio.sleep_ms(100) + + +async def main(): + monitor.init() + asyncio.create_task(monitor.hog_detect()) # Watch for gc dropouts on ID0 + while True: + monitor.trigger(4) + try: + await asyncio.wait_for_ms(forever(), 100) # 100ms pulse on ID1 + except asyncio.TimeoutError: # Mandatory error trapping + pass + # Task has now timed out + await asyncio.sleep_ms(100) + tasks = [] + for _ in range(5): # ID 1, 2, 3 go high, then 500ms pause + tasks.append(asyncio.create_task(forever())) + await asyncio.sleep_ms(100) + while tasks: # ID 3, 2, 1 go low + tasks.pop().cancel() + await asyncio.sleep_ms(100) + await asyncio.sleep_ms(100) + + +try: + asyncio.run(main()) +finally: + asyncio.new_event_loop() diff --git a/v3/as_demos/monitor/tests/latency.jpg b/v3/as_demos/monitor/tests/latency.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4cdea2c8564b2d3fe101d65db9b3d75ed2662090 GIT binary patch literal 76931 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

lj})I9Ps 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 z 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 1MHz + +@monitor.asyn(1) +async def pulse(pin): + pin(1) # Pulse pin + pin(0) + monitor.trigger(2) # Pulse Pico pin ident 2 + await asyncio.sleep_ms(30) + +async def main(): + monitor.init() + while True: + await pulse(test_pin) + await asyncio.sleep_ms(100) + +try: + asyncio.run(main()) +finally: + asyncio.new_event_loop() diff --git a/v3/as_demos/monitor/quick_test.py b/v3/as_demos/monitor/tests/quick_test.py similarity index 51% rename from v3/as_demos/monitor/quick_test.py rename to v3/as_demos/monitor/tests/quick_test.py index fa393a4..7d31d3b 100644 --- a/v3/as_demos/monitor/quick_test.py +++ b/v3/as_demos/monitor/tests/quick_test.py @@ -1,4 +1,5 @@ # quick_test.py +# Tests the monitoring of deliberate CPU hgging. # Copyright (c) 2021 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file @@ -6,37 +7,34 @@ import uasyncio as asyncio import time from machine import Pin, UART, SPI -from monitor import monitor, monitor_init, hog_detect, set_device, trigger +import monitor # Define interface to use -set_device(UART(2, 1_000_000)) # UART must be 1MHz -#set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz +monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz +# monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz -@monitor(1) -async def foo(t, pin): - pin(1) # Measure latency - pin(0) +monitor.reserve(4) # ident for trigger + +@monitor.asyn(1) +async def foo(t): await asyncio.sleep_ms(t) -@monitor(2) +@monitor.asyn(2) async def hog(): await asyncio.sleep(5) - trigger(4) # Hog start + monitor.trigger(4) # Hog start time.sleep_ms(500) -@monitor(3) +@monitor.asyn(3) async def bar(t): await asyncio.sleep_ms(t) - async def main(): - monitor_init() - # test_pin = Pin('X6', Pin.OUT) - test_pin = lambda _ : None # If you don't want to measure latency - asyncio.create_task(hog_detect()) + monitor.init() + asyncio.create_task(monitor.hog_detect()) asyncio.create_task(hog()) # Will hog for 500ms after 5 secs while True: - asyncio.create_task(foo(100, test_pin)) + asyncio.create_task(foo(100)) await bar(150) await asyncio.sleep_ms(50) diff --git a/v3/as_demos/monitor/tests/syn_test.jpg b/v3/as_demos/monitor/tests/syn_test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..783940d277f36500164dbafb0b5088ffea5b8a81 GIT binary patch literal 78274 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%0DaYGMg`{Tu90P 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 literal 0 HcmV?d00001 diff --git a/v3/as_demos/monitor/tests/syn_test.py b/v3/as_demos/monitor/tests/syn_test.py new file mode 100644 index 0000000..8e75b07 --- /dev/null +++ b/v3/as_demos/monitor/tests/syn_test.py @@ -0,0 +1,55 @@ +# syn_test.py +# Tests the monitoring synchronous code and of an async method. + +# Copyright (c) 2021 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +import time +from machine import Pin, UART, SPI +import monitor + +# Define interface to use +monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz +# monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz + +monitor.reserve(4, 5) # Reserve trigger and mon_call idents only + + +class Foo: + def __init__(self): + pass + + @monitor.asyn(1, 2) # ident 1/2 high + async def pause(self): + self.wait1() # ident 3 10ms pulse + await asyncio.sleep_ms(100) + with monitor.mon_call(4): # ident 4 10ms pulse + self.wait2() + await asyncio.sleep_ms(100) + # ident 1/2 low + + @monitor.sync(3) # Decorator so ident not reserved + def wait1(self): + time.sleep_ms(10) + + def wait2(self): + time.sleep_ms(10) + +async def main(): + monitor.init() + asyncio.create_task(monitor.hog_detect()) # Make 10ms waitx gaps visible + foo1 = Foo() + foo2 = Foo() + while True: + monitor.trigger(5) # Mark start with pulse on ident 5 + # Create two instances of .pause separated by 50ms + asyncio.create_task(foo1.pause()) + await asyncio.sleep_ms(50) + await foo2.pause() + await asyncio.sleep_ms(50) + +try: + asyncio.run(main()) +finally: + asyncio.new_event_loop() From 8282ff159c7403889f604557876031d7233f8e18 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 7 Oct 2021 09:51:41 +0100 Subject: [PATCH 097/305] v3/README.md add monitor image. --- v3/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/v3/README.md b/v3/README.md index cfde5a6..2b0949f 100644 --- a/v3/README.md +++ b/v3/README.md @@ -31,12 +31,6 @@ This [lightweight scheduler](./docs/SCHEDULE.md) enables tasks to be scheduled at future times. These can be assigned in a flexible way: a task might run at 4.10am on Monday and Friday if there's no "r" in the month. -### A monitor - -This [monitor](./as_demos/monitor/README.md) enables a running `uasyncio` -application to be monitored using a Pi Pico, ideally with a scope or logic -analyser. - ### Asynchronous device drivers These device drivers are intended as examples of asynchronous code which are @@ -52,6 +46,14 @@ useful in their own right: * [HD44780](./docs/hd44780.md) Driver for common character based LCD displays based on the Hitachi HD44780 controller. +### A monitor + +This [monitor](./as_demos/monitor/README.md) enables a running `uasyncio` +application to be monitored using a Pi Pico, ideally with a scope or logic +analyser. + +![Image](./as_demos/monitor/tests/syn_test.jpg) + # 2. V3 Overview These notes are intended for users familiar with `asyncio` under CPython. From 405ce3da8c428860f8168b07379bf66db8907e33 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 8 Oct 2021 14:47:34 +0100 Subject: [PATCH 098/305] monitor.py: Remove f-strings for ESP8266 compatibility. --- v3/as_demos/monitor/monitor.py | 8 ++++---- v3/as_demos/monitor/monitor_pico.py | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/v3/as_demos/monitor/monitor.py b/v3/as_demos/monitor/monitor.py index 611d4f2..a9ca587 100644 --- a/v3/as_demos/monitor/monitor.py +++ b/v3/as_demos/monitor/monitor.py @@ -50,9 +50,9 @@ def _validate(ident, num=1): for x in range(ident, ident + num): _available.remove(x) except KeyError: - _quit(f"error - ident {x:02} already allocated.") + _quit("error - ident {:02d} already allocated.".format(x)) else: - _quit(f"error - ident {ident:02} out of range.") + _quit("error - ident {:02d} out of range.".format(ident)) # Reserve ID's to be used for synchronous monitoring def reserve(*ids): @@ -63,7 +63,7 @@ def reserve(*ids): # Check whether a synchronous ident was reserved def _check(ident): if ident not in _reserved: - _quit(f"error: synchronous ident {ident:02} was not reserved.") + _quit("error: synchronous ident {:02d} was not reserved.".format(ident)) # asynchronous monitor def asyn(n, max_instances=1): @@ -78,7 +78,7 @@ async def wrapped_coro(*args, **kwargs): v = int.to_bytes(d, 1, "big") instance += 1 if instance > max_instances: # Warning only - print(f"Monitor {n:02} max_instances reached") + print("Monitor {:02d} max_instances reached.".format(n)) _write(v) try: res = await coro(*args, **kwargs) diff --git a/v3/as_demos/monitor/monitor_pico.py b/v3/as_demos/monitor/monitor_pico.py index b16aeed..9c5e740 100644 --- a/v3/as_demos/monitor/monitor_pico.py +++ b/v3/as_demos/monitor/monitor_pico.py @@ -118,7 +118,8 @@ def read(): h_max = 0 # Restart timing h_start = 0 for pin in pins: - pin[1] = 0 # Clear instance counters + pin[0](0) # Clear pin + pin[1] = 0 # and instance counter continue if x == 0x40: # hog_detect task has started. t = ticks_ms() # Arrival time From e15e4edcd79654c48f874b50aa5081f6e8d5734d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 9 Oct 2021 15:58:15 +0100 Subject: [PATCH 099/305] monitor README.md: Add note re ESP8266 UART(1). --- v3/as_demos/monitor/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 63dd2ff..d592755 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -396,7 +396,19 @@ fast in the context of uasyncio). It also ensures that tasks like `hog_detect`, which can be scheduled at a high rate, can't overflow the UART buffer. The 1Mbps rate seems widely supported. -## 6.1 How it works +## 6.1 ESP8266 note + +tl;dr ESP8266 applications can be monitored using the transmit-only UART 1. + +I was expecting problems: on boot the ESP8266 transmits data on both UARTs at +75Kbaud. A bit at this baudrate corresponds to 13.3 bits at 1Mbaud. A receiving +UART will see a transmitted 1 as 13 consecutive 1 bits. Lacking a start bit, it +will ignore them. An incoming 0 will be interpreted as a framing error because +of the absence of a stop bit. In practice the Pico UART returns `b'\x00'` when +this occurs, which `monitor.py` ignores. When monitored the ESP8266 behaves +identically to other platforms and can be rebooted at will. + +## 6.2 How it works This is for anyone wanting to modify the code. Each ident is associated with two bytes, `0x40 + ident` and `0x60 + ident`. These are upper and lower case From fc40d0d09863b2203ce3886f60888041d2e748e9 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 9 Oct 2021 17:16:51 +0100 Subject: [PATCH 100/305] monitor: Minor corrections/improvements to README.md --- v3/as_demos/monitor/README.md | 76 +++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index d592755..4df89cd 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -1,7 +1,7 @@ # 1. A uasyncio monitor This library provides a means of examining the behaviour of a running -`uasyncio` system. The device under test is linked to a Raspberry Pi Pico. The +`uasyncio` system. The device under test is linked to a Raspberry Pico. The latter displays the behaviour of the host by pin changes and/or optional print statements. A logic analyser or scope provides an insight into the way an asynchronous application is working; valuable informtion can also be gleaned at @@ -25,11 +25,13 @@ trigger because another task is hogging the CPU. Lines 02 and 04 show the `foo` and `bar` tasks. Line 03 shows the `hog` task and line 05 is a trigger issued by `hog()` when it starts monopolising the CPU. The Pico issues the "hog detect" trigger 100ms after hogging starts. + ![Image](./monitor.jpg) The following image shows brief (<4ms) hogging while `quick_test.py` ran. The likely cause is garbage collection on the Pyboard D host. The monitor was able to demostrate that this never exceeded 5ms. + ![Image](./monitor_gc.jpg) ### Status @@ -43,9 +45,15 @@ The device being monitored must run firmware V1.17 or later. The `uasyncio` version should be V3 (included in the firmware). The file `monitor.py` should be copied to the target, and `monitor_pico` to the Pico. -## 1.2 Usage +## 1.2 Quick start guide -A minimal example of a UART-monitored application looks like this: +For UART based monitoring, ensure that the host and Pico `gnd` pins are linked. +Connect the host's `txd` to the Pico pin 2 (UART(0) `rxd`). On the Pico issue: +```python +from monitor_pico import run +run() +``` +Adapt the following to match the UART to be used on the host and run it. ```python import uasyncio as asyncio from machine import UART # Using a UART for monitoring @@ -67,6 +75,8 @@ try: finally: asyncio.new_event_loop() ``` +A square wave of period 200ms should be observed on Pico GPIO 4 (pin 6). + Example script `quick_test.py` provides a usage example. It may be adapted to use a UART or SPI interface: see commented-out code. @@ -129,7 +139,9 @@ only go low when all instances associated with that pin have terminated. Consequently if `max_instances=1` and multiple instances are launched, a warning will appear on the host; the pin will go high when the first instance -starts and will not go low until all have ended. +starts and will not go low until all have ended. The purpose of the warning is +because the existence of multiple instances may be unexpected behaviour in the +application under test. ## 1.3 Detecting CPU hogging @@ -161,21 +173,22 @@ asyncio.create_task(monitor.hog_detect()) ``` To aid in detecting the gaps in execution, the Pico code implements a timer. This is retriggered by activity on `ident=0`. If it times out, a brief high -going pulse is produced on pin 28, along with the console message "Hog". The +going pulse is produced on GPIO 28, along with the console message "Hog". The pulse can be used to trigger a scope or logic analyser. The duration of the timer may be adjusted. Other modes of hog detection are also supported. See [section 4](./README.md~4-the-pico-code). ## 1.4 Validation of idents -Re-using idents would lead to confusing behaviour. A `ValueError` is thrown if -an ident is out of range or is assigned to more than one coroutine. +Re-using idents would lead to confusing behaviour. If an ident is out of range +or is assigned to more than one coroutine an error message is printed and +execution terminates. # 2. Monitoring synchronous code In the context of an asynchronous application there may be a need to view the -timing of synchronous code, or simply to create a trigger pulse at a known -point in the code. The following are provided: +timing of synchronous code, or simply to create a trigger pulse at one or more +known points in the code. The following are provided: * A `sync` decorator for synchronous functions or methods: like `async` it monitors every call to the function. * A `trigger` function which issues a brief pulse on the Pico. @@ -192,15 +205,15 @@ monitor.reserve(4, 9, 10) ## 2.1 The sync decorator -This works as per the `@async` decorator, but with no `max_instances` arg. This -will activate GPIO 26 (associated with ident 20) for the duration of every call -to `sync_func()`: +This works as per the `@async` decorator, but with no `max_instances` arg. The +following example will activate GPIO 26 (associated with ident 20) for the +duration of every call to `sync_func()`: ```python @monitor.sync(20) def sync_func(): pass ``` -Note that the ident must not be reserved. +Note that idents used by decorators must not be reserved. ## 2.2 The mon_call context manager @@ -217,7 +230,7 @@ with monitor.mon_call(22): ``` It is advisable not to use the context manager with a function having the -`mon_func` decorator. The pin and report behaviour is confusing. +`mon_func` decorator. The behaviour of pins and reports are confusing. ## 2.3 The trigger timing marker @@ -288,7 +301,7 @@ over 100ms. These behaviours can be modified by the following `run` args: 1. `period=100` Define the hog_detect timer period in ms. 2. `verbose=()` A list or tuple of `ident` values which should produce console output. - 3. `device="uart"` Set to "spi" for an SPI interface. + 3. `device="uart"` Set to `"spi"` for an SPI interface. 4. `vb=True` By default the Pico issues console messages reporting on initial communication status, repeated each time the application under test restarts. Set `False` to disable these messages. @@ -356,16 +369,16 @@ only goes low when the last of these three instances is cancelled. ![Image](./tests/full_test.jpg) `latency.py` Measures latency between the start of a monitored task and the -Pico pin going high. The sequence below is first the task pulses a pin (ident -6). Then the Pico pin monitoring the task goes high (ident 1 after ~20μs). Then -the trigger on ident 2 occurs 112μs after the pin pulse. +Pico pin going high. In the image below the sequence starts when the host +pulses a pin (ident 6). The Pico pin monitoring the task then goes high (ident +1 after ~20μs). Then the trigger on ident 2 occurs 112μs after the pin pulse. ![Image](./tests/latency.jpg) `syn_test.py` Demonstrates two instances of a bound method along with the ways of monitoring synchronous code. The trigger on ident 5 marks the start of the sequence. The `foo1.pause` method on ident 1 starts and runs `foo1.wait1` on -ident 3. 100ms after this ends, `foo`.wait2` on ident 4 is triggered. 100ms +ident 3. 100ms after this ends, `foo.wait2` on ident 4 is triggered. 100ms after this ends, `foo1.pause` on ident 1 ends. The second instance of `.pause` (`foo2.pause`) on ident 2 repeats this sequence shifted by 50ms. The 10ms gaps in `hog_detect` show the periods of deliberate CPU hogging. @@ -412,9 +425,10 @@ identically to other platforms and can be rebooted at will. This is for anyone wanting to modify the code. Each ident is associated with two bytes, `0x40 + ident` and `0x60 + ident`. These are upper and lower case -printable ASCII characters (aside from ident 0 which is `@` and the backtick -character). When an ident becomes active (e.g. at the start of a coroutine), -uppercase is transmitted, when it becomes inactive lowercase is sent. +printable ASCII characters (aside from ident 0 which is `@` paired with the +backtick character). When an ident becomes active (e.g. at the start of a +coroutine), uppercase is transmitted, when it becomes inactive lowercase is +sent. The Pico maintains a list `pins` indexed by `ident`. Each entry is a 3-list comprising: @@ -426,13 +440,14 @@ When a character arrives, the `ident` value is recovered. If it is uppercase the pin goes high and the instance count is incremented. If it is lowercase the instance count is decremented: if it becomes 0 the pin goes low. -The `init` function on the host sends `b"z"` to the Pico. This clears down the -instance counters (the program under test may have previously failed, leaving -instance counters non-zero). The Pico also clears variables used to measure -hogging. In the case of SPI communication, before sending the `b"z"`, a 0 -character is sent with `cs/` high. The Pico implements a basic SPI slave using -the PIO. This may have been left in an invalid state by a crashing host. It is -designed to reset to a known state if it receives a character with `cs/` high. +The `init` function on the host sends `b"z"` to the Pico. This sets each pin +int `pins` low and clears its instance counter (the program under test may have +previously failed, leaving instance counters non-zero). The Pico also clears +variables used to measure hogging. In the case of SPI communication, before +sending the `b"z"`, a 0 character is sent with `cs/` high. The Pico implements +a basic SPI slave using the PIO. This may have been left in an invalid state by +a crashing host. The slave is designed to reset to a "ready" state if it +receives any character with `cs/` high. The ident `@` (0x40) is assumed to be used by the `hog_detect()` function. When the Pico receives it, processing occurs to aid in hog detection and creating a @@ -440,7 +455,8 @@ trigger on GPIO28. Behaviour depends on the mode passed to the `run()` command. In the following, `thresh` is the time passed to `run()` in `period[0]`. * `SOON` This retriggers a timer with period `thresh`. Timeout causes a trigger. - * `LATE` Trigger occurs if the period since the last `@` exceeds `thresh`. + * `LATE` Trigger occurs if the period since the last `@` exceeds `thresh`. The + trigger happens when the next `@` is received. * `MAX` Trigger occurs if period exceeds `thresh` and also exceeds the prior maximum. From 63d818a002d7067d8bc879f65ef09e587ef5b92d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 9 Oct 2021 18:22:23 +0100 Subject: [PATCH 101/305] monitor: hog_detect() produces 1:1 square wave. --- v3/as_demos/monitor/monitor.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/v3/as_demos/monitor/monitor.py b/v3/as_demos/monitor/monitor.py index a9ca587..dc04454 100644 --- a/v3/as_demos/monitor/monitor.py +++ b/v3/as_demos/monitor/monitor.py @@ -100,13 +100,10 @@ def init(): _write(b"z") # Clear Pico's instance counters etc. # Optionally run this to show up periods of blocking behaviour -@asyn(0) -async def _do_nowt(): - await asyncio.sleep_ms(0) - -async def hog_detect(): +async def hog_detect(i=1, s=(b"\x40", b"\x60")): while True: - await _do_nowt() + _write(s[(i := i ^ 1)]) + await asyncio.sleep_ms(0) # Monitor a synchronous function definition def sync(n): From a87bda1b716090da27fd288cc8b19b20525ea20c Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 11 Oct 2021 13:41:49 +0100 Subject: [PATCH 102/305] monitor: Main modules formatted with black. --- v3/as_demos/monitor/README.md | 2 + v3/as_demos/monitor/monitor.py | 61 ++++++++++++++++++----------- v3/as_demos/monitor/monitor_pico.py | 37 +++++++++++------ 3 files changed, 64 insertions(+), 36 deletions(-) diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 4df89cd..4dc5818 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -122,6 +122,8 @@ The decorator positional args are as follows: [Pico Pin mapping](./README.md#3-pico-pin-mapping). 2. `max_instances=1` Defines the maximum number of concurrent instances of the task to be independently monitored (default 1). + 3. `verbose=True` If `False` suppress the warning which is printed on the host + if the instance count exceeds `max_instances`. Whenever the coroutine runs, a pin on the Pico will go high, and when the code terminates it will go low. This enables the behaviour of the system to be diff --git a/v3/as_demos/monitor/monitor.py b/v3/as_demos/monitor/monitor.py index dc04454..07e93b6 100644 --- a/v3/as_demos/monitor/monitor.py +++ b/v3/as_demos/monitor/monitor.py @@ -14,36 +14,43 @@ def _quit(s): print("Monitor " + s) exit(0) -_write = lambda _ : _quit("must run set_device") -_dummy = lambda : None # If UART do nothing. + +_write = lambda _: _quit("must run set_device") +_ifrst = lambda: None # Reset interface. If UART do nothing. # For UART pass initialised UART. Baudrate must be 1_000_000. # For SPI pass initialised instance SPI. Can be any baudrate, but # must be default in other respects. def set_device(dev, cspin=None): global _write - global _dummy + global _ifrst if isinstance(dev, UART) and cspin is None: # UART _write = dev.write elif isinstance(dev, SPI) and isinstance(cspin, Pin): cspin(1) + def spiwrite(data): cspin(0) dev.write(data) cspin(1) + _write = spiwrite + def clear_sm(): # Set Pico SM to its initial state cspin(1) dev.write(b"\0") # SM is now waiting for CS low. - _dummy = clear_sm + + _ifrst = clear_sm else: _quit("set_device: invalid args.") + # Justification for validation even when decorating a method # /mnt/qnap2/data/Projects/Python/AssortedTechniques/decorators _available = set(range(0, 22)) # Valid idents are 0..21 _reserved = set() # Idents reserved for synchronous monitoring + def _validate(ident, num=1): if ident >= 0 and ident + num < 22: try: @@ -54,83 +61,90 @@ def _validate(ident, num=1): else: _quit("error - ident {:02d} out of range.".format(ident)) + # Reserve ID's to be used for synchronous monitoring def reserve(*ids): for ident in ids: _validate(ident) _reserved.add(ident) + # Check whether a synchronous ident was reserved def _check(ident): if ident not in _reserved: _quit("error: synchronous ident {:02d} was not reserved.".format(ident)) + # asynchronous monitor -def asyn(n, max_instances=1): +def asyn(n, max_instances=1, verbose=True): def decorator(coro): - # This code runs before asyncio.run() _validate(n, max_instances) instance = 0 + async def wrapped_coro(*args, **kwargs): - # realtime nonlocal instance d = 0x40 + n + min(instance, max_instances - 1) v = int.to_bytes(d, 1, "big") instance += 1 - if instance > max_instances: # Warning only - print("Monitor {:02d} max_instances reached.".format(n)) + if verbose and instance > max_instances: # Warning only. + print("Monitor ident: {:02d} instances: {}.".format(n, instance)) _write(v) try: res = await coro(*args, **kwargs) except asyncio.CancelledError: - raise + raise # Other exceptions produce traceback. finally: d |= 0x20 v = int.to_bytes(d, 1, "big") _write(v) instance -= 1 return res + return wrapped_coro + return decorator + # If SPI, clears the state machine in case prior test resulted in the DUT # crashing. It does this by sending a byte with CS\ False (high). def init(): - _dummy() # Does nothing if UART + _ifrst() # Reset interface. Does nothing if UART. _write(b"z") # Clear Pico's instance counters etc. + # Optionally run this to show up periods of blocking behaviour -async def hog_detect(i=1, s=(b"\x40", b"\x60")): +async def hog_detect(s=(b"\x40", b"\x60")): while True: - _write(s[(i := i ^ 1)]) - await asyncio.sleep_ms(0) + for v in s: + _write(v) + await asyncio.sleep_ms(0) + # Monitor a synchronous function definition def sync(n): def decorator(func): _validate(n) - dstart = 0x40 + n - vstart = int.to_bytes(dstart, 1, "big") - dend = 0x60 + n - vend = int.to_bytes(dend, 1, "big") + vstart = int.to_bytes(0x40 + n, 1, "big") + vend = int.to_bytes(0x60 + n, 1, "big") + def wrapped_func(*args, **kwargs): _write(vstart) res = func(*args, **kwargs) _write(vend) return res + return wrapped_func + return decorator + # Runtime monitoring: can't validate because code may be looping. # Monitor a synchronous function call class mon_call: def __init__(self, n): _check(n) - self.n = n - self.dstart = 0x40 + n - self.vstart = int.to_bytes(self.dstart, 1, "big") - self.dend = 0x60 + n - self.vend = int.to_bytes(self.dend, 1, "big") + self.vstart = int.to_bytes(0x40 + n, 1, "big") + self.vend = int.to_bytes(0x60 + n, 1, "big") def __enter__(self): _write(self.vstart) @@ -140,6 +154,7 @@ def __exit__(self, type, value, traceback): _write(self.vend) return False # Don't silence exceptions + # Cause pico ident n to produce a brief (~80μs) pulse def trigger(n): _check(n) diff --git a/v3/as_demos/monitor/monitor_pico.py b/v3/as_demos/monitor/monitor_pico.py index 9c5e740..7277754 100644 --- a/v3/as_demos/monitor/monitor_pico.py +++ b/v3/as_demos/monitor/monitor_pico.py @@ -35,19 +35,23 @@ def spi_in(): class PIOSPI: - def __init__(self): - self._sm = rp2.StateMachine(0, spi_in, - in_shiftdir=rp2.PIO.SHIFT_LEFT, - push_thresh=8, in_base=Pin(0), - jmp_pin=Pin(2, Pin.IN, Pin.PULL_UP)) + self._sm = rp2.StateMachine( + 0, + spi_in, + in_shiftdir=rp2.PIO.SHIFT_LEFT, + push_thresh=8, + in_base=Pin(0), + jmp_pin=Pin(2, Pin.IN, Pin.PULL_UP), + ) self._sm.active(1) # Blocking read of 1 char. Returns ord(ch). If DUT crashes, worst case # is where CS is left low. SM will hang until user restarts. On restart # the app def read(self): - return self._sm.get() & 0xff + return self._sm.get() & 0xFF + # ****** Define pins ****** @@ -64,11 +68,14 @@ def read(self): # ****** Timing ***** pin_t = Pin(28, Pin.OUT) + + def _cb(_): pin_t(1) print("Timeout.") pin_t(0) + tim = Timer() # ****** Monitor ****** @@ -83,7 +90,7 @@ def _cb(_): # native reduced latency to 10μs but killed the hog detector: timer never timed out. # Also locked up Pico so ctrl-c did not interrupt. -#@micropython.native +# @micropython.native def run(period=100, verbose=(), device="uart", vb=True): if isinstance(period, int): t_ms = period @@ -91,30 +98,34 @@ def run(period=100, verbose=(), device="uart", vb=True): else: t_ms, mode = period if mode not in (SOON, LATE, MAX): - raise ValueError('Invalid mode.') + raise ValueError("Invalid mode.") for x in verbose: pins[x][2] = True # A device must support a blocking read. if device == "uart": uart = UART(0, 1_000_000) # rx on GPIO 1 + def read(): while not uart.any(): # Prevent UART timeouts pass return ord(uart.read(1)) + elif device == "spi": pio = PIOSPI() + def read(): return pio.read() + else: raise ValueError("Unsupported device:", device) - vb and print('Awaiting communication') + vb and print("Awaiting communication.") h_max = 0 # Max hog duration (ms) h_start = 0 # Absolute hog start time while True: if x := read(): # Get an initial 0 on UART - if x == 0x7a: # Init: program under test has restarted - vb and print('Got communication.') + if x == 0x7A: # Init: program under test has restarted + vb and print("Got communication.") h_max = 0 # Restart timing h_start = 0 for pin in pins: @@ -140,7 +151,7 @@ def read(): pin_t(1) pin_t(0) h_start = t - p = pins[x & 0x1f] # Key: 0x40 (ord('@')) is pin ID 0 + p = pins[x & 0x1F] # Key: 0x40 (ord('@')) is pin ID 0 if x & 0x20: # Going down p[1] -= 1 if not p[1]: # Instance count is zero @@ -149,4 +160,4 @@ def read(): p[0](1) p[1] += 1 if p[2]: - print(f'ident {i} count {p[1]}') + print(f"ident {i} count {p[1]}") From d9b47ccbcff77ced374dce2f06de05094661af14 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 16 Oct 2021 13:44:33 +0100 Subject: [PATCH 103/305] monitor: Fixes and improvements to synchronous monitoring. --- v3/as_demos/monitor/README.md | 63 +++++++++++++++------- v3/as_demos/monitor/monitor.py | 69 +++++++++++------------- v3/as_demos/monitor/monitor_hw.JPG | Bin 0 -> 259855 bytes v3/as_demos/monitor/monitor_pico.py | 30 ++++++++--- v3/as_demos/monitor/tests/full_test.py | 4 +- v3/as_demos/monitor/tests/latency.py | 4 +- v3/as_demos/monitor/tests/quick_test.py | 4 +- v3/as_demos/monitor/tests/syn_test.py | 5 +- 8 files changed, 105 insertions(+), 74 deletions(-) create mode 100644 v3/as_demos/monitor/monitor_hw.JPG diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 4dc5818..c6ed1eb 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -184,7 +184,8 @@ timer may be adjusted. Other modes of hog detection are also supported. See Re-using idents would lead to confusing behaviour. If an ident is out of range or is assigned to more than one coroutine an error message is printed and -execution terminates. +execution terminates. See [section 7](./README.md#7-validation) for a special +case where validation must be defeated. # 2. Monitoring synchronous code @@ -193,17 +194,10 @@ timing of synchronous code, or simply to create a trigger pulse at one or more known points in the code. The following are provided: * A `sync` decorator for synchronous functions or methods: like `async` it monitors every call to the function. - * A `trigger` function which issues a brief pulse on the Pico. * A `mon_call` context manager enables function monitoring to be restricted to specific calls. - -Idents used by `trigger` or `mon_call` must be reserved: this is because these -may occur in a looping construct. This enables the validation to protect -against inadvertent multiple usage of an ident. The `monitor.reserve()` -function can reserve one or more idents: -```python -monitor.reserve(4, 9, 10) -``` + * A `trigger` function which issues a brief pulse on the Pico or can set and + clear the pin on demand. ## 2.1 The sync decorator @@ -215,15 +209,16 @@ duration of every call to `sync_func()`: def sync_func(): pass ``` -Note that idents used by decorators must not be reserved. ## 2.2 The mon_call context manager This may be used to monitor a function only when called from specific points in -the code. -```python -monitor.reserve(22) +the code. Validation of idents is looser here because a context manager is +often used in a looping construct: it seems impractical to distinguish this +case from that where two context managers are instantiated with the same ID. +Usage: +```python def another_sync_func(): pass @@ -236,14 +231,23 @@ It is advisable not to use the context manager with a function having the ## 2.3 The trigger timing marker -A call to `monitor.trigger(n)` may be inserted anywhere in synchronous or -asynchronous code. When this runs, a brief (~80μs) pulse will occur on the Pico -pin with ident `n`. As per `mon_call`, ident `n` must be reserved. +The `trigger` closure is intended for timing blocks of code. A closure instance +is created by passing the ident. If the instance is run with no args a brief +(~80μs) pulse will occur on the Pico pin. If `True` is passed, the pin will go +high until `False` is passed. + +The closure should be instantiated once only. If instantiated in a loop the +ident will fail the check on re-use. ```python -monitor.reserve(10) +trig = monitor.trigger(10) # Associate trig with ident 10. def foo(): - monitor.trigger(10) # Pulse ident 10, GPIO 13 + trig() # Pulse ident 10, GPIO 13 + +def bar(): + trig(True) # set pin high + # code omitted + trig(False) # set pin low ``` # 3. Pico Pin mapping @@ -443,7 +447,7 @@ the pin goes high and the instance count is incremented. If it is lowercase the instance count is decremented: if it becomes 0 the pin goes low. The `init` function on the host sends `b"z"` to the Pico. This sets each pin -int `pins` low and clears its instance counter (the program under test may have +in `pins` low and clears its instance counter (the program under test may have previously failed, leaving instance counters non-zero). The Pico also clears variables used to measure hogging. In the case of SPI communication, before sending the `b"z"`, a 0 character is sent with `cs/` high. The Pico implements @@ -465,3 +469,22 @@ In the following, `thresh` is the time passed to `run()` in `period[0]`. This project was inspired by [this GitHub thread](https://github.com/micropython/micropython/issues/7456). +# 7. Validation + +The `monitor` module attempts to protect against inadvertent multiple use of an +`ident`. There are use patterns which are incompatible with this, notably where +a decorated function or coroutine is instantiated in a looping construct. To +cater for such cases validation can be defeated. This is done by issuing: +```python +import monitor +monitor.validation(False) +``` + +# 8. A hardware implementation + +The device under test is on the right, linked to the Pico board by means of a +UART. + +![Image](./monitor_hw.jpg) + +I can supply a schematic and PCB details if anyone is interested. diff --git a/v3/as_demos/monitor/monitor.py b/v3/as_demos/monitor/monitor.py index 07e93b6..ef13a2e 100644 --- a/v3/as_demos/monitor/monitor.py +++ b/v3/as_demos/monitor/monitor.py @@ -44,36 +44,25 @@ def clear_sm(): # Set Pico SM to its initial state else: _quit("set_device: invalid args.") - # Justification for validation even when decorating a method # /mnt/qnap2/data/Projects/Python/AssortedTechniques/decorators _available = set(range(0, 22)) # Valid idents are 0..21 -_reserved = set() # Idents reserved for synchronous monitoring - +_do_validate = True def _validate(ident, num=1): - if ident >= 0 and ident + num < 22: - try: - for x in range(ident, ident + num): - _available.remove(x) - except KeyError: - _quit("error - ident {:02d} already allocated.".format(x)) - else: - _quit("error - ident {:02d} out of range.".format(ident)) - - -# Reserve ID's to be used for synchronous monitoring -def reserve(*ids): - for ident in ids: - _validate(ident) - _reserved.add(ident) - - -# Check whether a synchronous ident was reserved -def _check(ident): - if ident not in _reserved: - _quit("error: synchronous ident {:02d} was not reserved.".format(ident)) + if _do_validate: + if ident >= 0 and ident + num < 22: + try: + for x in range(ident, ident + num): + _available.remove(x) + except KeyError: + _quit("error - ident {:02d} already allocated.".format(x)) + else: + _quit("error - ident {:02d} out of range.".format(ident)) +def validation(do=True): + global _do_validate + _do_validate = do # asynchronous monitor def asyn(n, max_instances=1, verbose=True): @@ -104,14 +93,12 @@ async def wrapped_coro(*args, **kwargs): return decorator - # If SPI, clears the state machine in case prior test resulted in the DUT # crashing. It does this by sending a byte with CS\ False (high). def init(): _ifrst() # Reset interface. Does nothing if UART. _write(b"z") # Clear Pico's instance counters etc. - # Optionally run this to show up periods of blocking behaviour async def hog_detect(s=(b"\x40", b"\x60")): while True: @@ -119,7 +106,6 @@ async def hog_detect(s=(b"\x40", b"\x60")): _write(v) await asyncio.sleep_ms(0) - # Monitor a synchronous function definition def sync(n): def decorator(func): @@ -137,12 +123,14 @@ def wrapped_func(*args, **kwargs): return decorator - -# Runtime monitoring: can't validate because code may be looping. -# Monitor a synchronous function call +# Monitor a function call class mon_call: + _cm_idents = set() # Idents used by this CM + def __init__(self, n): - _check(n) + if n not in self._cm_idents: # ID can't clash with other objects + _validate(n) # but could have two CM's with same ident + self._cm_idents.add(n) self.vstart = int.to_bytes(0x40 + n, 1, "big") self.vend = int.to_bytes(0x60 + n, 1, "big") @@ -154,10 +142,17 @@ def __exit__(self, type, value, traceback): _write(self.vend) return False # Don't silence exceptions - -# Cause pico ident n to produce a brief (~80μs) pulse +# Either cause pico ident n to produce a brief (~80μs) pulse or turn it +# on or off on demand. def trigger(n): - _check(n) - _write(int.to_bytes(0x40 + n, 1, "big")) - sleep_us(20) - _write(int.to_bytes(0x60 + n, 1, "big")) + _validate(n) + on = int.to_bytes(0x40 + n, 1, "big") + off = int.to_bytes(0x60 + n, 1, "big") + def wrapped(state=None): + if state is None: + _write(on) + sleep_us(20) + _write(off) + else: + _write(on if state else off) + return wrapped diff --git a/v3/as_demos/monitor/monitor_hw.JPG b/v3/as_demos/monitor/monitor_hw.JPG new file mode 100644 index 0000000000000000000000000000000000000000..8cca56e2feb15e17e625b064415b8ddd551a5c34 GIT binary patch literal 259855 zcmbTe1ymeO*Dl()y9C#P;KALU!QI{62~P0f3~s^Q2e$~pA-GE*xI?f62y)5$egFUc z>z;May65ihntH1C-Zi~%2Q zUoh;yaW&mb<3*7F*`ELBasB;nmW+&nkCBC)gPWV1^0j~nKc_Go7dPkML10i&P|(rPi7+sT zIH}2~IsdQa`3r!93{=Cs1WNggL;T3OrJ+SxmJdU^Z2_4V@) zkBE$lejgK?lA4yDk(rg9Q(RJ7R$ftARo&Fw(%RPE(b@I2uYX{0Xn16FW_E6VVR31B zWpitLXLoP^;PB|;^6L8L*YDfA`@eDhjq|_cUxEF<;=*}}3l<(84j$=mTrjY2UkV%! zJOVWbBCdowlDRt`4QD7az9ggw+KWQVrSX%%!ebhh5X8Mnckws0eZuh0-(deyi6V(4j=}cM;mEIa%(QUBV+uUgCVR28>%uuRiQ@Bq5K;EzU-|VH4=Sj zYBo}x*|7=FG&iwuO?S#~qRCX$vyh?22W1Q2%mG{F*$O)%za8hKBj0g9JfSgKMdnwK zi9H_ymLLZ@X1l}_dK{hKWzeJZ3s0q?vzG3?>vi>zs%p+aqb%s97Y`gXp!O-$aeM#_ z>8WtCGyaHgwGcRS=Fbc#;jp;%7B5h1VdGIIAwuf0@hs$;THAC>ET;5N>dL^=4)6>J zFVe#pSz*f$51*>4kJB$tqtl_;JW`q z_M5sSA{)jCt*9J3xReVg3hUg9*_?tt4cA$0+rkuGPsBjEcKOYnQ+=c~dce_7L!{CR z27iJ~fcK_Bq`H?(%PITE6oQhZm*9 z-9l8-CJu^X?qmJ&sB{T(fyCOM9H4*N3k5`Te@Y5}mOxoUdCqrDQ0Go`7>{ENf;tlo zSQ!t=V;QEL5xw$iD#p@r(nc%5KjgJSVaxR6M-|MujQb#U`n&AYnhjDI6!Ka$djdoG zR|pH7_b*?|CxRUQ`N;OHa07KVjAb1P(3@kV6XRhp( zc`~ygCzsm7%Xwg`&Y@l{e^V-zD&yWbW$lo729iP|ZNQn#qjo4mp6z1gjDlW&8iuYd zwV8ZE7OO>Sl;>OH;aMt#Ho)T)G;?l7WXuSI)e`NdVzfpEV_gRg>a$H{SY+|}ZTq$TM*$s5&7;8Ebw zkqmX^Q;gY+Tpmr1D%S`Sh-ejl$LDlH#BFGnf82ou9DDOb0b`&zzeWYj%7)paFS{2)6853fN6!FmARWsaeE(eV($ENeM6mCGr$1%CGZvWBZ|^WIMGIsc z!pU@CpTMnz0CcmlfE^QQ@6`4C@az%OFOG^RD`8HkA-a~9FDSH$|W>DF3e z`9s^I_)Ot7+->ktu;fP?RyKwqDm;xArkZy^A!hKHbX((lkUPzGq53-}KQ&AQyWx*V zd2xBMz}N@=+T2oNVV#q)oUu+7J2)wspC3xu;giM^g+pM*CtM z*-gGZ)R+1$QBHl$ds%VBU-S7V6q25?f-<6>w>sGp-1(&4QowsM$ed~A-|sKeL00^v zm2%*tfpr5GL%q0tV*5TsEoYeZg?~52^f67l~Ym4*-;|5JZ#7(c=pI39c9Tw_oF=R@UtQQ! z#m>m!;~{@7-OQ5Umhk#F-X85wfVmn2x=-moJA#H2`<1Mhp|^OC zOr(FDC8rfziwiXO@}9z4)QTu$)=NnD z74h8QY3gLcYW2Hu4#FxOE$9Y4L^IS7KBz>KHv0Q7^x5CT-ZHw*YP~kRlFSv8nG_BZ z7`G^*3d6*(@^9HI|IlUUvKD+Ll{R8U>-$}@18$f2{BElWsZ1|btt*F-CdH^UYmMW; zLoB&cMu7YKG{2xmnQBJR073ia2TlxAT|?5y^~5r0o(D6LSAE0g2^L>mq5E$OS(Xs6 z`r0a9EM zuYSuIl;bF+E`=Rh%r6QxIb{l0&^GMX_~jR znxuZbcaJhsKX?*DP*&x&a80D{pkfOR$CRIcYX7`xb@+B`GtfiefHGq8IazGsonSag zgE)t>`w)Q;;J}LyqxI8MT|}iXC*n$+07P`Jt?&x5fqy_d+0714(EA|hSa1_l*%91% zs$~lX`eSsQp*?CYB79U#8{lbEwC^fX$Y!eEF*4@2(S13Y;Y7yLmnL>QmwGOC!JpP| zrhPRB4xodXxF~gco2CMcr}-~y0nhh3nQ?`OM6_y^d7Tfvfxrye@KpFSU^Jle8H3nrbOrn&r3u=P|zH#Q-O`KwI#`W2o zV+8Gqdd?-{N2HOkE!NXiInD!FR6F#7I;D2d!O9W<9Y+63;X!FOgLzX1f5u!rq2#if zBtHgys(qLv34DOSD~;Y!h*i*fj)~BjP2e8pu7^{aOv$88of&^0`#Z?A@6eOlttV%H z@23@?uzZ1}U~IPRzAQyguL^ z+1LX=hnj*-%WM3bgpog=3kZ(geQA|d+Il~)gXV9A87l^5S+<^mPxgN}O&soNhwHe< zayzwYjZ*wERS!+Ne>9*y1LiBO{oAi_6Qe6BZn`bY4fW%P;2na}P(SBX_R{8j5-qd4 z{D{3!NuSS;-BVv#QuVs*RlB>s!HByMLIR=!&*;jsv2U??K=6!Id3ctYX84=VYy7p^ zlXm_&*P5lz?ptvTiG=c6KK6S!;n(N)8Zui%JG6LuiUpgo2evn`Etb>QOP0$7X~;;$ z=uh78zeMF(kr4t)(8E=}8I3_f{t?` zHm8rqU&>fK(9IQhOj$#<%~=dV0*5V?rhC!X^|t*KwAl|h6-&}%yT&m?_Mbv{OJ0rm1ETq@f|eXX@?i=d z7iPjCFTd5hlZeMado%|qVVY-*vBOdW}-Za7WXU; z2IGAEX}o>3!j#ROt$J=oOCx3>BJxLvFY4yTPByxFbh`Xx)o4GW{m}fxWVSnG$TdZHC zYq91apQ>@uIm1yQE&sqj+{CorSC$oVN70qyQPlEJ8xpDNZ-tYPt`O8ZD;0=0!Isi9 zV4JtewoMjAp68!<{hDNSW;&XsDo}`G1I(QFb0cPRzS)thut%aG?SkrtU^;d`kIXoG znz)L&=xau@#$IVrIs06JttvS6BR>fcFIm$1Cat7ZK?dNaBCit1KLP`cO{2+!GxELa zvI-)Vey~-bAE||RCsU^mEIv8HcCmH`7GC^{ zqWr_^wQr|Z2$Z}-a!|+jX&b|5s3Kt_&~igC-*9YOaBZ`P-CvKW6#(x|IgBsz{K3Ln zs#`BUfpEPaoM%xG5!B5&GGY3hdFm8oS+H5(qG^r`)oLSqvNYg@$Z8sUWl zPo5rmwOR*6Iog}*U@AfHfySJ9({TpUtkEcp;WE0j^84_r;q#?E>u#>q6`6co8~*F9 z)(0WR=>B_6thJ!sLz zDq&i6t>B_8+pnA2dx57FB;&l>(Vif%_O0W@ePa=Bl{?cEyH%?TLY=d`ZDby{V@pg+ zb(NTw)-V;R&3{EvxmVLku=@=Bgz{JeBRh>Eyyaqb><=>YD5l?~kuESgPy9WPW9Kzf zbrEz}DRS24RQMZsLf-pltETXW?^h|s+a4bp3i8EcoPL=5o#_KrKm*!Ncw64xdoBvQ zgVo)2tp~v>!-qS>)hV%`X*?YAsy!SOg?|rGVm$-Uid6N+gq@ik0m!S2cF~f@oQY|k zu}4n3Y&|1C#VAS7y@AssvGjmuXD^0PVm-4eG8_}jtp-5BmDcpY>`pwB{f*wF$ET7x z?b+P$sfkkjg$lz82g01h6sz|^HA>m;EEr=l$3qpD^@!+!g?mL3bKt%{n;RyxQ9}bEJZScAO1))a8IexI?cdXZ}$wCE{1&W zoFwiL)ekXUbnS-rnd%$awf|aN@mjay4E&YJ_}l8d<(}mH`uu%H>LHGiBK)MNkFYzN zN^n7>$EBg%=J4JnZNNE(iYIHkKFjK#QNgWF?i7A+{f6t@?-v|YiQ@;s9jC{VAN4Lp zrg_BPp7$P1YKO$|D#joFcFfNWi+^f`dr)hZQ*+u;Q{g-T2=4O> zZh*|5T0iU?qNV?lJlEc-m!I46x zrSUf0vl6VqZyE33!OtV<`o61c^Z>GwP^+8vy6Aopt4uVB(LID^Dv2fQSLhh#noil{ zcoj)h2Yf5oP%ieY$zE7l4K_W)t#zPZjDiTyTzX^KnlQC?E7JM$(5L0GeXKyq4 z+K|eLNjlcudVqaBwx**eO-HMVQzXAM1CaM*PjdU^VdjVOe`^ZC2e#zCM zt<5$~Y@y8ksDzf;TxT*~gnaE|o4=qL17xDC`Q9$?y!dZGnl48h3;hH7Mgu>;=X`8Q zq8V)xV5sffmEUi#lOvlMEM6E@K0(HHL2CRO4>^0e7hO--BHchk@#>Ca*tPIPvFTl< z_Zteddh#A{Nd2z&J;ow!&@>MwjH!T+t#ssdv7iW>B6mlNU!5(@D>fxFH~IaW`n?Uc z_l5(cwB73?zk+^ZY;kl3yMlMm%n9E%51}%s6@3lO+wpt~I+{;}hLpA_+q;6oh4SEq z@c8dyYRSsDYYlh9=hyaxWo(@-wun}nPhp%M8U24r&PK9Vm3CHg^;C(x-WMg zOd+^caRciX`Q1cTsRs^N_?IL2ko{5Xv}^UV8+ zbt49;&C9!){NyMnk%IjOS1&tGQj-0NP=xD#AutQK1FZ#7jPZ05{NCh@MWBJt+R8Ao zcURgP!ai)8|HW2pv^7x75n*5+n$5{~>Xgs_J!vksqO_N`i@ZN3MMgb8ONVk!dlF0F z737aG!vkOZGay*H;T8~L60#G3&@#9+xE{DOhAXxQQ@KY#5eM0P6sD+wWqd#sE7ME1 zjfLiH9-_^g3g%dY+{MX45`?iZ+mvs@(E@Ic6A4utK7Vx2FrofD%_fuB4nLj7uQ0q5 zr0RduX~n1bu!Gc$K0jN<EcDr~#%tKS zgtR&bn{@_t!V&CttR1AIwbR>us$p%JkwE9zHB329!75{RxPR}OT>`hlVNkH#hPbbu zzdPCii7h;O$VZTr$)Y6$w&%xDO5-Aa6hk!s*j%+h_7}$pDy(b3zfz3Hxub{qDfD_R z0?>8*{s`W_Uy-_a6n?N}>H&h^_nX4l(RD{VpW@f&@=m7~vdo|6 zT5iv;3EHyxf*L{m6PU>R*W>)m3VAJMjM{l+3j?N+3#87%H=;T{Q#4r@xI}U ziE7A2mA8llcU<~R4)U2*n{g9(2z5wcn{uJ*pvI2-d_xJYc3@fYbOjiudb=NMl@qT4 zZaPV1)L;E(78*k?x6l&Z)56s?HPaYITGQ!b6NR1N5{YJY;@r7?7`xLWL3Bx-?bB#$ zzK3pgwcdrcdP|exdwTZv>nQK%4DEIRF@$B|bu3j=glwAzGwe=6ipyHZpMD?FIfcW@ zg$AkF7G`Gq9toeT0$#ayfeF9G5d;U78|6^%(BHn16m_B;4sIQg@pC6Q z$t0X`9;5H|=tJ&vk-R$6(zX-Ut{SIm?&!3W`LayQfDw5R;E$(2o2xa0GAR7Jt%7G8 zh1WN$3hBLibHT5W`?(EberKzE&gm);o4|nBpw3yK+6m6X=jQqas&~N#6%FfBoKiW9{T9Tj|_)Y+5;cY`&t@+?l14*6vU(S0;d&T3~1jYcOS7UUptpJ7z~*Z6|q> z;_1_1VcUYUv$v(NiG}2G)N=au5Zr9RL`k$p%)@++f*yDEZs(d?$3#e(#a*s5E$?nX zshD(*S5F%}c9>-L^=lxuxlCbqoH=RJ`572~2EgNC=O@$YF$&CLSrLz7_g`!EjWDiW zAq4iez#O+MWbpjGdh7+*Bum-biFJai?&SOKwayo+ef<`kaRt(L>HR=Ri=KE7BQ;H^ zjfN;)IVR!i9UwC`gn$?o!3FKn?l5;lnhp1ny{Kk%K{X_#>Xq)VOT%9pL^H=@eSG7P zG-M?jJiBLb>hjW0O~l+$%TABB(*jX5POAvWOFRXNOq-}7A^sY2I|1(MhY`Frj>D8a z?NV@)OHUQlTyRH)q*7bU;JV#Ay!U9Fdc*RKwI z%*|Hw=z22pYqWo#mrfP*ntR-5Z$miY@MB1%B(A|42g%M#g*-Qm)Kw0PQ?Xk7=ES$=@iDD z&wxWTG*_SjV|n>g8mJ>4dK`-R@KEyLV3-?#*dwZ>_jRz8+t58FK*#5UpQp1q?CQjH zScy;w)=&ID&p?Zzk^Fdd|0MuNaZAq3-<`)^x!Pn%)b5iUwm+uRO9f|CHwxb|B!krM zaL!|BS9kr#HbGCBcc$#>Y6nxrmu!Ks-Q2;Vt22r{k{-FpkQRxj)a=y9Sp_#d@Y~eX zgDU*n1>TtnjdAjPQ<0{4pJcB5<$<>*pbNPu*Fdqi7-jR_YutvGm*Q)rv_y}N0};~6 zMG8DsmwUtjyO zJ_r>E%1RFzcka*Bjplfv7B!H`OSWw=T!TWS3cfZQT-2VC=n0ItBa zQwTm(AZ-vBYEJJ@vu(G0bkx@`!G(j*T_mQ3|KT}cf2-J!n?z`zfg_EJ4d!rB4D5Ii zoSUdoIKTW{@JGQ+i!JK2y3Ro?THZP$h|)uDcS7 z!$Rd#r30%9&Ze#57<`ojRC&Xel}04SI`-O1XByA_u2&=EM>=hu@J>WoNqc%+Kry@Y#QogjeYp)p5~J@?Z4j&IbSB%^NoPTd zVEZmqWeVsWCa^fuCCp@})|E8+fn{|4aQtER2gIe&o^{p559CEuEm=Nyv$wRY_bn>xu-2P34-T>JIfyi04^bf@lnxhd;#6WLvruk05o392ay?7tc|i5`LAp%5tHkX4t+b z88<+`p0j)(cww*D9>Vh^XZZ}EL7#yGRI&3@hvf&=rUkFN!p_!|1vvoj!&v<9>jtKz zulGGEPS~3P$ym3#5|*1nzgivbUX8N$r;dt=Gv&?B(6{`YDZe|m%Upke935V~`dWk^ z;@JD;re1R^;oB=-pBX7`4@Ln5J(4|(TiwbgxW^iR;%C7{rLgjktQM>Il_NebLgCU5 zsvve|sf2XrL?`NJz)hE{7WTFKO6@by|8+9nMdqZ({kqZ$!Zw1rN%r!*BAocak&fJp zzer)wMPcn+(Y0n!K*A^M7(~y&4;JsmZPa+G{&(XR(xEEY99^1R)I|?TcPjTVv1EWxarZ($( zv$h;fgl6E|cgZ}mf=QYNV?*Q?uNZInazoE9{&eIC|D2j!&!v%;&_=!Tf#2tvx5f_* zB!>e{VnG`hRZjIgDdQZrWKA5d?CF%VU3SV&fi za5~aY>UKZP5dNknCjJ#x0`Cq!@Zsi(S~X{U25fpTw9Dq+gmp6tP3SiG{|@r|W#9CO z;hT{hW^D0;!t?VTiPA)QA8UC+-4DA}JM3g#w=b(BVkQo@Io!uAGHy#$H(!u$#%*QF z*2nDFaD_rx!j?GBgejjs68c-uFNd&*!#jR#e&vvs!k%W%I*#KJu_b`3g2IF)j6Rz% zhewRdhrX$xEs=Lo@YHG}o5?{wJv{9mUkG=vtGGEv*BZ%Y(}8<8kSzNnFbjS!$3B%v zGk~~py|jt5qKg<- zzd~G~LF18VM0D=6qS?id%48b4lx0UzdwcvP@gwjIk`X4_?hm02@;}d#K~r_4c7M`) zm+fi_g)@0C1$Q=jpXy8T8otS-1ajW6sSORMCfzf#@%dL-2+=p%-|b=_%OiCrddQ!Rvl*!-b#NBR#*5M+Pm78f&K zSyv3pxd}Wx8WhtYlMRO&J+_yqrBQ*<#EJ1-60S!u#;(tSacaJk5g2<;W@r0nye#%_@C_ zZ{JPCTJ(nVE|DAPDJVL^Y<;C%So8MF0>~G?i-8A_b3YwDY2|cs#ilg#@@sgY;Ti=32`1c! zs|tS__bU-wTVDfS4NB6cXGx=FsNF-OVoU-!rHqx#GObs_bG|eRf2atFUz%>VAOzB$wsDbZQ(p11hUlram7~Z*n#681q_qtg{2QfHDSe^Tj z(^B}z^6jT#^3z3l@LS332iXoCSXt=^WW#1`V!?ocJq@HQR!9xrr8`uuMR3n^;CcQG5|t*Cy6S-mY{qGd;cdM;&XAFs;!)%{Isiu5|u9`kAxAvL7u z<>xKfMg=J;Q#B1$Sp_AT7hc#4r>oG((%BQ99RQqNygW4IB&l8~N>oU@0O|{o4DDs3 zFt_w{lTcGr`Y)o?|62cA_=iRY%(DI+>wne%AA2yYtUWDX2v$@tR^pa!9xuc+7_%45 z`_{|tFV1?w_?C9&Rxh~r1+#d(9N-IH`rB{uFMRSB+x~-LUm$?*p`{`95*z*trn3DX z*y4X+OFIwe7aQ&u8<3T=%ggw%djG;!e{tYn?Cj+Ia%}&|U!oYgwTrgqOAUG{WPlu? z04M`$02N>Xcmwu;(+jDN^`&-s>G1?KUi6ax8-J32{MBB(EMC0q0m~N;X}}e52F(BQ z2ma2%i_MGtC%2w9T%7-O!Jtb50K)q7^VJIv4=EJ@9+RJ+{}eqxKNi1m`qlv8i_3rf zT?+w#|K_DX`9FQ20sz2@0D!js|MXd80zi8-01z#^nR}T3^F4p*f3P+$+``KW0Km`( z0K6#xKsEeV-d=Qn^+3rY0BF7BN@*MbvU31{-u7i|qyJ6Ze+i2Jowxs^&A;>a504NI z7WVJ{BJeLe0`gx*9s(*75+X7>DmpqEDjFIFCN4GxCJrVV8a5#|4jw)M0RcMJDw?vOJzfXq7kE20!J(DYi+hHL$9(P zU>z%wSv~r58U2P1p8b>?_8p8kKJcYloFsxBV26>!HV3FsGyt<2Y{#Z{jgD5|Tv2$rfcsz&Ao12M}Mx9DS~r>7)Q6tIcj0Rngu()3@| zF_uki7m>BL))&UDj8ruEe|b)_P6Ot8DeZkGa#Z68QQ^Uql z)Gi4To+Py4L=le0H;s-(Xw~b?EXKK%o6LtjKWdGIw+(B6S!jbazCp zLx(P(oQ$l24x(dEE@Or*3{%T$79dTvQ7U=oDU#)9YZ8<%f&)oLiGa-&FQvhelr~tB zl&)b2En_6EEaPq|r(;)ex2Czt(`6<%l%=zoRJKOuVV~;Llq?&?{=xtgg`j~7Xh0<1 zu}4HyN+;Bq0r9S=oA(ro(QRh|x@rDtQE^sCf0kR^`@P1-hIA#a0D5#__D zz!fxv2s#vvFw|Tb23Q1nc?l+Y1^F?mY!*ZC@_Y&jWb#Z@X^8EkZK*bLIxH+U$eLUy z3>*7hgp^l|KD6xp;72>_-I0Q(UPc?^cb`az^)#d-Ru2PN>ta;(fvwy!f9*(skYEtkb4g~WGSkY(hRjo zmj{UGw4)G^izHCR%gK@Pso<-KWs$eh2x;?bl2&&*f>=&k6)&yV zgghn>X+G03vayyI8J2NIvQ!vdjLj$*6 zxevczjutyfmZq{#E$U^Su^CYTs6?R}krx#r&>ul$N-i-7&A zDX9@LOJ`*3VdmbH>fgofk&3^{ae+zf!jb>kYC@fwx)PhD3+pz&>ahDqcfAH!JlTLIzeVgXTXn&y`9v7B@u+Fpv0D&@RcL2PF=d) z^V@<*sxhG38AH~nyV*Pr{&EDoU` zLQSm)UPHx(D?&lw!$!q})7F~^b@^IK9n<{fidB)){CJtqh9we+Nz`J@H)^svX0&pB^*O)tBW zgGDS3C9_Q{%P{t()z@t&Z*4HXM@JYpdppf}?v`yIe%?80+dl-wwhj=FLp^4?8DS!y zfj28+f^${b45*%5QrYPP4*E_7i$51TR>n;dJ#&v(npmZ1(KWd&oYFv{6(w`z8^pD~ zHyY3k0t~PD+z#8!!mYyqHyI)v+*tNL$qZ3GAvV1))?Coyj7?fa%q)JJH6Gba759a1 z7Dwy4F`{LQAG>0-TQt<<>3f>$27WFv8e=K;W=)K^xMg}!2q#pz1WuFTn+1#48MUJF za-Zh+y+tq2S=$=Mu~{TtGj0BTJ0rp3lrSYW;=C@hP_!JT$E-)#mRwXHC@cO~Q#*q@m000T6r*tV3 z(E_-|{ng^7(TZ}`1^K)lJwmy(^~XE*u3Q-id3xHRR{GnT%*bD)hA)Tp)meg5VUv03 z)OaxBOU$+W^g5cE30~n54*e1)Lrkm`&|j&!jQ7aQe?@!@vA2y|?aVsbcdhww7stQ* zifn5$7Z^Sp2al;Qlj_WEn^p;!zZQN4!Q(gZ>7AZc9-Ech3P2ktPDo5D>Y2T!(zKvQ z91%~`jHRQOU*4@L**i}l-y3odT2*OGYyvZ;L^6bW2srZH3aB(hD6LsRVBryk({Xdk zllAslwpFdK+ao{f#ic}(HA-juQ?Hi21n zXmFGPJIPiiy(anvXvn=ITFZc z>HlD|`;=5-^i9u`Q>IC}-oTT+SC{APqT=jsO!%j3^ilKd8YGcvlx)O~j>$Wf1*BYM zNot!Le)@2_+}iJJFo0t@BTr^E?lKm6&@op8Ull6;E^8K#HFSpQn!|qoVMpu0|LPDQ zUeB$@NDc`p293(iA2kI#O^RNvD943FquiXAyN*_pMi6R=JE#&ArRDR6Zog#T{*hAf zJcM5L+qHYo$C$n2cGi<@PDv_<5}e&wNCwf8MvH8EE^%5@Z-Do=)#~8yxW5iv$(n<8 zYtx-yJKlKpJOg@e+uX(FlCk*1bF>MNw|;{0Pr;ba0G~qd%~E^MLo{p#Gud3U2Z*p* z+j@LF;2Gd(%%vN3_+i(P<$QF?nyn8CJ=C>GXD~yKvU2=jX|XNn^48okw)DpxIFN0Y z?EXoQA22wHZIcwPCL=(BGI?c*OCm<4MM273NYHJvi6Suc`k^xd`!#&|pHn-31tRsF zt(~WzD(;=kTfebRYba^V)lU%|kl)dcjW6|gASy45jjwV0{yw9lD0Vqb+hr>j=MBZF z+?!=K4cICOb(7JE1A;+rRnP_kbLvWJ!(!dgqL&^EYE?=Y)RPNAu%kfg(y-S5L)KWE zacZiJAx$vPPEHdNqX#*DAvGaFZ1(Q{rMdf9X@&F%Zw$YWig)CptxF+)tS)>>brCEa z32r?N;+IOuX*Wa2g{4iO=Etetk1c*` zJz83L^9XbOa(6lTwq~3YXUhR$TZ?8B?_8ewJ><&pB$LlcXOc{soiwi`RCI}RaGA!~ z8sgEtehVWZ9pG_I6)mUg2yh7ycoH&MXYs&cMXs5G6;zkW69cEWjlyMB`oB1%)F0_n}uAlt!wwTG3i#%VPNmcCU& zTfsCs@;U-6>S?h`-$z_)%Ae}2ebpG{PsK3NG#krc5u-MXy3FyIefZF4Jq8VgZ>)o` zI5Qh!Ki3$E_?bUo1=_E4)n%XDK0Qov8da$?&CzC3B{m7P)l_V;@?LCQ2GP4>m8Hxe zaB=wVDntk;buP+N5L&dW`<3}p4Xw5@jb~A@c-Ba z65J6`6+Hgv@R8io!2%YyGzw%V9;4FFKq8F@a4`c3iNiy_r&m$E6|1in>#zoR-^W>$=~#a^ve`?)&N9#UPy%*`W-kk`T?@;qW@z zwf}_IHzL~2J1zqSA%YqM{8b%6Ayg7sKIffvYBKo5yH6J)$2y6{?l=aOE9K-ahj)@J z)j)8vgN2WvQy>YIj~YD0miAZTbF>R%$Tomh#>W1B^2d_nd8^A1_Y{0e$#%HXoL=Mf zNUoFj-LrMCS{bA!55pu|Rj4h&G^>*5&NR)u! zEP-Ov_MZxlLu$3;dT=g=k=)9Z$0K9I;_wBAc^@G+kK3GQP{gB2(afH9mi4jm2KlUDAXY(s$PphY)1IORVfpX? zr+}urK`sqsP>vJ7CYCNP+P1v=6P#l~ioBAh1qdWuB(Ej$PM)f&11OF_PxGEs@s6v~ zm8gP-PxzjrEz!>TkxHt;z#s!smKM}n{kZMsQS3B03dCu&K27gwBkGFfXPQl{hlmzB zn~gqpnU*<@_U5>^dJ=hBa156Lu$8Hn!}T9Xu%sque%)5=2L7SQ*Ix-_nZSibpu6Si&;t%#dn z&cy{VVx_AAZd}en7cD(=6l^{W4Qg6&E~yR#md+uW1HD4*{3_2Jj?TJj;*GF1ds9<8 zfs1x`jCEZ078^W#=4YjHoX#%d&1*%@j0AmcHqg%HHC)-UlXY|70KfGVmU#^~b{Y+SL<~YqdSlckewLs#0rt{2 z0y)w&8pcw|R^;;eG_!9(1$_#aK_I(BM~sB+G<(B7-enpL0AaMpGnmdYx9j(p>8|w_ z{c#GtQ4p5Y(ZLp@0(E+gIuRFP`j|%RE0gG?vx1?@d8f203y01uO{+>eeCSB7UEwm* zJUD~?+kB0Wz{AVSAsbhJ@;Xw@Ij9K<`pZDz6s81xjzop@hejFfwNhSCCCBk_>5}4n zQ>J&yBqhFirqZDQ*@7Je3~7X?hHazbwpXLUP&bFC!>vs~AB}@;<3iVa4{lZ-nFwmY z9h$pj6McJ-Ika9A*Yfj@1611uVLsUE;O`6~WrQ}Q|MJ32Fi>}RNQ~1)q_ZsLtQ{Hz z8koSayr2HCQX0Odi0n0R)2YTxBAb`vB4o4D+U8EM#BpN|&s55-pPnr45lUDK3UE!# zPVOkf?T=7{P@!iq_FG?wLGYrR(4K<%qZfHqaHrFaN>Ju6KCQlV69fMnn9SUG`z*j@rzk{#`I2h4Up;a33)`k$&uY7E_C!c7+Sgp7ajnZc-05KQ;=)+9Jwzz9o;aaj<_g!~coZ+>gHjg4n!48eARqE+Lt&@jp z>-*tV%zK)4)C!gIg$}y!VXP5&Gsb;0?x=S#?p0P$UCYL9KRmMq1<~fa7Giwh8M5^;K&#Jy}>b>>5aZFE?ZM*Fn zcVxNb{U(q>?BXIw9w4Hf&xoQCQ6;H?i@nko0aI$DJ&h7|*lL3tA+0G{-j7kkuqN01 z&VOCyYVl(K?mN`yc*)!ChBE7Z+2A(=`!v|gj?PUXb-GK*){WUl@WZXc)Z7$21Biz@ zg=U(b(|{xzq#c%h*!0=7#5@{IO2FkF7m+TIirr_S?)T%$C@!MA-g}Xc)kleqbH1v} z`-Byr0Z2oR!(*Vz5@TN8p$IAZaIc!VV%uP0SlVe_^Gm|W*vj2hdV=z;;y|P}I zOe%=)`gq#v@~)x)*_iKF7gCY%d=9OtQ2BIZ_(~E|TB&O(hDbF@MA>&#F-%jMOcBm{ zVU;8MYphs02^Fndj$Iw;i8{vlrgnEp^&fv+%Mh8NtC^!)y5G0SjvDHhP7-Gj0Wjif z5p^XX*msj-hL*T3oZ@Xsrdm$^WVstLeva90dGpr!HO$o(N{|%~@`|FPv{Z*cMNM=n z_(+8?d0W;C(eUVFug^ty>(2ml>>7{yR#$b=a95S1L_QgbZl?%E_^!W4&@+&;(=2%6 zYT4AyxDVe7oq;{)**y}zaF&Fj(WVjSm8IFX)?BLg`5DndN6zW-3?qcoY?}`q+?s^6&*T5|708A8}Hd-U<%d-UYcd&@*Y4rw` zwa|1Yjc&}u#HzSgsIC2K9R?p_<&X&alfep1ELw#ydISt=-}90NfBTw>8lNBZU^XW> z#8XjYut=9tD;9&`-2*RnOjJ9+d+zLsCLubKb@gYHXI;kYE=$Q|c3s_M*r*}$Qfq`t z@;L=6gU{3=Vo2~9^&}C$rW245#?OM)=HtODUeN7H!KNPp(CK@V5V>E09Q4sO;U#zZ zpH_SC2{s$2A`ywq(Qt_h5nuibK?1%szJ`&&JYGH)^`OHbYb&ehORW*3Rult={HZWM z8F@LjnREohUVcc$e!-jBb1}usN3hjWA%F}<+@e!cAoilx$23uBMy1TX@A*9)nDG{4 z^|pSJ!oqY38}DZS#A!MbMVvlITLKxEor(iik~pl0$;kyzUI)UIOB}&$DW%Z0F>XCM zbD9UIaxUUL&s?Ibbx)ZmLaJI(P{)03ns*;Z5u9cundM*yhobe42ig4H-{0sVP-v?Y z7=c5wOwG=r8o7f9xmqI!Mluo+2uH_GRv$~*JHC>OJimEUaVNUQ%f&Qx%1#wl2zqBL zt>~z>Q)Xh_cVxxqrgm-Llb%=3$V~`r>g}Hj#xs4z#%&9g#GWY?&r;@v!7aj(#3p#T z&s~C)Ic&|Ruy~^G2>|Zq|`=hFBb0Zwwx)rb(>{>2w z9z`UM&iT>yYcy#)QfcoqlE=93G0Ld1iYxD_aU9lEk-K_mlq#9PAvupW7g6ELW}*~^BM5$>$&bxnY;|KrA42&g`aJo=x)t3 z{eJ+ZKw7_get%o_Jbr28`&z$i!|qsJ-_QLVtviK)02B~msUm8h8mEd z05HM|3{?Bki>{o1c)0{K~hn*5~ToyLbq#Z zk*QNjMh1!>q4}yWGuU(KzDKtFuh(00c7*fhto;4E7qMP55Q2my*v7y=9c3;+xu!3hq89RS3JflMF?NOLbde7DzO zzK`YojlZ2kVJL*yg(NXx2*+9C8J6!Yeumec>n0LTeT_|Fq^z0MYu_JtEuz%frKDKe zSF-lzypK4DFsR1WC}Ap5A{C?|r3g}{iU=wwNYykDg=9h`VSMxSUpL>i;mACHptSn$ ziSGXZKlFNUE7@|q-_M^K>D4M-F8xh=eol{GtY0U8QTIM~ule4F*TDKRr73|70e}EQ zQw$0Kfk;?j2v9+S4u~jK001ct(gYd{KoBV)RE;N}zI*CTol^lQX>9zMTsEw7vER{GvgT)aI$<^IvkYE?)|XrRIj0f9{d zgaD(Iz=jw~U;v;5fC>NrA*n#H0a5`YQN{fK0Aq>n=Jr=#pJuJ2WO0EbWe&YwYv=PX zxP5Wmmmm6mb$Gk1v~6>Cm0`5)v-ddZJq|9*@g9_Z7GK->`kqd_{@&QvYdK_hJvL*J zyI5Y4=KRyx;!?xw@3iRk>N<2x;qxBVf`f*A4*9$GJFY6odt^DCwya}pO2uB<%(gh= z6K+@hyLa<8q-xgD3eePrZ<8i0Gw8oj^4+_Se-qiAr(X|b)1L3=ewFMq^9H=2bm`3D zmtN@A&3#`Y*Qbx8`QDE_?dR&9o)=#EAG}ni7-5D50f7J&K_F5?3^h;yfj~%Nf(=Lr zAOTLoP!K^PNb=(_&&%_7vG#LZ*h++su!w0ER?|Mm%l#gIM)SRj&iy>g(d8arN#SSZ zjo&{0r-pcsq2S7Q`JGedJ*@{E$a?-x7s=5>ztfL)v2$KoXmT8=VtHLR56`tFb{X1s z{RSgd)=jZD{I!N$r75t7>GqDkn089TChTq{&3ljYZLIb9_WJT| zP$X$1t8wM#Fa{a)kEZ#)siB+h zR?k@M#`i2!U>Zo}Qqjg$D^jZ*c<)yZv$u5?>@5C&)aP4tx?3a+qq{J^_c%13Z*M&B z=G6JS+SlPX=Je;Y^kVGZ(GjQUnZ;nC&?F?sRqAbaV858;KC8Murur@>)A_OswOc{{ZOx3)zO# zyU)`2e*GW0^Zh^PzK+9(yUNoz`RL7T%hUN|=l=k2%X^KO+!@DUR_nwxO`yO>KiR?<}=x#--g-X;pNkaf(06d9{47J9~1bcpe zY4Uvzm(S3MMIdNlQ5f!gyyMY@-graTn}(a)@!Q#la_{o>Upe%+AEo(jqv3n@-e;@j zq{8Kni){3n=U;cpmCJ-ZdzIbidLgf&eCO3Bj4uO^g9Y~tp$Dg+QUuu>~wYd_p2!|Vp?Y)ENJ^gpm{MukrQ7Z)~ zPI(-KD+Mb3 zFIL{2$*In5YuD+uyH+cgmm9yAF!Wu|F0I$2oa3=P`x?(vtw)aXbTv!FpD&XwCh)_Bp#QS;q-bh*l(Sw)$G?xuRHC!kJR%ZGxV3G<7&QZ>G`kI ze7{HX{XGYb?s-)?rgNw5$GptE7XhsW*RnV;mEXX^FG{mTQ%kI&vDhy1Gj>v?DWT_;%t6r z>bY0a{Kng#J$KIiPn5CCnQX(eZP(dmep&UndJlht%)V#py06rHjlalclJU6}w-cKr zZOM0OLNd+^D&1c}JIVUiHfv*kk+%fx)|~Ngb`kdTD(Q7J(@x@fRC;-8=A{9QZF=6% z8TjpUpNG?a*V{jrOGH(HWi-BeFj-+yGw+gmV$`4jf(e>lQ711W5HP{MzcpTuU5+Xo zajElq*Nu}B^<^)+ocgsFMb`6U&Up9S>C+}=arv3vRt~abMo?>UrU4Sz zyj{=BeP1N{Pnywe;^==X_TFRFpCH|baNn=5a_#d!v*g`PIFB>d=>Gss@|&NZ^|Ibh zorZfk$3KrJEvRJ9SyehAWw}{q_EcgM5<-|J>ULLVQYJ56afdyotmbE6i}dySr0^UQMZR(HinVo;U^nW=e9oVbdRFvYIFKI<<| zei-F7-pewV7`roE9X36>I|6HE@~?i1?t3#%y#vN;A8MX-?>M_=)I6qlr_JkLcc)`^ zO~=FY_pil_!_&3huTQep@4~Btsf_R}v3m@T?$P=DS)TsqaMo~iwFs?!x}M*f$nbq5 zy&i6-n`C{s*>q_12R~_NiY;vkTSix0fdaUgjT!kxeu5BNu=1}Y1+C48H zdGjAt$-U14!_o12kI4Pk0_Wer_UrGP_4R&TyeI0uTb_LP&>v=wi|F`#)8)RCu5$Pk zt~ajKD_La?Z}aM|*RH>JFvp>p=|7i{Q+%8~L|qMK=eclJ9k!Z%n+<-2_P+dIU*&FN zw{xF;9*%f3M2%d&jP^dR$3FN=O1$|Y*PBz*3&dsYR(cgs7?eQ}%gf4+UlA;UhA#DS zYxH^;tgQw(_v+~D#Ulx0GWuP1mU%m?mp94N63C3Y7bbEuUF+W?5LSD>Vxv5|FF#{C zaOGKV$L`xRZab|z;CfuSeh!3OjzYOu=-1W$d&1=Z07dRZ-Mz_(kw8~4>(uo8_EUrC z(c9$fxi{AC!%x3QXyojjzBHtW8?~g>L4<);XSH7|s`u*7VV2LUmwD}Y(fGX$$F1P> zKbiWTb?9(b`~LtZ)&58Bc`pxdo3~@ycWcvZ*PHrJlEiqv^^a#RXXibxfaZbceDsa< zW4b<7y!Jum{TkbU&eU^$iss**bDQhe)A8xi^?OITZ^g~YJN|mpE3nn8vwl_S@Q>BH zH$EpuGwMH_U}{-5UQM~(d`{`-^iQw#HoH9<#mpEoO>j8&MS0d(mF#qEAP6Cuh2?R| zR~;Xj(@*GW+U{d+tuGFBy?Exk{O9`lX8GQQUk_C;HgNLGrun^=eOy_s z+AMYIs=3}vjoXZ?dJiizIdX>T`2=kC^T#h{fag>2HX3POCewQwbsiob{%8r{%cFg z(+|_}^tj<`;C(hb)bgx)_!aZtPv@^c)2rLBqc30!wVHQ~XGYh$N160|f6le| zXMpa->#I?Y*L(T$d>ss0r3CFd^gKM#Hr&sn-!EPTVa%0QeEM!Vx>vhiE4yB=k0tap zIrHuG_HK4!i&6oh7HDybqEHYJl_0YNr#~@gjCvV5Ptkn$McKsz9k$+=lWFIA6<%Mn zN7=(+*};qQ{f=GcdmArT$fxlAT|UhYarjO*HI;{RxVc2v&ehHuztio*n6;FTH#Lb? zCfS#BH%e5N>mAc{{Ok^I&^6rOPtN^6jNkG8aPFNwv(WbMdOeylp>XF0%MVdZ`$3bJ z!L{$?&;SaAhGrv|9HwOvE9MdUxb^$`?YZwg_BlESf!!C<(PzuGsLJ}>wmwencI}+6 z)$-+d9-Jl_+;;R{4wUrrQ*EB3_B=Mxsgs=D_B#6bKbvdy?!)$LZsVfIJwK(*&V25M zZ8=D=t?wT-e$BW(jj|!gC<2(Ad~tsY$ut2c` zxjkF#G+O0tH*;NR>+74NN<$$COO!QGK!n(8+7R3}LtZ1VK2M$i8!uA!yd6)aor7eSp*f3P$zF`k?^;Ifs%3E`ahpOWM-=Tu z9hLd=^-rJubCP>Mn6sBX-JJ`}`DN>!SGUr=>Gvyb#IK&Kxv})4Cx>I_&p)y0t(?85 zZwUZN7|lz|laj~ZypFD%>X{+!%uCSvSZ`UMeY-S!a%q9n!r%TtAu6;~`BN%rVh? z{{WRs)47jp(Y*0^aQ?4V{{TL`&u?73bSF=#s`P$qEyeHMOI6=g?y!^7nGi&U-s2WuEiU z<>dZ%Sh;Tez8s%!^jx*xcNJFsm3>`Wu1;L1XI#Xq+q-AM#hK~$Wnsj|&6XFNLC=|P z)o-=krqo)&M=F{`_veJ{_Nv-^29hwddQ2=+U|2 z>zA`#cUL~QRy@ac7Pn0Km8Q-vx#ITny}pkz@8-CIn>r__#ml%l*|}+W`Q5>q?&-Vb z(Kkk{-=W!c&7wZs=W51}tD?_OJ#OsPFk3rZCXb&gjk2v(2`kD-ME)WOg~ zQiRydtc+Adua>zYRB@z$Wbk6HT?gIpt90$nc)q)PF$nd_9Hxvi)x7nyfV zOyl=8Kl?@0t4sW3%vS4U_TC=K2!myl+1bMcdidy+5F@WyCU` ziNh-^C0a-Yo_F%T^S%0C9WOpY0f6ytG-suvA)-soN`-SE^edS zk!-=~sVQftedmSh^M0*WxU@Pp-|XgAcSCJF;70B&_8yeWw`Z%)tmF4NUv9kwC(_`s z^f@zLAF%Gcn#&>`#y6HpAIB2Tstl2gBvOPmvSR~YQu_J2{{SyfR?e~{KuXd&7?}kL zMF>cmLPwRAk7Ow!TM>*La*t&~Q<=$X(k^px?fAJb1KbuzV8z(9A(mPx$Iv;v+6=l0 zq~!=yprWsDRK`cAnq20CpP{PrKT5mq{EBqnON7vF+a)>Q50BDz_V#a6={R|?u7@nU zUP6PlQOP-d?G_zdp6`|*hKj?TM+dP|rWP_XLm7xw z5=g62B^764DHdRyDz+@@zYS~8bX6*1TlXJ9=JKfS=GhY>l~)&W(ECn*tLQDce1t5= zC2oCsPb2lsPpA0qZ%vuAWad{kVR$^AjlZL~dY@0@y9 zYbd*Q>ACj$CvG!Uxj1FVl4GA!W01$ot-I{EdXD+qDSm^^g|m%H1F-A}GKUM42C9_g zH6g6#Sj~DFh)DvEjQIh{REms)-Dc%Jid*rI3cP*AC|0 zx{HlAtul9X*5V~*v{>-;E?+8}AjeyH>sEP2y|3fWm4(;tP2}*=cg%9u=auJ``@D{? z!Y1zNT-|xLQuFDFr-OBtJKt05IVE}_U5BYaQ$kcHoxE?-eIM-pYLz8QMKUqx%yG_1 zRfOGg{GU+j_Sm>Xnaj9yZhmO{y{T`Xg@r{`W!VcY)R}cc&!MHz>cVDnzb5Yvj-8F^ zIpLp~W3HYRhB{m&KK>~AceB5B{{SZ3-@oKJd4$NsTy%Q3M$K)FJ1TR+(@u4}wa1vl znFSIvl-7BkpI7qxb9;4lP0Rj%&B=JOMFKIW~@ zZN~@f*>6MP=(g}VmTkIgf%v+fN zhGopB2gYGB!m}K=oZlkf9_O}YMXOugQs1Mdp8EFrc@TBR@>XTj4(_DOst$c^u|>>M zOR07j6aw778s}lpB6*GM_2Au+Su?cGI41#Eym59$@~vmG-seh=NSj>8T#10dc|~E1 z*l6kQ?XJz+?1ZuG5~7<4696ErN~Hv>*qLLD28*P_D#0GcB#SLPV5Fk5UcMnTwU)`- zdh)Yn##vUQLKe>*9Sfh$te7($&L)pzJ?Q>; z^vp~r>(#T-=4dk2kn}#U){+&k4he8{<{RFTV(A}u%-Hyu|R_^c+jW;Xjf zn%TBp$_@&Nrmq-uL9tdlk(PCvb{;Y0u~`VLbTx*QO!c_qd5#;LcW?VU5(ST8N{ax( zra&46MIb^lt%(jD5+Ec>w26e(9v7U+j?Ps_M|Zc< zCQc42>`JKs1)L|$`(Cx#^Y=TvupNk6891&pmEnbabPjKqgw)J2k-M~Iwkk&H&rv=4 zpG!r5R}QPr@8{cNeB0@DY*Poa+7QEnsW{=H)#p6LBq)KeXH4kpr^_6FKO@^RY=f6Y zUVR%0ae3stOJj7eQ^cD~9S5h2MC+?^?KnN2*Xrs*rpA@DLWo7En4mC((h)Gqic~rs z6a)%LfyheL9J5|L^E;=}`N*9g6!yH_$3}x)=U$)Edw(s+99I=q5Cc@A&PT)a`rD4fx8{P9 zB&9gEGn3)2zg_4WK4puGHyJIkFzBt$t1RR8daikkuAZ!y+I-E&f$d~)-MPm{FwxN` z9cs^cA%OzOnCbNSW+hUwZZUAkT$h$fxuu(XdTFq-D!$Z)NuZJ?wMz_)Yvze|=C00# z^WR14KYQof*i5ZbQL!qRLloLbN+m-Qv&eiz?{RA-?rD=x$pTf7?PGa#xs*+gdJ`6kD6(WT(H+{@6K)Q zE3%h7^WMI_#|NR2^}P7~9(~u!eLhdKlcR0p3{oi@NbzmKZ;9 zLcZBso)=i+rfy}r1{xmd?9l4uuWdW3Y}CB-dmhZ*Tz4E_b#si%dLJQKj+AWb>fTq# zdKc9C^QUf4p~?`cN@#1&Iep2ReI4(i@*znQRYw@d9!G`{ac?wy+e{IbRZ>z;+P$5z zvd=;5bb5;&d_U0FPlge>`*U3Q>NC-b^Hz;=k-BDWk8_Q7 z#LI1VThn(kD#WsK;+s`Kc3T`|`c6F5^K418SL6G>sr0;;I(5fDt=N$)kb*>^gsQNh z4qCBN9j+h~E1(@D2`dwjXv>ags_$2sX5E>)^*5rIZyol$4(+Yab$+cymRc-zRA;`Y zJhd9R3UjouZi>CW)$cblwsSRfO`79l)itSk^Rc$r_@BBn-}zlzM4g)ToF6giUtjFZ zT(T(Wzz8KxoVSbVv~xYDcgq-+B&j&wGmzng3|rn`nP)5;sCjLn&$g2Hv6f8xPS@L` z9pU%%jy?68FyYX>%~F1RkoH&?JDhg6vp~l-raC;bZ#@!c zPduKbwqA4i>5jpiwDeqkx@>W~oSf^E(obeuPZRbYN9%rL)ObEsja)r@+xqV?=1Qi- z!G~x~U7gUS@&5k+tM>fG*GG~A8!DrY%y{+~!@_Q~e9{=UM9Xe_KH6QK(OV|_99rKC z=x%+_8!jWamb1n5_Whi7UN4$!4(^qgR3;K;vtpKE8@Z%HGjnB=HzrxJmQk6N*#_lr z5lEKGNlac-q};R|-b`DS7Jbm$FTDQ%W7V1Q%=9^sq!Ki(Q9-d%fxrqDinQcXs3CSN zRyEp`I5cPRab0tjWre<+qpUURj_?W|VZ69&dyzjZx`rnyx{XE&ujnQ(}q(In} zSaoWO&zA4zziy56zm6*ytgMSVrWdVac>XRihPZfRxaaxgHKxd!?4i%LmoC_?kNaHu z{tsJq)y2;H_07{VUN?VJ^nA)M7tKmR%Y2L4tbEnVLPTUnHN3KASi2+K*k>CpjIwU# z)Hca=X>zW+qcUop%PS0Y=rnnfG1Xm`+C2RTd2O`e^*v>;O^>O_(AWgiU_*tW+Nl() z7hn|!X`-cZ6gsZcvIT83nBFCX=2=Y^zbVa~*zDaGhmuc6mQGtdv$(^yrt-+nV<&dL z{UaK_TQ9#nhJQ%xH*v9;!)evw<4ns$>{3#>E;~#Hg^Y_TgO@|D_0;MQl&_!U8+Q@u=PBS306$JaZOX3=PmU4Gj{gj_HN$t@auX! z$g`7MhpSc3uT$N-568zz(bDDiSA5#Wte$?%xRWG|tp*&u1>#7@OBqJ(3K_9B;jhrx zWtX#a!xzKub3Z%u90y;;=lxHQdS)iVl&m2)O`bhH1Hk=*2lQWy(BrmZ!<3b5Yn9?% zl9olNhvubfj;EhJ=QmF^?9+MCb0RPyzm$H;RW ziqzatv8F7tZ!N4`;APU@ebBPmp~onBVpwac$5WOBw9l#6EPYzfEv)CB*H+2d>!-^; zZ+1$_C+JsFcN=Zd2!G}i+p$Z%pIx31Jim;$8RLx_T562#TaJSRt z%iY(9%dc_e_H++V=HP4Ky4BJd+4NYgSW4C{%NTcJ%bOoTjIGzARyr4k3dTOheC58+om%Nk zMamvov!AM)#w7|=iIwv^jP_G%#s*g_vuy@_ItJPdp_?swC}xXXHfVCNFv8AboV=N> zo%5ZO_Uos&sOoYZhjo*OZ>+q2trxxJ5~LK&jao@I1snuYs5)4*RI41ZaJ6tC2^cc* z#5J%v>$cJ5y|=R=vwruFUAI@x*4eMq)2Vh;?BuDO>`hRSjJLqYelK^wV=)&A^F{8z zKD#Z|`TAD4aY1@i*-5iUMsVgQEkxQ}KBSyDD0f}wUtc@J=z0$q)Xm`fdbCq3TsEV> z9NC1 z)}jQ7wmwtgrSUlb6z_aw>;godpoQy=F(B4!f+s1l7TQ8La&3IJh_3PmfFv~p06C=P^(SjJ-) z55i%xTIlj^w{_t&&!O@0jofnb9u|^#E9J)Opufyg%9_Hglj(4Aj**!02ZhZ;M6Bma>yC-L<YIY=M^8dwXj4byw%JMRC8D29G~c&*Z-_JZWW$GB%A}qEZUvn-Mc6 zZKHW)!o|yE3)JIf#TAY;#?#KL4r1iN#7(rB`W4$cuk5jy$i`-H%%EU_kD+n1j@n|%r#$NQ>gb$) zmpak&UAxr!N%{31s#yc$%3|SE^2td6RE?w~CnXFwWPLt+cw{?Wo5aiKX1Ljh5Q1by#BPOI79kpKJ8~j?ZUu$E|&24hA+z7A$p52r6W$iXh|& zk*J1Nv8^H%(AecWZqeAjy-P_=174BVTiCoSp-F-h=p33ENgZ)t>Si*DlREe zAflzpDKikJKD)f>D=2{$q(UK67HDCL4Hm1VqUA8c3{`HdZs)O{W{(n8um0o+YO%ob=aRt71+VBW}@zYGQcdYrk%1Rt~W^ zT)^sP8tirQT=MJ6XIY>g2XwJA!X z8YsJ4mF05qyqZk0zmDr0d};%uqK4_YQDacZ*yRwqC^m|fyLzy=NAtP|UyPOaD-Iv!Qwc3ZN@`GT|gO>x;I?Q)W$ z!VswtjgeL?7*+^7L?~Edpv2U5c@;*fk-JqPQ)(nltq2kbKq}W5uu{Vo0O-Smrwrb_ zp=C7rNgzbUe7LiT>zT~4wB_;iTQ}pRE!}Qh+Q{50OB7dH@Nzq_N$}VB{eb7sZg3wr6e$e z2nZPjNI?mN4bvrjR}?W;JyT1&Y^3wNykjQ1ON7#46oIjU0z!jTBZ5^B5K<*EbanxuwM-av zge3@tY^TF3EU^|_!-0VUni`nkRb8t(403Y@a%54j>er>Z&{WHW#__p@CC1N}$mo0e zc{e9NMD6RTWmV758L7m#ZL!C0Zu#$Jl7Oc)X|d^-j&t|kIfqnz`>gLv87_&uJ_fs| zs|V+BbWZ+?J`Q=g=wfS^t4Cz-^|w8q>O-SZ05GixY61Wd)R4kbLJR>ZB4tR()j-8r zS>mk%+E$@5^7&L5O|j{+_Ee3C8ygS^DmYN3ByIw>HXVzl49coBWNl6rL?lWfT0|;K z$rLCx7ZQ}kQK8FCqh%FXVU->>4Q+Zf-{%^VqFh9AtaZ}mZn=D2E>k0;M$3O;Y|Qj( z%{iQw=PImJ%}%m!EUC8l#nbP-x2@h^TIYY9tMj=#s^5Q0`govj>A2+Zb{C?DkC&kL z^=J7s%DU;bb4%5)uKGTf{{XM(6Go**00IR7z@Z2L1Qh^-2q1u|5GEP2I@THJ&j^be z)!59wPARgR6VlS_A*yUhkrImtI662$g$Wi^4xq@1U$6-YS^!}PM5}O$YDCri0KkT; zqL5msP|i7FH0+t4H89n69omAm6;X@E@yu|m%3bq#$=A+?J6?{Q-xj+4x*oZyQ!sKD zX`@$dZ%3xit%Y?b9BpTwb|z{&nd7Z@Tlir9I)sO&xg`0WraNsl<`|8(8sFv@&O@f?b;Rc08UM2~#0Z1gfwN ziIu2klPZwy2<3zjO3-2fVM>7t(jZBG#N{wjpr}Hq!wwUTqr)(o9k@3IOv@EBM>Uwh zBXIa}rzl|D!9cmRlcr7%Gj)@hWTh8k?U9yGvv%gIlb0u%J$5c`lafY9M>0KE*6XdD zqKQ(bh8REsgbHX%XcZ`-AqogJAkiW~q%?&xBsyTOjQN&G#It2=!^-i4t)+TiHG*L@ zfPe@(NYtQeRWV)33{b2@s|66urDD|pM5+W04TN9Fswh}M6=-5$Q3?ep0YpqHvPZIt zF)wvD0xi|)kxy5mnyC6eIAGPXqM^19)!JEPMU>10ZQAt|8=R-sgs zM54sX&4nr0u_7gE7m$KPG6;liT2hPVB#Ki*vkYx7H@0Q^4o|2fkH;rFi0&-joMN)szU;mBmz+? zNSH+ukjUA5#N`MrTor`|rWk0YIsqV+bk-=uT7rU8u~HqbL8FqLIEs{7>KNiK)ymo@ z8z#fbn-waB02VDoE*!DdFi|K&gQ8PF5NZI0M=2qRP#OR%Y_WG;UQ{8l#t zqbkH!reT&@VX)IKt0U3jb5C*KOX;BHDsB}-H3%4$d194Xr3qV7LqH)>1(7(4k|hjC z%C>+1!~ixB000I70tEvD0|WyB0RaI4009C61O*WW5)cy+5iXv0RRC70{4(F;UfP4!Jp#C4DrVd3=N=?2_|D;*fw{-GXDU=V;oF6 zB@nT1y?Nk5P*3B-=hZepdr!N{w)ErnEmRA)4DrW)23g>p+mt+(-uxB7!Gr8-FlUjw zX)S%8bXZPIWGQQu>bAo~cqP$|$C>QoXA`kEZ|Dq)3sgKn_BUv%Jd^3CzpOj)9I)`TrRjE)dv3nv_fAlW|CIjZeaV27~9zS4q!-k0>XM@A!>4C-)(zsw&cgFYmr{ch62bdhmZZAC&wcrBOrwPcy)=H z>P#k0-&19nU#OPr4r@!NgSv*%;N;0pf<{MA+mbms321>bLzW30v4OWE7#K)W5*&}T zY3)xdPdN^27Li6hre4f{55uBBoRHVr>_ZXHw*wxdwZBcX{#pfc_42%oa6Tp67{);Z z;9bFwkYo>uhZG+$I0vU&lBO`IE~~*g7d>XK0$-vW!%pJ z#fMjcCPG_HrTMrbKwPy*mfS?gjO^#Csu5n%q-_cywB!?eOGU;{B<&sn1eb3I!0<7H;77#NMZ?I?42*;PT}}ptwEBfN69zngGlU)? zFuv7(a8JBr?ZYEomUN)0bN>MAob0X{;C{>xkYfP&7(WMqrGrpf#jX1gM5*#^U1GJk zshMPN3hK9P7hP7|Ch!b_kWe&*5LXWzF~GQ=4;(yy;6DZo!JYxE8=&4A(&b5Qr1v81 zGCYDKAYFkBjkw@oyMpuJX8^DWcq8t6aAU|X4y4pxo=W9fsJrn$=U98h!tC*3P)tQI zUHIz+b2PZG`z{>rc?NtBJP(NjAYlE56riO8F=u@ixLE3GGmb4Yj|XN(txK;vEy~R} zrw%k!w4I{>!G#3{1$$2>3)p@E4?GL_58k5+Fr|gM7U*1!qHKjum`T7dJ_bi_1^009 zJPzt3JQ9AiKXGBMnYeEbgJ%O#~>>F-zP-;i8$?^tw8F48|JCL*6?_aot{%Q|p2 zyh&FT0zC1+#{vdJ$S|O813ZUZOKGvzP@;+2nstfCsCq)Vu&R?X)#+jdL-WqMnV2No z7#JHy&}EQeUQ`r8WIj0O-j!}Yet(3+h8C;`79Dp1*;_@GhSXNSWkXiNjfg{p?PYVrD)MX+Ny1UC-@f*vy}gq}#a{QEP)Rh&pdX1=YCCi;Ul@gh}?p|ey#x;TN` zj0a@9AmdX?k|61Jn+-=xWZOr%ERhTY;$c9+g#{-eMeZT<#{+1IOK}6H`^O^lIc#-s5_JN)J zGW}0)7?|}ANeKR+5`#L*1EH&^4}wxwMnPf0C&i?aWTja)@{QsTNbbtFAb3CijDdOi zNb;`$j1o7lJ8H3;Rms#$X-bx_R~+y=j{_k1co^^Gi^&&f`xcI0GKt5SY?pUf$bV4O z$2ax04032?p~o6*NxH0LxPEq?1T8-KN>~64izs^#bX@t!H$w?e(kSawSLv z!H%4ATOg8UAf+~dso4saV2#(+A5PDbBk24H^syq2QH009OIZ!Z32Osx1_lDWp93I% z3?Bt>Q3D%^B=y`zLa&yv^|6PnVu_diI7X#};WdAz$T6z)vtgZkr<*4A^%}(eV_0$G zTf%u=OKJ;I7-7{Qe^1bNk;zwr%{927S)1PCG;~*h0H@vten^_ zVI;!!E6$7iRXl=dl01iqLOy zkpoD`z%hcybrEJsj;F0PT_eJ@)WA(ogUGf*ZB!ABw-u$aLt;1Vz~ z!1$jJBLP78C@3qim8e{BN|(GG)fik$35rgVkmxNEvkTFTq|Zr3A^Khz{V@z4my8vq zTOXxDw7aA~P0JuvR&4~ablJ6isiOpIJw|}mySYh!$dghL>Oum2M~FYhZm7)c}%?uMVbDt_p( zn#jNZvOM!y_XmFrZ94u+=o@!gQswfoS`43DI{7IWgA~ zMOw2#SET1#L~qmi>NNUx=trlq$Ty*DTL{uMO&Td{nlz=Rfw~Q5V{w9P%o6~aA_UQx zL~xcy+yewK3>a5(J_;EM3I+@)83hn^E~TkfW|uyvWT4V*9vv)^(YzY3L7Ez^XmvG6 z9aVaPF%jUp*ya-jmWJkA2R)<(TeOgOh#+^SW9ts66zNI@B2YO}NFAz@XL#B|Zel1V zd`AH;I)^O=4Tl1v%?q!wF)pH6w%H#mA@3E3_K@%oz)0KQJ^YMr1QmfPZW0o?Zle>L zfm5#{#Cp+DWLx0z++!gmV2dNc_c63&eB9+#EO+9Ng>7+?{fPen;b{K=VJFFFdEwK_ zRPI|!)C#I<$*nJF@MqKO1%`b%EVH01TMcO%hJjL6G!+&rMJ-K|Wvqz~EE6D@z)M2_#sQ3QF@St<47e+S zkT9TRBec1#>{|fsb`Pz&hqV?0^#?HGv1MXy>3IZ8i3Sl!)G#B5^;Scgf>VK3l9W2f zjCD?qt-;qgKBDOYwS0kG$RJm5kVc|(f^{e@M9PQMtKS_hT!3^?91T&$Kz^~@E)BXt zrr{z%S}6=G2oC-=pP3JkN;M}Pus#U8k#JXXcO4btPPL`Te@si&lbjGYZE7F5U?zfS=n5K3Ze6ZV9Xzm23xjjnPbaX^JPStQ*t3tL~{o zsZi%E%bKujP~Mq(})MB zjDDX``T~K`I1Zii2Uj46K_G@CwmR7@gk8!&Mx=cpwK~JPiAzExZBINY<4~+W>J3;Q zkVx)eyN{AC-Ub1TWJ~`5M^QKRT{`oMwP#s8g|y#{4+ZhyhCcM*d}9E+g(G!*bS-I2 zN6Zveq!es~#B38RhS-UOFPe4rDq*sSVW=4iH zt}e~9xjcZDFg-wE+P2aw+h^(T-qAEUW0W~#lnJ59321WgHsE7{c0XzHQ3D6W!SMJ{ zFrcCh7WG%@$KDJ?jRh}AleJYd=bJi8I*vAz8H0#pqz4cjLJEowAq7PpAn5|32VtZ;5jk9NZIJ^&*ft0~q=F&v2xlW453uk< zJ0Wz8!)o>eRqfeSxnEe?i(#1X2|P(1w&Zp&N7%ate&*q=#!{5YSo&PYjT(MXMw)On zrAW>`iH0LoF%caYD5M)_1CTi*k~t%gIUTtm*#^gDvL!~yvWMBE>~^&KMNUZC9HHuK zTU^rX*>&l|TV`@WGC3oZIeoSbqrg8uJUnneY4K45E2izEiSi}V2)*Fq0 z)DxF>jd#fC!Ht7TL8?>|RJP!dsmNI5p1T&K_86mXB({dt}^$jl-Wd4k3_xf_l zi``(nY8+aWx;HHVQ1yKEZ=s%_%CPo6G)J5haf&_%= z>@whe!T0uI1>ClSK-dkiZ3AQ*BajV% zYy)H)K-dzUjT}8>$%iDQ!so_FU9*NB=N!UO7|(CT@sLIg_L(GO0Y%%u+F06~0 znHp>CH5{c#epTNtiInY5;|Iey83bfA*vt_Ez*4q5MA;orByxLf32V7FHVuPlz{l|@ zgXNN%j@D?}{Yu0m1PFSlU)br_veQ5xFIk_v9DYZiVfn`s3M`Kt40#8@!n=}-x%e^$ zkf6=1A`MwVGDEGpTTEaP8HA(}cNrP?;9y{F7#IeDH&9nWIa<+92j|$&1q{e+j*#St zkx2C%G=*xm#Jd$wnb^;;cl?{T5AZwFMd04K-KsmR@? z;P#uN&t|Q<%^|At>SvC=3 z>_d&Iqc{X(UFj$Z-DEz=61(sc3y}147ctx$iFvSby?mS!9ofa6APsXyy@-D14p> zenEvHnL0~vT|oPk*19GE!j#A5;C4~&!0rkZ5JAFE(Xwo|Az)-tB&nt>+G z@${G0K8p}^70m-xV?(R07U_)Ob7?DbL&ipNk1-QQ~<4!c%%|u=> z5<6yF4nHx|v3gT)&T*_@ZONWYEAAhV?YPZ2EDVjK@;OF(eLZiNS^OCGAH2hz#G)3+ z`yl2UgY`W;k2Xc}Loia#juEt1fO|m&KE6MFi5s#FBnOCLnGcpjE`E`=?U zs?dLHwvCr)upncjvL;H2w{QJnbAC05rD0v25 z$HoQ$g9;&>kWpiTlic_iG6r3i&NwC2q$YEzA5d_0(hN4^yIfo0$5uI+86ntq7#JB* z1WSFf^%+VCg)Um7>BSggt}J45;v{h+Q-nEfXd>w698`Tf36gFRBy`3VAG48+KNRr% zMF$!Vv4Cm7G8Ne6mJAT`7T)n&Z#s$ljbCr58D$$mjkyX}+LIlDb}s`NEcZN*wA3<1o!21s*pkTDsI4wI&9MUly4rp1xv<113&~_;eX(=oksa)FTpEZ)Su1D!p zAEk%shRA-KWPY8FK7ka2bx>Oz=y3tvv}2iVEEbisoN;s`L?daqaDpI&!U={tanl2i z9E=@{l93Oz);29j$a#QdAunRg!gzMcAu27Uj{wZb5#Y$nKRC&E+>^jZmD0qf$=^j zSqH?y@INEKG4No>c`W-3N0OOnGIapoeIVvb9bCf(?2`x028RbNW0IlBG18fih|Cpm zDzt*DKvgtjN>K@wBEXO<Fr+;A{suu%uZ;_W=J^*><@`we79dQ@B_ku}2M@_ar)@c0M7#yEWc0QJYnKETKr zPqAmWf`NqzHOq?RDq2E`wxNeP%gmyg9Hp3vN!iJ@;n1B<_vk_ z)1)QWGaxf5=nh7CnIxFG35p6ROmZBe1d=W|;edV?j(o1036{jumex}HI8vMLro+*92{8?|qiljcYe6y_aYkthJOWW;p4kqmq9c-(0#m9RB`9?B z49X0F^z|6}YeS%lL#B+7l{SD+km^V#T105lW}Qx#LrVNh-eAmGT*s?J@~rg{r8;(u zo%KGhFmu(D-TweBJPXy=Ltd{I_};5jy059}jZRZNJ>>?qEvBA!zTPdXY7YsXEe{K; za96J(Bh++}YN1+1^#1@%4;qCKVk4NV2ofH51az_7$OJZV2dTd_?R;94&`l} zTVy&$uEx`l#ZB;z<8lyb8v!6}9I>!$1d+6I_Y=%?k8j+WN!(`7Dm5%JLk0XM7Lg&W ziE+N=Gu=#uG#rn-Nvbk5ytY-fi391eK~}~EUlAafjDSuyk<^g`mXZfxf!6K>x7G?g z)r8g9EGJLTP)?SRpp7PBts>BCN=G3Ug@!{B64>$X`34r?w$vP@*j`1faiYJ~!Vje@ zGGo(<)n!pcQjT1q}oWF%TvPByvJo1lbTvK?oTB7u_bx*5z-xX@8NV{WBzz8*P*f14vUr z$mF2o27uIz8*gq2b7;BKmDlK-rY>|-?b}4vXGw9_F)+o)m@aKPcd|A1r+ShbZ#!cv zX%H_DMp92jTF*|VSCoU`W9Nc_hcmS)?p`<<4+b(1iG`L)1Pmw`Fnkmph}5>VW`kX5 zY2`Agc1=hP6U-BG*p?&nA}Y4zoKIyJ@NF&y>fLgmx3*OGG;W1!PoUMFCB)Jk zZz1^j4ZLlePHrrX<5uWqr;6+OF)d`gOKQ~e8Fy*Yygg0(o~}RK`9CN4>66>6mJ5md za2)fJA>@pC;AO+)9~15i?KALT!GqvoBeIKH=wok%b<2>{%aLtRwirbi@lhahr?C~}7=a>p!k#=)?1yX+eV!L)4bO@c@w1^ily;L@wx zVWG5Qbo6x?&%DSuoRo$`FPmnS?jta@U~3&Rn|AcX^-imt_Enzs5^#Q@Im{~m0P^@T zQ_H3_u_m=!Hk6`JbZ9J13V9jgW{lm3>ZGbx^s3hi_gZl(A*=*TxHbi1W8SW z8;pvLQ-xQ~G@MC@mY@8%20+pcAln9kuxuL!!LV!`zWWBjux%TAl19;ifJ8C;ch?00 zok4bSOViC4^wyMl;Cu*Y+GHF+kj?~o29xGwJfQ2C6WUE}wfZ_+Q3>6u)$a3Kf&Oov z5f5#%o>k}B;1P`b4}px20QeX_<=`0e!{8qiAY>|RRa*NGB2xg2YkdvO9swxQbh?po z6*`tCqh*^3+J03or)FS_W=jQwFXJ!az`(%THjSfiZN0EIj0_A63=DS|{tq7eYgX#x z>Ko<9KTh;<%5g`YIO1N+@KFa_$dcK%Y|HGoHx!o*BGF{)R;xMGjk-@Z!tlLBWTs$$ znEd<#UfE6scm_TKyMp$7nGYl5&ja9ny`KQVgW!Ho$%{%^6r)dHCNZ-5B%{D18tX*w z<6Ug*2N5n7rj2PyE`{nnSf<5g!7VIFM1sfg;1|4zV}?DL1YAqojBxn&e0dn{@3F9I zwmtr!G#-o6PHK6*lAQaq?SEEM#SQApSKHi&+LYc(QJi-p%*dYiY0qvEpV*?zDF{CH zs@%jc_6NX~_AsN#E5>*k@$u|1d<+r$JeL#TV8_NY!2FogR~;#8x+T%QMW{^z4U1FT z#s)^Y(Hl4+SW`$=+O0;J4O{7gBCNW8z1odHiDDI!U*Yk?IsX9Nk)P$2Ex2rI9YfAN zH>YK5TS)PmgBbmWGBfSgxh*6UA8wZP-@cN09jzl|jq_p0=59=vGNpGV{`idXvZ>14 z%udowAA=J=)IzezPlNH1kbE=P#zsMm@GyK&hB%pV`7*(nG6pHFFm#rwah*><_LrSY zeZ1=KQz;`PV_fK-oZ%&>R%ww>Ep_O&VzTMji<@5>>A20vg)+MNNOH31jAV>>dq?xVlU_g&k4~{{YH4WJZgs1h5aff|FsE)t+E{K20bVF8Z zc$&PU7$&)|#Z7jQ)Q)uE_H65c-KULchEm>+uCtcacA6V9DjT2qkzLqt$qm2Hvv<0ng2kTof zooR6V`C6Xd8WX8*9)I;6(KNkjJu%^HyBUUBn&WhRUu7u=sqGSpdfLJYVr!}MpGLEm zr~d%tO`;aHi2Q>t*x+NpFh|_XEj%bM&80Zo7gh6Uxl$@#pIUVH34IN^m&*xSb*_(9 zL$vl$?jk(g?JH=Q@io*1u(sciIFuNX8s;3hvb40y8~#Xb#JZp7F8=_)em{@T9~=xV zWHklKD#`D?8I7l^k%k<08qsGh zw$VCML+X;ZI{UA(iSIPavffTa z;<$xUNYnTHgG!R(fAQzqUc)>BJpTX&crAD-_6@{pe3_co+JcTgovl75793D{(I#mj zM`SH*3EH}roRcoau5IPCHfuylPa=h^;y3x0)=1e=2(FxWr&vj_4pYWvn3?9B{VOV_ zG<*|opJktQ4~9|AdCeTOrFvs(o6;(AS2}R(ZjrSO%5wpS$fPv+H)!stQr?}^Sjmqb zQ!ziNF^L8d&PmKHuLd@#%+R5<#>EYa9HkiLC~}q!rI2`yDAL-g`k`^^A{~#QndHFv z;;>`qn6`s1kbRSA%kLV0P_7HNP=4(BHzb5$k8FJDP6yX!HL7qdarOhCRjajEScCfs!J5{l?sc zDcvaF+EQ)gO>0WTua+rUYSO~ODH|2_@-5}s)5@fFzB?{3jkkt-p$=CF8HGOMKdahBYqFtO5aTPXs6c@y{^O6Pc zz`(%TV`%I_$!s)kT~{(buUa-~sL3STgr&VlvZS=JJyfR@*jrWQTVbA!vt@3$wpvp) z^3)Et$@NpvE=;~ks@g@0BuKw)$o0OdR=+t+(@Bd0-VBH>XCdd7wZdtMELg)hGne+7 z8hP+hC6Bqax}qXJVn~dPzk;xdM+OflX~-<0*qVbuy^#lQp{v==YIQQGRrUr%FZ zWS4DrzaCwuG6Xd;lqtrIMr0OSg42NIY>E1H5VvXD)|>h~4LrAH-neA+2yU9%GwTs+ zDYlV)IVi2wN{Hn9sMd9KE*lqjbC{2Haenjht0y6NWqUB5}~X#Qf!jrYGue$+q91&JCGzmoSQUHtlm{tuGLAVxlFdC#$w(p%)LPE zWwyZkEl!QAbbHh;jSsDkq0LpLS=4b}REo(h6L)p2xaDN3>kdKPB~Q1_$mWnIizH-a zk&tT+J%Vo0$ZoGztbVF}K=mxM3%0T_Q!L&js?pO_TRqQJ&cbQkS{bQx6%h`x`uVM{ z{_TdNyhpO_Su!Xz}k)fMRU7?GTI0l~l1Y;oxq81= zx=Pim?nZvq7ZNe6G;3E)$d2qMouD5*S> z`xPYk@v5!ml&L5NDvvb=VP+j(1JW>Sm%aUGE2`8UmBH#Y9;!yoP3Z6XK7F1;f>68g zuPO)SqY&ptMPpXoNQohGY*kGGx8c!M%9ZLhL~7kc{{ZwYXs8Wa+b*7=rBG?=hoxUr zJ4s<8O5Kjs&XLy|i7;3yEy%Cf-K%V*$1N!**h-s3H`S;CwsZpzZ+H{e-yWwDzEP~43^ z?M2FEww1Lti+VRIT&KO|6K$n6;i0w1AZm>(A5z7JPOx^B>#vbmuipCma2k%I zWmYniZ5_WH3@p1uuB@2dx>zf4wGs&~rAN4u65%K}9d3skHcZ|mj%Z;HOLj#}sLbif z54PpfCp#IX1^CiaoQEFt-c+W{ALB@QrBHD~LzpWGm^g~sAA72o=`=o7X1GqxTj?L| zIDXeeqZecq9TG@+eV_v-CODTflOD`isIL~=E& z6E5LtxVI>E#^iF-5pF5e4K~9r*G-7T9a7tw1pZRBk+sWO&SEA?mtUz@T?Tqzp3bRd zG4B=OUvo4rG}?7akZ&*W1P{YMw^ZhbuS-!+sH;Kt_C@agm6OeNFPq3@@U~QZ%Y)f7 z)CA=o0g4Zq7yWflR~t9zFlb7aggA1l|CezH{hxw<61>&29)WkvKfv|x`s(v z1{9#$PD10hxeh%M3yB#F<>WTDl_O*c&9LG*jG_%h*s`3|=G#p3Q!+y3w-&;J&K$XR z(%MC3CH!-jWvGyuS1hHAb$F_^DwOJJuURzTg?Q8I-A1Dhs7-eSw)xJ_=^29_hFpo- zPNeB6JyWVo9YyJUzMEF7$UZKYX(I2!{{STg2vQw}`XiM1jm0J$LCGvQnXjfgjhr;IiEg(38}aWUeNK2Nw%o#zV79q3 ziThh4s9G$US=1n#jjLNxYDZaHWSo;aUOXaqR3Olmc8pvy8IVXHsBE1}i*1Gz>}|Ea zrrL5m-7O^kptsbijDAfY->6IWb*Ju9X~l2T{=**`;hq^85~DLcjX|Lwty-3wL^id= zeKL)+s<&#$q{HXA7R6l1mph8ufw{swx1dOabeBt1==LuYCL^$)hi@ta6jRAaInUF| zn^zv$wlYo7IIpU~CtId$7Cfzn%d*nkCs2q`Ua^jb6%k8Q zvXWo05{8|CpV@I%3z-m`N{X!E#8c8&GEVbQte;@Q?&Mj$5x)oHO)E4`7xf|9PxsRjdblRnUc%NYClrH=)@=#EsVG``r%aP^3&Wi4zNjqOp zN}CnAqT5vrBr)`_)7aG7%TA+DONwqX3j|hG$YwEC%w2QNb4X~t%VN@Nt=<%OVm#BU zYC_pec~cN_qDxCJk<>RC71DaBdaRb_t$k)c7`i^OcLk{=%UY4%7mh4PIghGbR>vi& zEhSklHLjt#b|%tQK>4E1!POO`CxDZ{zD{6jAKcN}J$%hHd{5ouhn^dTs?qJ)qEQ-@ z%gT{-=Fsd=Xjr9hs=oOxBU z>4RU6op_URRb>jky#<7*I#~qSl9q)i8eCF|=hz8{E%Qx8^EUMDi=*`AhF7PhGDSq; z0-z&FRWZD)AtW7ba#D!ZEhY>~$*(1{!#cS&&P%Xejg7s{voH{2BTFm$6NV(8XNz)) zDT@>Cb$UXWt+goiN-;mBE2Z=n3c|gH9q}k#7iE5G3lt1>SCZwFY0?fww3SG)tdkE% zIU)HoY$#ogVum68Sdjglyo=`)@=z5aN;&K~BTf{D3Nx)OgpDLb!J&G&+n;9?Kn)iPGN zC`y{TXTPz8lBHZ8MGcWew7sz>ZJR77ciC`Rs+avorjB}6`ApuXe4hf-+xdvDcnGsFYrzMx~4aK`?PB4odi794armU6KJIiX$ zLm`Vo9@cc`3%N{{RO=B;R1e%nGu$5wx(L^dThYs+knE0mU{_te1mW98v}M|vRm!fuZa_TY)}zlWS{bm{ zU#Omnsf@+cF`Z>m-f7NJ>Bfn7O*D*bPIi#N4*VWo1edo4uxac@Wr3au{LKfG-U!rd zQcS~taAHZSQ!$pJx3l_dOkqy0I!%8jG?KrxzN`n-YEx#2%<6luuUkT|T(v}uh*89* zdGJT3G70@3#KWE?3$Mk`$w6YNE;(-GBAQo>{WZ-&p(RFO?>>b&*($QK>C=~^mfJEv zp+s$?5>ka|Hzt~NtaaBn3PXupZ;c|Yugg42xvg#;&ZdUV#KcA`CN!fhlloFYO09LR z$3YP>YW3^ICRXKaX#W7F(-z-I8m~{6J)ZIN!NdDB{BoN`Q4Q8ih=F@f+8$Dek1p93)^mZi0M zp_I^Zwm8Wkd_aa^+A5z9MdgBn49+hiyu8SJ43*&=O@lGLwMS{Llu%PB5dLvMWaEYxnF**}zY5I;^PZk>@; z*GRWoBG)pi_O2pBWj`7ujO`~d$3gA{8&A)^rKMTn{`;Jvg7_79Q<_LlaCin&4WnI%HmD7)~n}g8n}dYbm}xrzhsTP z_I3n@+j>fvElnWX5`Wr>-ZOuET@FO`H>PX*j2JWQV>~i4GsAa&85ss(%oxd#u&Rk< zn;i~l5^H3|GD_Mww~jP*x}BHKRSR#aoA+zoTqyHH&h;&|FX;}T(6px{nGZUM(%+XA zDL0%PmsAXmCkvKU8ib4MtHxR8IF&fzJnEl}TO=A9=EzD`;+>JrjvZ;lHielo!g+@L zHyIG1pyb@El?^LVAuqf_Ea?%6AjXhi^iGKA`?b{9CF&U8u@^|x{T_)Wz>I5+D*_B1 z0JfJ*e#Zo%b>Yv=L0($_04;;{CqcWWT>5roQ?{&zVlC%gqn46(ifTlu3aw(hQOScZ zPs?(?Y@{LA*BHpGB)05-jS$ySZ)Qv!MMdZ%*&)-tv96lAcCKoBcyzEWo>X=cr?k}l zhpbVyB7YdT*;dy*_qSB0I5l7)g*cT&Geu3G(;kIt%w}cWwIvqztGJa&(+PlyO~IEY zQ?grLQWUt4x6e}%uVp&jbc5XGf8NYO`#n8YC3+*Lh4X<@a zeM|$M;BjW+pt_?M?f(FKlELKR$cG74?>=d%kt#eYN$HYWW_t>ir=}^#>W*D(Xg^O7 zS?Z*{1`DneP|782X1a?N^%D}`YA7vExhk60A+WB>8a-dIWw_Zu^)IJYy*HvfjAPj>;1e>@B}x-6Vx77cpGTO0_zljT4{L zHl}eRHE|}w!U4wA43&(Q#bl=q$qumbn@=3M6hBH{sdcyM3&dZx4O8(NmtR<$2+1x~ zlMt*81uk0Oj{Ay`>k>KAY}MCIlKQ2+5AI=yBmHJ#T0|00Xww}_M2_1*5QEuOuONz* zU>rl~+sT&eGXdJYKl2C}P^Iecc zNKtl={unODjR_xU-a?sC{;?8O5*0_KQE3UGqf7Tt*wi*3N1^n-yw@{t+}UH;UVTH2 z$kY|`dfM(Q5)19VN7(n~pk>`@<`gYTm!inF2%wnhO(9I= zt8HAReceS=6KEyTF6N!uu0Jg3aflWeb5^XR zQ<3(Do%Qk9B&%}Bv9>{lBoZ;e(~~4{>#2Q%8 zH2YExw2{kRi7)Dqk?OUPA&m=N^!+xH3HJJx8+C4olhjagSBHxwbdTGRSbW02CmGsE zq?;v#R7zY!fcXhj6CuWnSmgH+KL-s}^11R|Zm|`$4n14zeb~8m4)FMQw&AzWsSGlDO z;LphUK0XJ}0|sLt!hwYtT7rxy8D!Rg>EJ!wI$KntB^C>Y;RQU)J*B#PNNL_;35Hzc zxMz_J&!)Xfm3y6~%t+?8Ifw~T4Nf5>*oa*DW%!UP+)Ec3f z?Ivv6#f>>t&aG2q%$*Pa0Az&V>>WL2t<40Gf(7l?ak{-P>AaoH@i6C$bY(M>wo-%r zgYEsVU9GCf*U(z3`o&?_$d?v&ov3KkmcHxjM*jf3@`1IGhS1xx(p>eIf1jAKmnKXN zEi0_m2ys_Fo0cvi*+#QRaOA{CKT+!5uhccP@7gt|G~BxO6IQ0|t=?>>X~vpjx+b=p zB1OeYYVSI9j+rB;NaQ4PP(zY|%0T6ya#A^I9JGRIBa}mwk;+JMvfHAx}JwiW}!G!xR4i z)g&jD(IrbzfQ|7lJ6-G}7$P0alB|bM_nrR$8r89?=Jvw>0Q8go5-+u~{+_W5d5Et? zh#^3&YVPthSF7IDO-F+kJO`^JA{ics{{Xhu*8c!R(|(jxWnhRG65%0|lNZys&NadD zFtQeC$z8R}l%V*L_&-+(y%}Q$66h`(t&m!!f{#H^zFK)ofv9cE``|GszismF=aqR3 zDmO}*vC}40qH*s}o;PpwKAo9I4`Pg3H6lchQSGCq#JN$*g4gLQpJ-S>YLJmA!&p1_p2+Lr8xm-~Y*0#b(E<)(Ce3wqd^4x%E`No@uc2pBMZ{{Vo` z24g+d`C}t*V+KId4It1K$%zeQrl1&Pzi7}9=T7vw#_&mGSYv68pvXmGM%C+%o!;kz zocvNuh}}e&T5{db@-Ok7_bAWzKm8;h?4x_f^gC8#%0#55-U~8Fp+(x5F@j+*N{>+7 z`BIYXR%mA?qE?24=A+e%?qNKBco&^$j*nI$e8zaFPA=M&7%wFEhs!P5t`l*rbp%ws zI(#bj^+bl|OsR2}r5tF|EiB${O}62@KCjxAe{+D(?id*0WI14)Ar-PUqACb-6Ngwq zrbTMpi+2vf^2paXsLBiPxo-@clgJ^+A;}@jA`C!!e7W(YR7`dEWh}5jk0zMG5eOShf0r=2^wiBu+YV_y!?X)$H{wq1MCbKR}G_R+BSDI18*aiIY8R&EutZ4-qJA3 zN<&KfL#7F{CfiELjeYrsqb8ogtxXgeYBw$xmD_F*VSPzrFD>nbkA(MUhwivX>=o&0K{%$ymrq?lhHJ*E04jNlQ~(AkUcI6oHV-CoW9d zm#IZ8Pt=;3SR|JM_xh5L=`?!zntRzZ#;5-PAH)7X_zL?kdatTx{z*EG3ZCUd8>^RFDfm(}+DW%|VOx6)jO1h(gsrlY{YQhv-+H)rP)kl2DDPz9zN6@* zD2fZjIwgaHkWmP9=m#C7jzc0A#L8M(=4@v(>f4G(x)w-L^+FWivF zGDFeiGSt0pQ7YhQ2;7S$AAP`#7aaRNMn>v=5UkL9CV15S+xUO+{{Zta?{d?;qkvZo zRn|bbVWxS=Oli^)Ak}qh&YzcQBJJn0mQ!6+y){(h3Y}ccfL+ErA8rJbGU%jd>GA{^ z4l?hw$QUrQ8@9UaRg(%rw!7vea*(cpw4D_tGxhD&)3OqnC=RX)qmj4PC(GyA-@#rq zny~hA-@y4$@xcA2LjwZ?Z(>L!jiX>P7&8Q2!cl>+N^F#!NRQe{{jmQ4)Nt|~#(Jfo z9JkVz*C|AV%6X(kko=yJNo?!Zg-3*`3`ei(roL!oDz}i_dZTg`O>Ypxa>Cynu5rtO-iG`X)W`OV=R&M%{M6#-ij^m-fqi`g&n)W72o_%HDoPIqz$-89tMFst5vbv#mL*wKP$1&v&TZTCf4ojv?v`rS{GgF+jneJ^zxU{MN09hSdqj5eb!2FDM7#IYRjDjv0 z1`LcOfsjHwZ5R?#H790M_{LTtOo1L7QItn6u$Xnsu_k7vnbfJdF_^5OnHnprQc)^o zmn_t~Mqi4==`~G3mi=q99bM}z$a+Ijnv-6+F^wyFNz1T^w@gQb>EYFW1>+qP`;W{>YHLjMjDwFb-USq z2~)U@tVeMSG;SOw#;sg+Syv?GuauO znQcpqBHl#keXg;}UKDTOP>z1OcSzlJ22dI_z z3fJ$GlW5}{YJ7IUz%eVvrT+jC2fQczJRBh)C@erMrd3T_I$pO+6ow?BxHGIAM3q!b zlLpEo%39oS5;lnm{k13ii)OSSma-hh4rz{QgLua?w=^4|-Ui?`kW#_-4lQd62ydMo z=*wVd0S*u&GXzQ@k|E4B8!d8Mf5N{5PSVq7w$ih`arw^!2i#-Zfr0amC5|LR9k4cx z3qY)F-P{>YP7g-S2W@x9gEpwW3ih#kE|L^LG-2IrpTCiqG?KG zY}jneaiq7gBbvz_pDozW%5dw=ycIGJ^yTZTHvKu2apd)FHZ=mC>3XJ+r{qT5@#Z7n9qa&6|Iu(~D_wT;P8F@UeNDf^Rv*V>oR)9GR{#g1N}-?H0&plTGU zUZUy;RsR4aU$#9z))O@hxk&A)6Z=@ed%_R)0fz*Dps@|jsr@3NMTMzXAj(#wxBW48 z@zSBW7EdYX<2{3sfgwM%fA5k;1_l9)0|pPlgD_?@z|RB7<%caLEiFWHTW>;<87Vao zBU*!SU2roU5y->+i+67&NPBFmPx=$?K5`F+0q`Oi;f^@qK)a2gfdcLcIUg9oE3r&I z+LZp^{%R1ziMOsyXs{e6=D#zPMHOgmYQT?F^pu@5DUl|gR6v+fBe|#F$~1T#ZR(V+*GvJLc5d zD&UmIZZx7|u|s}Dt7erQ63=jLS!V=F9JLx4NgK+^x)Qt!_Bqa6yUK|B8<}AwrX#b; zdTeg$ZOG{>3o;6^v~@hmgcm8o4BkiOw=tJSLc`<@0caXvn*Bqoib-lTsY}pwYJLUf zMy;^1pIHFxW}!-p9lyCa5+!pNeJ-1$xVfzBb#BOA!xvuP~LC1k4)6Y0+|z7LO(V8%Fna4y__2oU&fDg;3# zf-b-ukGc=Ua{hvR6rwC<$>|a!l(8L$rPfu%OzCztY&CDGEvBN*7Diwq``Rr~mr4~(1kmfWAZJ3oYm~9z3 zJ#@1fh~gwQDm}KEZgI0Is{wgH^fP66PW}dJ+O-5Pat%TZMN3{03Owm1`AT!`OUdct zJB3Pcjrme}h~8uqAw&^1NHVcAS5+P1(?oP8EXIe_(N}yN4(kZKT^yo2i_dbE2jVhR?Xu_Ss<&Bd=p-y^34?=^Q0$DX|k^RkD>|B%W-F z#!(?Oc930K?A?<3r@tK9j`f?9Ba~QF^l7m=H6osqO+sT&TxzzSi)9Y==%|xN4$%`S zB|!!izOTs-3r4>rl+6tFN zzMZXYKTl-B>d+j7ZG%G68<4h5yQeXl`D5tv2)dOvq$v&#NG3=dC6*Y$rE4PYd|v+m z8uw$G=;phgYong%ISvghj=IiWL|L|Rs9T+3Duw1>w`b}04lL9wUN)=SE?{lLj{V80 zDBgIYVY>@SdSu&p^!$X_R%zzmwU->>KOtEZC$iq=am$$FJfWg?OH56@{e zVHBp=%eRLbQAL9(bD!$$6rxB{jn!Mn%n0l{uyI{<`UM$6u63!AzhXty6xd9OHRys% z$e$LXQ#FZ02BoF~45CfnQz|nA$4K^Nu-hwgvoM=WifZn?b17JvF0F92Lf=Dg+zW;;m)O zTkl9uHQKicn<;GdkFf@9v84_)S<%=&b`}z?u4zVGZnXGh9|PcCIADAkcP{)f!25zO zC)?P+6Fh1Y$*b{a{R?K}A&T^Gn&Uf3w|?~`luC|d4asV3kl_uB1eFb$2wQEFu5$t# z?5EN}t*c4pDu(YORsD}kVG=X*;GA}pm!l!1qEg;Yojh9I8k>7+%~npaWm{)X{F$}2 z+`9f>-wCY3IYt9p-|{lIt31aW9jP$Sg>_U2UMZ-r4Oel zbPFTs)(#@bbc!81$0b9OVaSSD=pEdME}5nZh;+&a6=A53xaa9hGbpHAP|P16pLc+s z7~_GQdkFX_JK}HSd)-R#-}+Y)qI6Vi*=N={C={K~NSvtc$b~)|agyZ3xhdW9UR-Fg z6U=Hok682a+(g;3iHiE(olxuL9P0~NMH0(UGV>L*PxBk6mh3AXP)|p;w1Xt*D<(d+ zOR0c*frRzSQXkWrf2VGIo$Nbml+GG%(Y3&7uGKB(vywR>Ed;g_-+oxwSZLdl$u!UYlmXtth2boM}$Pb-8e_$%|Q< zdDuLQIQEr4BzM2(W4!C_d%>JAXx3Sjb9ccPy zN74t;$I?gAhtfyV2hu|oWQPd;)9wssw~huf4~{rvBt$X5xMzpR&tdW%6VC{`?zMPt z{VRv#8!-DlR_{|_>RspGvmRXN@1@#JJeDi2UVu)j&Di#hvg0g4fbwc=UtCAzOlOx4 zHGxAn+;21NI>#7#YT9B_;2ZJ|LraqBlN3^oq>`2t(X0~~Ebp(I;q zQM#j--MjcH(oZ7pC<*JxGq91p+q+U%C_4Is!8M<{ae4I`Eb9mWBJAZ6PC)y*&&_-$)#uu3>T5yy&aomy#mPfbwTmQP5VL z_sFf%?6hUA^Zkq1*Ms+YM)QsfIJ2#wPfC()zY-9 zth(H`47lzgka;j^3P9zN0wPe`ju_#A@wkV<{lJKef%_DqALpZrj5&_OlD*nI=M66uFlaA{{XDBGjdt| zp}+Cn{{Y(5M4Iafj!B-*nPs6xUk+@Ws`7-xehbEs?@xh`c0ZpT@Gv$5KyjRj5reD_ z=?H%TnPHh5i&bbwtwf0KM0yRj#;LC5X$LFpW81_sF@nf2VL-+PI1nxdL&(}d{AVDz z;2N+nW?M*x;9$#-%7*omfc>Z3xEF3VOXb3bMTgVan%?B3WVG-7cL^uj!@6vb_m$xv z>Cdqx!`a!T!P^Vz&$|2UyA>?<WLT?i#Z_97RxiM%k73i! zh~z0T*Rn#IJ2slEeQSXCI$hE~y3p3YCazRNSX>C+Og&~$Pf!M;90gBS zuM$O<<7NI;cx4%h8sW~9m-f*~>6(cA38!xLe?*k_+uKjdYQBB7yYuG~a4;2wrHNfa znUZoHX6kGNv}GbkQ_@(!R}jOI)Z4L`Q_*>iVi#$SptDULQeoUzq6Li<2!~$=wNdQ# z6D&0?AtOYe_WuCz|b z(w3?kX0An4Z~X`Cz*k_d;pR7q5ePcbp+*^!Z5(Mtw9bPb+AgNjk~H)2j8Vwf@Wfq{Ty0Jt9w*#=}hjhP>aBlZ|q4?GNHeB>Xu9r%~A#?tatMHxX9aoQh= zE5RtSlQ$jib5WGcV7Z)<4ku~gU>jr-f*`N767mU^jzfT=IyxsuK0*gMSdo4S4Wol_1p>fUop>R9@g5u~%QhRltFmP3|9ltYw2 z+bk&^&9AhWER$qL6ky9ExeSc(-U(Z{7(W~gAGEl61U`R*86_41LxM_jPqX`n8zE3q zW>xKif`FJv1q~pg5rT+7qu_MoryV#Zdy@mF1soJ{(}Q3pIO)Mh9X3Z!3OMP9C8SUDC~~{<@Cg|lut*8) zU-|qE;|614B=`_NW&)BPYb7Q&N#?d0k%*FwCd1pqjWsomS*6|6vmwiJ42ZLwtdUW1 z9{LBlN5jtpF_Daop#7g2&$PktF`sA20`c?md_F;VA3Pij2i*!uXK4+jHjvs|wvgID zl365>+F9B|A9uz(kqiWoP})H$A%Tv?;D}%t#{%K;!9GlUkB%Q8y!$>uAKtHH$~2M^ zUeAN~e3(~<`HJKg*BD!4H$+ z@L;(A0RO}QMiBr30|EmC1_uTN1_1;E0RRC20s{mQ5+N}K5EDTZAR;nRVHG1VKtfV+ z1~X8Rp|K=^(eN}wQ)0p47GQGH@g@V)w{w5mZ zph5{wjZLDR+BJGQerdZZS949-4)}_0#{l+{0!k8|?t=j?pH2aXvf?gZVO{{W1kvl={5*|B&g?D)P^ zDB@=%V2m7s4Qec9mt-tWI-u~2k#fc0u^VQKv2$6hmMb9Obya&lI)>kxnkT~Y$LsIM zQMtVR>ihKk4_{!`mz*tB+dK^9)d<6RBH_tmSdOa_&s0x}y1LJn>n|i#^0ETKTfq+k zwBE-7lCGl_k=SZ9{)*Pi+uz$A&FAf{>~r6Budu)1mcBtz(g+34t3{rRo@*om9!Pj1 zjzIxssy-+k*{hCd6>_e;%DNkv?3UQ4;Y=kOJNM`Br!CFM{sEuh`#PTy;+{(QR=rCd zN($&%Vcl;OGhaob2Fp}83ml5ETBB}gXqk~)VlgEiA*)aC?)K{)%^k0Hy4Tui*0;Xd zQpRJ11)cA^F_Pzf*%fQ85d!NyRh&Yx12oZMuCp2L=CIvc#aoj4qOw`0>#D16I!T-5 z`yubm$;kVi{f2*qFkRp)WCG0v{I_2%*SZ763tq?^&}nn7R9jq4KL$1_YomiHMrs39arWBp8^X6$*$eI(qwVzEPV6IJVP$Ty z^Y-C{qZ#)wLtYR!Xe;Ql2;As~hz-ZdT$B*eNr*!HJ_q(&9O0eE>VrCUVQtk5xO3s~ zP8nP)+jZ`=S2`^M2FpczFRG>k5Kn1jj&A#V3wu2}$8hfj&)M64F1CBM_FDT(bs7N7 ztNE;B#TPN$)<<={w~}_DMVxJ3Won0-u?1wb&08y=xvfHs*yg9AnhBl53uaKho$j7( zRU}eAtn1@R#b zWBxPxqqC}C-X(e}Uk}^*uS3Ya4)y4=HNWZoQQ619{-4!1XFZ+Q5RENEPEsG$JEq6} zXY9H~b9HV10KX(QliF=0X}eCH+T@#41N*NFRc@dP_T1r#QM(ikJF^Pn>dQ2BKqHyS z6x6|*+7it@W1=25vW7{hrwo@xC47-j!ys8T!brj9f!;;N?83r=wa^w(y9(Pw`1jk( z_E;U@iyV0(#|33!%&e8$;H(aa`zsZJ+Mv~IZm{uLF3QPvd^h4)yp|=5=&_Y`eyz7% zXVo6Sdns$Fq3^vrYq0&>KRlDG=YPf<^cPqqpCgUmqHfB2v%&)OJ3cpliMuYDs8Q3j zSRor5-BwKq+~}-PSMfyL@4n*3okHQYK9Po(<`5_HkGh~PL{{WTJ zE0}xN{FkIE#;pi#3bXK!@A;}Z=9}*V&qKdwAK}6UeNh2tmG=yaA!v>4fI!)8C~(ym zu=cVlwxP8Jr$fzFO4%f7Y4B9y(JNUERuz8@$IWG;*F-#)L?&BBx3Z{d9t!r|b#;EG zFBRuyh^=#1zN@mX79<=(#8EfZUw(aj#=ESntgXD*YOWR5d#!JGsM{bC%3*Wjr0G>- z);%(k7M{^e=*&IKp;lTwTL4`;r_1w4Xid(`(ozmV9jNihFG@eaFG@+^5ZX@W)Hav< z5ZW!~)HZ}#uSiGCmD6I=66uimqgMxt>6HE`Rnwo0Ek;TaHFce?U;R2hng9nvIP^K>-cWb4EF`L z85o_flf#mzFuKMwbxBD^`%L4aZqlUg1EOo$BXP#-g`#eX>cI@v)~eRrHD72OeiP6) zJN(t{)aI^-THR}n*uuKHx*4OpK1;N_T-A!Qu;#3XxF2T@%(l59LrmX_>!VWz!lWJx z1s6Ln;5-7wL^QI!5zi%hBK#m55$z>a$vFyUeY-ojhmFH(lEA*M7ZQg>`etEY*rFy23fEAaYh5mYraZ zh>>M}weRj13qNPks~&SqqNuiOpv&aC1g9*QK$!eNbO6lckOd&788P^ekX$F8Rze3j z**9m%?p96NjriP~LkoFb2qGFaaGBt^Oz>P5E&~@wVO5$!<53gcSE4-A4It3*|MA{co=8?+Mw9E=vCRcMmS6Lq&45%#|g5JXtrirLLA; zEYOsf z^l0Q$c6bK&SaJh?%{HBPLd>SD%5Q=rmhBan(FL|4Gtp(qP|JE}K>k^wROY~Gz}0MO zc_N4h<$Tdb+}4?H`k=HZSy^a?YO%MZkPij%WV1!5nzFY0TicGXM@7ru^6C;AE>zo4 zGbny>qV=CBE4 zb38(8yGGlHJeF$8%F5p5rv7`I-TV$}nQGpk*>~+qqlzkVXxqD=ynQ!Y#X4wX$<9JX zy||jLuBlC>X<6?Y*dwa%A)3B>?V71)5}HPz1+ylKEO5$lDw6{yk+&u$kQAot zO~WnPE89h)`YUdTT}aT$p3&m1xTEf1=hf%Az2A5n_4X9>%%>J;_C8+L!+u+zBO~vd zDK?xe?&hDgDj)Y^%lL_pY00##8OZRU;o$XWb<{_+3d?Z1$Y`vxLrk`F$i_c7JyxM}W( zseYxpzfHqx(mUe)5Ui$lk^~D}GB@{2Zgh-ic&T=bZ0@Nm<&z+PCEnEx&t)>6XXpy( zGaXRb&$eY#!@+H?t!=?HlF)|hfR1~HYen=~p>)A=s!QuDpBVA7l8`p5qheH3K9+t9 zv_#@ycH?8B7vzU)RiebTD=pXL6k^M5GX-;xiod$));f*M)9^E~VYWpbhn7x9W6a#z zQ(&%#wTRp5xYdlzxc>k)-}XYjR_a^&gf^(YZTyJ;0HijpKgV#JT}vj9O?w_YCjQOS zM$Yq-wv9V^-AT2-(rSHOT)8IERLz;XX3WEz&6fs|o#)+jibi)?fA7tDLCyQ9!s&Hy z=CIYfTcJl#?NuZWhg1cTK~p;Lw4VigEmiR8yImL62U}3Gy0}-qNruW#;WmSfxvKdZ z%9cxPwMI^rS|niA1K7!A7Fut((|6Hx$qceqvWr#rfwrk(KQC1KkXm&P=5zOreU7_< zsOAUef1L_uho`ad%ou z74mI&bX*q?WwU?66s>&AC$RrsRHqDw$Hv=yV zswxfv?O1&^PB%ny2xg6t%&iK@?(-_!qq_S*@al;F0QUa?UwOY(m%025y~C2@0p9(W z9iA(W&Si9n(3&E+j6BvR9%{`f!p=y-c{y;V8aZL9AJ|uvYTwZp9&8_yBK(lck{G-Y zY^Rdw@}6s;+-6avr;j(3W~7g}+oxB*MgS4%6Zn+PpDEWQ&78sBDYTF`;X#ZVHC76F^;Lgz4>ZyzPEe_NKY_wdJ^It?W&1!f3SJh;+ zS&YzMUEf6zxy@~Pxb==c8GG|}KJtB`V_m35mLtguIM+ptE)X1-4X2Xf1&7bAge*Qv z)=#O9KJij*0q_LsiRH*9QB3^YnX{TZ#Wo0|&@A_5*81;q*zUYe3sSzDfw8i#ppLdM z9jMaT5&iy3mte;Xf>~rQBgWSK0Q}K#(W7=B1$zVy)<|7_S1SedM=-Lzmn$t*t*X&l z`jv6X4OK-)MufdeaY#>C`;h88b4Gsu0H*8i>Ga<^^o=jhy(LZK`W4gSv$`b z&0PSr)mdCC*;x#;-H_y)VRs!BeODdD-w0lQ`0dQqAAcM03|L;H*@97tvLUi_uM3JQM((CNp*x+EjvT^b*i zHH>aeYQt?&Xb6jYT~XfBVtyr0HyNI2-4S#ZvRUe^3IoG_Yei%(5X{*Q&S+?wc@$gE zaXBj!ngKoRg}9zoRa2_FBOt0I_#5r4PJv)|g3({t4x!x9+DGsUZX7D+cs(9H6V*Li zCl)%Rms;Dbv+6e?b@f*8+5Ob#mPQ0DsLLp|arY!4>}im-S!y79YEc-`~-H0O@fbR3{v); z{qp{*r)*SBlMWECe9FmXyCqx_ z_PxC#lQ!?ys5cbeuDf*7xn)CM8>ria*FRq)r`>O4u zkJUq`v@IH}R#cUbqkbmo%q^ix$r;kA^e7$2t9A52*Y~USGT)-rIgNfrO-}Y^)Nnf| zC&pWzp$3*5OvzH$;_S$^)9`cbZ`HqFXV7tv7EbT(wuOHa)kWP}`)xE^E~o>+cXi*o zuBPbH<)J|x+|lYZdzkkQ_UY*xCSz3YBnJst8#i}U_Bn>inzXYgXqfiMF5iD0T>aXD zjMYCr=vzXytQ-hE#@J3EoP8lji{c5cSMXQgbT6*v9@oD8cWJtwcGNQv-;@5kq>ZwR zIZsUZU2&q0c;@U(r=xs5re+mAL!mbc3EMEZ_sjd`EiLdL-cE6Ox%WIpjt&cG2GvuEf*ZN- zzUKF=j=z9a?B-OAaq6imUqL1bbWD3?ZYJ*?yt%D6R8lrv+o}(enaPiiskmIKC|w6? zJi@20bY{=YDCK0L-PfKpo6g5U1 zj$ug*b#27eH9O%qRC`M2hbjj7kHhGx*+(BqpI%C;rS(t5dbi#;K320-937b65ggPY zIrb`GBY6XHa2lsP4O3=omAn;`O~r~iZaq$D>-Yw$zJm{}jcoe!_i`y1$G1HdO%tFz zNVHV7*A{vN9LDbd0M%a!$zkWo9|EV01DXZ!8?>nEBcHPMTeFRe;?Yp;CsV(JK8dam z7>D7sH0{oLyRB7~ZmMJw{} zUGcnKPbkH3p7Q39obgK+7Y*E1HA{Al;*qv0Q#CvA;wm{_d6d^XngpS`+rU!Q&e>c8 zzrV=}$ACg+mi$Oa>HU7D7zMo3z^bJtFK>F-cZW@aLUjyWMIy_Xg1 zi6!|bIpdiA{{WiezxyBdxQ`nC-}bn~4F3Rc`$9u|EOzF&s7PJi3jRG|yeyn7RdorB z@gvbu)#byHO8F^WmTf9Zc1(Db>Xxn1b{?4U@o~A{uw3eci%w{Ps7`J%f$CV@7EVrY zHA_}|lC7b$8#{QBstV`PKZ7@QHNoQJ@V1Mb)YS%17~bgiM=+U_i$dbXCU3~8WNr)$ z(FWouWw@w?*9beBo-unO(l2Fr-yaHHITa9!lwF4rQ=F}dM4PyEr)c822CNE|m`7v5 zHP36Y0;gj`@>Nas#bF3E6k4h7#+8hmmLhdn&WjPXkZ~F;e+myFV&`RYRw5QNnv2y_ zG0garAIG~N`KIWHvB|>9u8hWbk!YZqvp0t(iN;DvJd@&Ob8)vPDCd%u_#UB|vo_&q zi$lU|tZbarKzP9QEKY#sqI6tFVq&GNF&qzDLWz^N9BPKS^e^Dc-A!QqB~J8EECgQ=^KyEYUL1zqG09n>aw*FrL=PF$C9gL|s@vBBhML>}*oP+J@tc zRY_68b_eLTmd*}u5t=QXUg~%7C)1M2+5Qmsd??1s8W7eC6aN4{&Dp6rAef(9`)z$Z1T^x##5)*DslpOlnE3WX|d{(=vX)JNZur{rnC+eb>N=Y!e9u;JE zDrh92ea4EGuu3qyR5HyL;_U>|U}pvv!Fd zrerhE+u{hz8+CH&P<-R_T_PWxBUb?Uk=hjgKcQZcr}18rgUvf?LOB)bc)n7O*Kfkb z>5uk)zx2H>YSin5eo6acslExO?Y~^Ac+9f4KNYs5?Y1%OwcRfz9#=!Q z@qNdsD7y+2;57)#dr7Qo^9sOdr5TGM7po@eoHn-Msj^JoStm&)@(GT6l(ULoz0pNk z4Mc)C^CcGWU1ujU@?BL^mUaUQrX8#<7DFTy(cE@aJ=Ahecp8<(kLMqnWA1>-c1~AT zx3I}sG?Xy3+=T<0>{lCyP;c*Qv|ZfRp%KYAV0xYbR?GZBK|AT6g~)K~W6f1kK9Sg& z-BeZB`w}mrExus`bV0qsuz0aWj;3|nqMe!XSgB_dvd&vZh{AZ0g+p6z%d}NBO{TFT z_eW~ClUJpG+4}zg()6kMQ?{(X3qtgq{J>t4sPO#}uc>!G(R9k66v^tOHfbw^Uj9z~ zP}%!P{4V^{c23iNMZc>50LQCcnYZSn`#V+5z%Tr6S8E{rlYYz(j$hFavlQIqDZgiE zo8Ba^K@0+fngdfEg2%rl~5K@ zXBhSr62}DmNfl(mqGIvv`6&-~b@*-t2?LPB%{a7-x1TkxYm$w)Tv5$LGb(9V#*j+2 z; z2Vr`vr4EDIPuVl?tv@U?-i@KvpO=8&-wu!{JYM-?jGze``Z5O41$$C-O zwUhRogG6?emh(-ol=q71(_c3y)i|E_>Yr2DuqamsG>fLl4A`-MHB>G|opjyTMnZ#$ zHb(YUH^QmTC|Z2F{O|1Z+*I6@40H5Tg4~>$o=V%Su=7&5me?TKt(ibP4$=?7nhxYb zo=zNP%|eC6xba-3vxM9{)XaOV=z`%?cq>&~x}f5u4L) zQ+YSb$;E8`)H07HQ0GT08V*GUT8ZCO!h-Qo9@DSl zj=S$}z3-EeZM9wBrs*ON7Adm22Violr4=pWN8VKRE~kD6COxvY;cYFOtdRaoXYDsa z?gc_=zHL^;rsLVo0;}nn7lm2@VKIrx0;r2x%hj4ds({{ZQ2nG75WhIYEAypH0#FC}#2 zXI)I@*j+-3E$~=LLAFJ~t?eeYzbXFz0Gb3A_IkK)m@0q_PiKfKyjjt@{M2IfvVwdQ zblgs5MmugtUuF*?oGh=0bg;HTWXYdQ><(2lt*mA^`^pLAr+uCID5PwyxLZeMiH10a zYKv$|MD7&b)f?L&#MR$EYeYqmHwv;BQOqS~iW-L03yoE4L1oh^B!6lpEJ9eYyS${k znAq)@Ocz$4)Yx_V8Q;Y+!01L{K^*ht6EP_xsEGQ;*--4tt*b8zoPR*8 ze#%!-MrFHQZE&CJ2k5@e{hW5ujn!2T!4S*q^!hEk_F|r%k<~joTtU6yZGWxBza-BB zoogyLVFgxlIBc43r|v2r@XY#kj-JN*zMIOEFcm1X4~m-$ozw#seO3}m`mDDIB9)}P zT>VxSvE{7LZX;MzZ0)htM=R-Hfy#KJu5KZd(Z7P_*j0AUWk|%`R5Q=BB*J^Do~k;g z;$c_5>sv)p5UGT*Lgz;A&Y?$E$%hVHijYf6`?V8@2Ps+s#?hL>M`#4obO(yEju%u& zBWE7<@dI%-OM1Z8w3T87b*n$KrODve{cQ zIICsD%k;XIkUTk)cL=iBk+MCK*lCY&kbgB*40Tiz(^P+E`7M=45P&it?5gT9Jvoj; z+VNjx<6~&58qWR2{-6E+N{SbBjvTrl*=#wfWTbrzX4qV9^g3BoGAc?bgxtcNt!%al z+A4WNKB}?Kdjp6~(eTR%EhdzM>@klM;73^yuKgn+a|Jrmu9G5C9^IbCWlq>y() zhQ$3A0ekO=*{)O0B9yhtH$C~ZCu3%*vzLp3lHqZsCQzTV*krg)rLedwW=CS;Y^7)t z6asj}Ku%ySu__^Ax%D2>a|?kO@l9>cGsztKg~6jWL_w)-meggVcad8>Vrz$sQ#TRX zL+?TIR^Mbjh&EOZS+v;hng-Mh9Z__Y&#_Ms*0Rp;DO)`o_03Z4M!&NzN~V%J#;IxQ zHf*@qsJ62v?8%WphlE9!nyjb5{v%fbl`O>`stSj|a7m)?W~3QrqQn`fviOKVWP?>P z;L0Q%lQx7iMjvfeU29!FA@HwBHO;;trmekC6`a3S>3(+2Q`gB)DU=+_e%TT>_a#y7 zStz7)+Us9iDxIL|A#Dr=MzG%+f0%U0}b!3g};}Yd-1!7<$PKN43aeAXEjv$cwk5uB`~{mTQuI8y>BmR zuIz|U88uVm%(SV4u?%Hn4P~sF%bb*vVan<@_N;)-9ItfwNYOYQN`l}8nar&>L8FwI zc+NJz)dxuyO|t0^c2aXx@;2BURDx!^iSRBV$A!7wOz9hD+f}piGDk+^rN8@ApIGjD zo-+x+xF%a(piXuGfOe#&xVI;pqBq${(p!?FHmGSUYcj@Lc&(9}o1Y}ykVjCok*NE7 z;>fEOYr0t3J8bnVGE%zylO~R(yeXAU3*gPUg-us2JF+((3M$Ae8-ePpWFGxGg0^}` zV3AYpD4&zoL}G4<%n}SGOAM6H#>B~>tL)2@llvmUNYP+ynKK!v%`{x!a|$vj>*^wI z5OQ2c9LY_dMglLYyPcXC))$^j44Nzt=CI$GMTMudKu(41W;cw!g$9Ri22qaHp;_^2Q29^Ev}RDI*F zf_bvN^h_DtBh;6XQsnFLEQtm?2VEW6Zg+RVQ$~ctA`6o8Vw4kn@pCgz;T1w$* z25L=^=JugCLG=`;Jd`^~X(H^(-w>tQrlR!_ovSe=gP=)8=Tp4~cY*Cid5tgNL>?gZ@; zWJC*w{)+e9=LQT;K!@fL&m?Rf3fgSNBbTX|9xqFB_kscd? zRTMGIGRa94^p3bfc&aj+CwQqLd`A40fzd%(H8;^mUr$!tG7A+`vl`)elv7Da{3k@$ zhqn+%H1cvnX%ED>ufKUY&OFgB85NDr=7M65I~9G|4yCv1uWb~j%q#5H#`K+5DMw+e zZ*5M7&IB6%$=IH5_`_3J2onM*(tj& z%qc`wti;__1ekqMR#K6o6M^sqC3Ey(LDPeiLZ6E}u0@vt+{zGtxG39z*$q0Ld$Q|D z3&9I!K+TP^@jF{wsit*IhEu6Uq^GvbAST!+fad15SKYho)F@@t*>odlRB&8utEr6@ zQ|Zam*;zfnGc+FdX4Kx-XrpnWmB0aH-s%eI=OO?E$nlJkyQIPjwT5Z8y|+WDQnma` zx5GVDQ*k82`k*sfm3+G)4FWl)7aE;;DvNZ0e3vxwabyNbrwTnu8N`XrRV|ug(9T+@ zF~ZCb%<@IJNotcAb=suOwovPY9nN!3N~B&miSSfFUH<@8v_HgMSvXy#Pn=~954v4s zcLJn>IlIAB(7F?es8n2V>IMG*bow$zI1R$$U$Y`+D31~}Vl^JN6@=gboYUiF?Dr_+ zr^jR=jk*PbHu4JX8g7_sLvFizh9QdDZnCn;c`GeeMFnt)7P{Ebn%d^F-H^W4*KlHO zD>qkb%vm60ckubSdv$rt*}S3;uZ)=k%R8MuW~;wNiKu(BCR?DS9)`6B%*;0Ng+LrZ$y_ILm&I z{{Z}_W^z-TySa6Mxq}v|jB8ER?wr>&;Zt_0;y+bR(Z-RgzrMr%(7vmZc5J$g;$|;q zeVw;mWzAGjHc@tJy=@z=gW#?fc1}`6`gd5UrF=cO^g!ZhDxxsqajJ$RixT9V(EV2r z%>itcPjHZ)PBjzqa)gJSRE`6(oLd;U%`~#ukaJvnVL8I~LNPg+CQ9Cu!zVjv zst0X{$#YK82X;~I99PwHS_Jlj=Hlj=6Ipw6K=xCu7;N94^H7$^=eo)ha7Tijjwd(S zHb5h~jzH&kZFDtG?`FMR)WSDB0uMX?07PkN$N7!_0Q&y`;;Nk+xNVKsu=D=_mC15P zHmE-A4=;DR)eMj}QZcfNI4X)KL)v1BelXz8{ECD4c?)FGUgGGB>as(rE6B#3GlYOF zLlec+aa>yxb1-+GW$MO!Mf^F{%hY(4>U`DW`3ixJ?_si;#m3k!gsu4$`AJ}#hh=P0 z^hL$;Vk+eh%ho2jx5+fvqnKMWyzTt@`#<>Ik0d9M{j)>a2Wq75w%RDWuD*!B4uuKY zGrBf(hcw7oEp;qxVeG7e4$sL<=VHlFj`8Ph)BWJ_w(5K1$xEmu1~j}m+?26IS?b!j zG59HAZb7*$FV&eR@ns?>oRfAyq@A-h*Vpz{1fCggi0Yl5NLU13ad{!41xU-<>8-KK z#WE zLE-*?zrJd!>4Q!tsKj+m7>{WN;T8){wBXsrN^nXFFOBB-GfbA*HZ)6#`=yD;Ml5mx zph8J}ml{u)EN^kPH1`-;nNU5?lQNyHlNpY=jPncaRJ|W&h6E01IYVrHrHUT~9W!Na zBD5uiY}4C!vimDFq;yKQ9*8{BX`MNs&L8(peesU;Xduug^L7@ccba3A2B zWo}5iQjZlDNOeLvLD5IWiP)Q}=`MG{pNh`q&ewAs8g5jR0!Iz29vtneYqGdi6&;aI z8QKc>P)+bT5FGrJkCC-)k?uGKx~I<+xOivxRz*H%43;+?RA%iP@4Egf>i1Zc)+Kdv zue_ja+osiEb?l&<6CIa7a=!4dyFR6pn}}Ga(Mfx?GW;=h%dIImT%KI{gE3esVGVV- ziwuA2BG|dTe+)Gx15e8993Lv;LNsEIHo4${s~vXPQvKjg@l<*3IKPtnX?}#!^iamk z7@RJdPQp);mKL-FYbOYBl@K^BnsaRT4OY$j$8OKqY$>=kgEjV+9vyjq%mM40?g2?4 zx?pv#uH-JAa4wkZtimtkxP=F(-`cVU%NFp?-ZQ!}4j$4mmq~~mk%5nKdGN9jMl!M# z?7i%O;pWtH7AgJ0N^F%Z?U8n-$k`*r6PR4$(gQ87+Au_v8-zt@L1OnEMZ~` z5JmP_)-+s6PI0x7jpf%Wn@5j7cRTOAlCdCDv`jr&ZD%Pg=`0$IG6UQQM<@DWaHRP{ z;mhJbL|Y(lcqT~u>YZRzSw;$x_^~Q$i9&yP*+T9Wd`|i=want|!13g%+a3~JtkWfi zVN4nZK{&U&$we3~z0=y($Xd2@uQB#ij^WfX-fQhk1DOQl=!dY$O5`UvS_0#OGu3gL z1+1R&heb~A)ygx*Me!#-&Oob;R1N|8tX-tjybZ%rhfo|Y&3UH_uzaZ2J9#48%&5V)mr3gQ*SLPf+_)-%lqtVI`GfylI!FUGJ~RKFNqBbHp6Yr)jCHt*qYm-_dDT2ql8CcM@q*$%JG>%@=SgMo`^{s z4km?A^7kuQYO_`=0>H2UY_NppSlM7`MWH#kO_oaP;W<*u8F-(gkZf#laW}F(qFjB{ z_5O+G#RsQ2pbBWRw^C-<;woGm-O^T+wuGP_`0D8-*e(>(mL-&Cm@l8#)#isl3H{Rz4 zr&P3jD~RIjl^1J8m|3Dcmt}TWWn3$TiMC9$vIu|`vsePci2-(3Wp+^^Irh;<+HNF0 z+Slq?Yr5)VXDmo(s&0FSQcE11q--uXbF)^b@hbF|&gqgsIVsy77QQu4qmbmOo9i6q zTyhDgvQ!^<)g|2_0ZS8|g!l#aX0M}E2#$q)tA)`ywG9YVqfm`f>27hbRd0~@!k}p5 z;z!t(_F35Cd`?og1deZqSdGIaFL^ph?8_YyotYcmaai8zRW1(ruAp#f%^IO=DI6U8 zgkBaasE@1(=eAP{8Y9<`mbT#;oH?#L{864k%||%Z#R*2K(+5~CAk3#rZ5I@-BbOAT zAxsW>U?BCLFz6Q)cp%{HmK+`B!_0+9;^_U(=Y9Ix@2Nes6HwpvS6|IZrO8uf?aW|# z%%?+vhUlMC)d3cA1I9TTeY@d@={`k zLdR+rD}>^jKzO>SG1|>_%GvOnnH;tDUu9ovhXu8n!`c%MKC#@(T|9P(SS}D^1I-5W zL<6eA6i}I6W~S`;qaCASr=~D=wFpgPdnlMBZZ|A1_(Efkb;zsrrLbme6lapWUn;yG zD!fBLgQC8Q?YU@Ab8zH_Xl`pmh0|IBIi9&vMje?>X<-S?;#l)Wo=0fqNwdpo%Y(hd zdr+!-$%DlN)|3HOD7VdU(2Dw$>R|49E{7Tvu=plZM$Suu#~axg*hQ>^SOu(u8qVn5 zuDia7CuSCd4cFN%pxe5Y#eJ>Wy%wjA!IP5>#g)9#7~BGqnejWtF`^a*m2YMCaN4zf zq+n*!eZ0SJv0RgFbo*l=7N=MR6+52XK?-6;*9FUf?rMdcHw&tW$&lYvf5TbGs(Wy( zQI)Lboh93FgS1!Att)RGmk<5naB}YiA+|-~2v5ux0Q|uSTseipc9#o+?-jx$R3jra z1%e%0Sy>jHRhEgz3$UFEXu__jkEkIXdYXHup8KvzrB!|EYH8|Zt`)+#Si;5^5vz*T z#S4tM%N51Zk#ScP9SU^TPpNUyInE_fR7put*_+8$!4})u*B%N;B&K<5#*2e9vH?d6 z-`fJ|l%bBsLaUtgsNx!XJ%Vjq)%K)XzWzh6d!ed#e52xxil%dLSh)uegp`xQVttSoP=b6M>QJ^mrm@+818Cz@SAa(Y(oHfP!28eA!}GF zhS`S(s|!FE)l-Ao;v;iXJc6sSj?Kwv2Z&V7z|u#G`!BUHO6o_iwYxRP_?Bpa%F2z|Dz?1)J%%q_)%L6WaQY6hL|F|}`{i@ZTQnPO6n;cE%qC&m5>)f0k{Owk{wG0mg@zU z_dU3UXz>9J*5^P*gM?9g}oZ$Qslt*{gjQB+JCL)x1>kh>MKT_RyG8WEz|b+GNaEDqIeI(O{Zj4`)U z`6wb0UptjrM+*s8^{oD0&+Owd*MBAUoZo1XZ4);zm9MWM{~+ z#jXX4KapaH{3sTDs_gktEbv0eTHEf9@2^qM3>R`~xlKiE^WIe-dhdM-sCv~GbZ%^K zvBi>+l0a?>l1U$D1nR1nH-zAWY8rb9yj)nhspOVhIqB`o2r4FrUB{9PI)lkh4p%q2 zvElX=nk09EE{JJmOCw|!3TVrw!*nUEM{iX-8xCRDDTeL?nrw4OFJKo%oSWhbwh37o zaLj*xYiI2!Bcf?0~sa$;5Y(VNFD>pR1Knw8!5 z5k%w6Dys-h(`I+%x>K9UHm&Q4=A9vMvSulCa+Z<$MRak-aKBUlEx1f;`7Qt~7HPWZ zvK~sArV)n%;Ri|DLNO|3*3k{xIt|$C54A)nx>{QGZpe(6AQyNTOVj0 zp6(ePg|w)BB@0~=sHT#RUKFCm8hE8Qa&s{I4oXQ&a&D_*F`GYUV}4m(Jn>|6?y^?% zR`XhK(^LwO_R6;5bkC|Asu*78v?%Gz8)+V;r*tR8AsKseLYCd+z}DoZ9MiQZD37da z9r;l6-H|~bHL3_8f)?Fv`XKd+{Y?|6jh<>*T`sF6(3pzZ{{VP?im&f|^eUXE%`(=r zRPmW&EF9ESwbbmj#gwm|{ize3Z4+tCou?ZkOBn3oKUDfx#!ej94mN%1{FMwrl@16J zwuBCnu=px?+9u)N;bX$vHE{~WQ8rT&*5hjFlAdYuz5U{;TqAYL>0}40Wb)sjjJ z8KQz$z|1WwneUY1DCo9E*mWDYa`y__J2EYy9~uIyX{MolpP~=MhmTb5e1V~C4V?P= zp(DGYeW$Jz^x~2|UHd4q&vD&v6_UwX>dIP4u@a0=VC@B}3N#heC$`d5m&9(Q`;OA5 zBu1PZ-bg9=lrr5J^^ITV$PmkTS3^b3Xn{>Z1&70MD3OP7@JzjRP+QIW2a0R4;#Qzg z+={zrnEzo^~t~X~tDf>koQ#yFGv?J6!B-P2BE`BoOS|`5XyUq&_DsS!M6p zulYs^9o45uQlZWZ`_P+l#OIimgscoy@Y$t_i3!rLnM<7t z7VA=2tFwRd-a@F%{V>it!Cl;tKYPE7Ewx#%kUCQ?3SGIYTKlRw16K;K&&fEjFp8o~ zl=$V~(qFaBvUObEy0p6!Z1-ehR0)SuI2R1anUIMmT8ez@DWGCkU=zXk6DXK6_1lUw zh@DcPjN)}_C(!~NLl2UCG64Mji{#IGFxxKV=~CS(+R;apdww|g8!=sQF#T>#ov#L% z=d@%>dHUtGQAa%a<&DjG1tg$;yYgL{RrZ^F=Tq6Ar)*JMOjy1YH;fF08h;%sA0T+27Rk?Fr5vp5CF= z(3bJT0S=4I*5Fgsey2fXb&Aikny35hh7{f;w)Mr$3%eekjxE;65&G2&8>hHfa%r!sDcxlpHyHLpj7Ho6GV0 z^*hRgWp_#7D?Ld<+HY@r3kB)g*8PoI$98*RJg*FrF?Mv@czK|Be`bsxOQXL(E6)0S z$1rx4$TwRke!E5BVf}d;>EOH!VDF)hT&YYq)j=2QS$|CBE)&#+juvYTHn z0HZgoZ01%}cRrytNB*+oom|w8m&glM0|;AK)^8W<7rGc$gk(dV&mU@|g+7R#Kh!ko zVPaNGlp*Kgq7hV5<#IgrLGzq3uqkx|Ql>WsG07k+J6f8tEyAQfzwZY=<95G#h8Djd z+{?a)@;Q_R5@~dK5;9z4Y=pzn8^Oe6VxDz#3V(pSL_o*{KT%%hY;p{^QY$VYcL6Vz z!X9kmHbg;}L1$vmK@9O^#QS(khr4E&6aStl^Vq{W=T1-V+r~dDp1*yEYdn0Hu~illpRwpW!rf?c|g4& z;y-#<=XIO(TX9!K^I=?@YuX@B&zmoqYuXguODY%ZDe8oVjlca5gKDKnX1DW-I1Q$f zSDv`W%9aI_E^Ad*yB8Ey)^HqEX^?2WsorjyQE(=qYVbz}vV_Y*6fV_43kk_oS6gsE zq1tAH>hoBOkF| zQ-7!`C;ud01!O}QJx+y`uPmEut5T(nnU!_}NXB#}#F!4Z6`BauL)KPEw*d2E*j)gJIr& zQVRIIs1*3~4ow9r5udgG$ELD%))0S^57K;?DwEx{CixFTWdB##RQa2ksdDY#Fpf_M zka^E1^_&~QRNno6WH_uL!V?p8j@As;2JyZH!g@Qi-&FO(WFekohSw^mN()#gxF14N z)?s{S?~IJvQhMiDho&GQ8VV=5+FC!j?ZtTJb8bZFD-FjO3(~SAV)yAs-HGxfdOr(S zZ#j5ClW3rtW?UM?(L4)nN!~sbWv+DNM$&c7uUre9>m29!m$lk!9cyK{!Hfj`~>G&DxK1VudJy|nFUz=_UpD)Gc8}IL!#V#>XMENsX=QKHQ4VPMu&KWF#j{S z)?u>Fa{R^cDe-;iJ#%=ojA~W;p;(4oAs-drzTw_N&EVz0#>yPEDF&YFYnF|=b&ONW~HDMI! zj|L`%A1L)b8?8c*qV7%>1)G0bBWyzo_RMHfvaN&)eoKzpx&3Yu6qOc~HD#Qjub%*= zW5ATH*#_y6GYknhFKqMuBKWLw87Ic2SOvX|#r43h z;_P3x?l@tn`v&8Uf{zWMe!O;v@?~XYtmZBr#$fBiq=8g)+6S_YQklMnl-6ABu;Dj< z;n1t4oJ!&V4cE$L9*`x=F-utWq_aQrLJO7Ac?W5SB_syLQiYo~>q)u(5s%NTC_UDRSQ69|}Z8W?88 zf_$sUDoi|!N^_Gce|cA@ob_HG9Ji?RHb=fQ=h#U!S{*r}Q$6&3iJH&}Lz`trl8k(l z6;em*;+dCNpL*@xk^U63hz1&6_CBiu!J*)W57i2-4R5lNOz@T`0}~3{%&i&ug6c&~ zEh-hUV?O*%%~@4LmczTdavp2=<9B4;=B3PPY9YL`BGk-k%@PBZQ(+T&MV%H0sqY5b{0<_>?#*4fmHz>0gNKMo@?hH@WHesc7w=CRfJ#d4#+xYl;vy{eve zS7)tfInx0s)G_qIZf$OLO8)R%95S+v^(=cwN&CExlBr1$=epBve=hs8HIXUj?96oB zEX&Lke6J9W-f17(q&rIrMn9(#PJ2^2xC!G>+U!IQTjO{K zaN)w)ZcQ~4!?@MyZVc?h$dQ!~cX>$9t1FX-x85)QG;Y$M!hvy*m0zM(Pl>^JmX}vm zJ_lc%r5kp)mB^9(Uh^T>)Z)SAqsp`9C4m8V=Oexm$fT!Zs|DyI z2GGpJVm^zm<9&+Ur|RBy$T13-w6o#U#4U+xBF3w;sG})PK85*Xx9j?s^Q4o~vXF0nmZqEVdZ_8*3NV_A1EZSb?ZT3LkoQ*~is<)v~S z4Qv?IM4*9)3G5(|vbH*dLgBXPV?2K7etCKIC#TZG;eY)%DrtCO9gmZ{>STWjpXTjB z6S6B_xAc#5y%k0QZsKi%>-!Vz9qJ2fT(Y`(G2m>Iq(vpO0894e9iBG-7KnH2w-@gU zk0e-xZEp3sKGzy4e$~jzMACN7y{KK|Nic$x#tXgtJ9>W;ZcI2fNn;PDB)0G>2z^lL zu$x?yBw|B0pZc_qn+19`5q=e3E&I;GdxA7a>I}~}SelSrC}Dm*UERdcn|fZ?x)qs_ zupBA2Bbtf7)ke<|FfpBf-z$@v_OCPdDx7=eKa2sD_!pmSokv1)ufjBIY@Ibxrpsp` zjV*s3weA(+Q-^3L;mog*Dt$i!;WF|1>c_?)0z>^L^Uz%Vs@sP~{~BiU_^MjPz5g(X za~1ck^3irmDtX@yM_dkswpk{{sKXV19;dfJ$mVWrowNecAZ726ZP|;Tm*}QNv&b!5 z2kni;X|jOt+n<3vEJ7J z`sET|mp&gzskgd{J7I^14qm7@mn2J*`!9GiQxv7HnD&Z!DK^_&Q=ocx73W)AQLTI| z5Rmt+P0I5xmx|E5%&V^OS;i8Ppy<(0y1QP6f?nS^r^1yiv%&;&G=X!Y1GRjTHP#5< zeW-aRbh@NEE7Vcg`bu>=lKnc#<>@2h^sfAfps)6b4n6Qdu5atJ^oLWbNA}Dy+-73x z=c??%JWsRs0SMU@Aq9hCQ&Ct0sH7IXi~)aSBm8YqT_=||XD_ewX?rK*JQ7V6;o)E9 z$s3%3CkFj0&6U|-ZT{*7Xry2Cl)o`PpL)#rP#5+e#@=aj9m}S^r#VBMXV654v)&r3 zLowU)&7uc~ks3{bYqcwht;H7lmLA=9O&g+bVh0gq>z-6FOZFS4=j5RsVPbM5N}t|NBNT+Aa&Zx1xN@G^gOdZgNvOc8mT4B|mc4{T){5 z7mvCTC?aEO zmCU!1hU^Y7clkX*DD=Wj<0Jcoq~X$IxA%F@aKPWwJt#7X*ZTXm{mOd1>Pqt^L8uBg zf>KvVqsmAnLVvSdm11A>T61}$Vq;u$c`mG-XC6#~{5TyNHfU`-0P`?XocvXIc$WZz zIzz|K(=H8g@y^s!-irez z71Qp?p{AmfcU?HQC0!IGIxq9Pqhc?PX32X|EI_lHZ_HDICwfB6$Ae0=g@Ts#Ad=C0 z0bi_%D(39*fW77p;|_`qvuyj|{!* z`s(93xDz=blT28tp(qGOt&bAy-AM?*oryXgA$e^QQ{E3Fak>3@9*wi|YFWPfw0w4lt{<1&2AZfie*T52d>l*iVk`JI zt$uCps!Li`k5{WX#UCxPQ=!eHrB&$0NBQ`&FWai#;~yousY>9lnXE>yzC0!$;j?52 zMMmxuZ|v@4UH^Sb-l(#9G(`1Yl_NqAuaTiUnvmzxrj4?vJsFk#(&2Ef$jOQ;4=A0Le z$*K3?*zTIfwbEJ`@#w-j#SyB`V6n z68(vP_0Hfusw)yWY1P!^)^u^^&R6)BU(&!(8xV}((pOu+OBQo!HB~xAIGmt+$oozB zjvIE>(*v zIm=QNtG@8c&P0*OqRY{5k^ip2%uF0l3}nKrz|hy9A)G#4-CN@Qvl_9^>bq1n`eLqxUt{LXj-k*^P4wHNv`q>LXt@;YSTHE`@pkQ-b%cmt*m%1BX z-|=wX*MTfd4x3tD|LITGqra=T%)&TRY6Jf!^1Uj*o0lOyDBb$IeU;;Vgyv0T2La!+ z5b0-}^x{tY7a=%Oyah^+8h;P@B?9+=xt^7dcMu{j`SnY~e*I{?-=i#dyxk8CngiJ; zuD8!lVfep;o>-2Cc!~+T0me{Yhno$|Nr7G}@z*~qM|H6sY5WTCRlpCwD@$9aXC||X zFS9~tG?4g%_bM;K7`&*T_1yV{OI;lHPZ~UYC@TVpOlnQ*jjlzKU2QJXFvjVdupmQ9 za%6ju^FZ&|Enzcgz%`i2O=QNR{b9k?5?eGRN%G#r#-T(hu7F>G4E*Llj5kL6X`?T& zy`~E^eiwFZQ|0c8PF6nwf!q);U8H>pp=8qIS1e-fD|Zt7*)#f*8r1N4L0+tQign zZL2%e^X>DR_eu4ijOP^a&AtXjupA9LJ{yzIwwV|z$Y|POsI0%`(6@Ia3Q=VV?vAk2 zjhigdyjQUS*cg0`YZAq~DUVW(nBYe7mmmM#6rEILEvqHF?>MgAR~_>NwCPs{;EHHL zmhB+b#T4BEef%4p+(9>f)@zys}lii7y2$Hau>L#Z6389Co zy6@pFDbAT5?IZs$paB1M@9+*_^P_8X14q461eY-NLf)q82bKdiRI`^5B1-nKzsNpMQ#O^~igXnr~#Z zgwy;IyaUNpi!@9+{-X0^SJ2O zu>Gz19rvGMW7x*fRVe1JID=b~vpwBJ|7o?$y5ru@PUGI{rBkAaqka%N%xilA!m}YO zd*BHK^4Ap><6jcyAgI}MEYU8_&srD`Tng$}3D>=FFd$ZzY8Xq!hH+_LI4;%Op$t zRsE2P9A;&lN3;MspsE`1j0IM}rSxP^BLGR~nAlJ|nfeR zRJN^dG}Sdc^I;r+Ui{=fXhnf&7wmuovN!8<8Hi07dOj+ z0Kqm>y$p$A1Q-V8Ev_qh`JIGzS>XI5sX~&LU7GNi3)b;n9o7izU^u|b9`}=YNo~;} z!apC?HGOHdD;P>EUs7B06Zq2^ve$y}U%@IVZquVw@eNXmcYPtfO11@v5Q^SqQs$Q= z%By?3ZuN2SB7LTt3o7&Zn35we`sx-~R_pW@NcAK3FO;3lt~`K?wyO1*(%NBB2H%+h zPiB1H4_3U^6d^Z_Kyd;!13|a${ z_koF$ZdH)olotj6H0~cNbcwl~l=0uFzgj8J<6W@w-iNrp4c|RXB%Q5PKz=XdH?}^{ zSyU*#ef!Vo}+;{vUNtWVHZkJs)Gk-&rcqsm&MXmwWLTi{^lBZ0k?A9Wgf+sTqr@U$PB{pmGIYef@*IkpLCfFUZxhZxBjJTHZ;k?qRU_n|A?%Xcp6cs(YV_P*|h} z8&GK9=AZNu?$7Ew!EjBuKd(%eMKy{kU3NFb|EQ-Q89JgGoC&F79Pd5O@2f$aMr5UW zMDh_`&;5FJLfJj_oUC3}Lotq}r)_gyQf=Jqd(V5H6cu@_?{R1?0mcoRsV0BxPNo-- zL=iUEF7yndt8Ah~Pq*6`2zoCiTjm13{4LH?WyvYq>u_M_ukyx&a*WO5j{AoYPUD@y zoA{l`xe|zp+J6{4y6!KXNPq=%@}aUip_S_gIe+rl?SR-XmeQ=~HW?x1c_Kd0T8906 z=vC_Ryf$x)neo%*=ofB4dQzRtAx7*XQ;Oc-<*+o;0duFgJ;7o`FmJR1sC<&S)eYS@;BW zB)LDUBHlMKIqr^<4%D&V&^s(BH29t}l=s^BxhqJobB#B6)nJu*vE60u`@faXry=|W zG@i0(EXLY0PF=EBGxac`xezgR7&P=^Jkg`@a6Fv+3Zz$TvT$LxOpYAX z8$|-VNJG#3{4JPXOCHj;b=M{cWM|x1FpnE)Q(>LMCbA@S?IejDBXE|T;l|O^^z7nN zGOyK8frqP9H~%`ho0yXVqWI5LPh>}l#eVMu@P97anuVlYqE8l%Y6@G`Eb16+E$NV0 z3sy2b3`!DcVPfQFT?mpQDNmjhNlbl?jkIg6hS{8 z;tip4Di8;U4+iiQ!?}N=uf8jT;3*%U0OI-&!j&D|5fgp31g%ZmBa7J8YR=V2tn!>O zF9e5fepj;*@I0Ni zj=?r@^2#!<`)eJ`^xexKo3Xz?*uLFWCXfKWcGaMnHcK`?1xuyB1?N@cP!0++hEVHP z2zuU?@)d}>qTSWLxrFXIE!xJv-l7!6;ilW|ya2Mc5?6jbczFqWqe|+YGvqxn_po1} zT8KRSHq;rP%0xo89A!bO*Iv{Op{k4B2Z$n%gwuILg#?QVXEP?kv|Mmm*TwN!+ zs~sG}aH}F9TGoW4lZl0S<;LW=Sv;RPI)REo4+RpcUNpZK)b!AY@7tO7Ew2jktra08 z7LiDl2c^RVBiQ8Ees4XJ==0hkCPLeAoK&#M+p-dPzp7d$>3yiu$wvM5H$w=jqR+nD z!T8Zwu6&E=+b>i%e*yD0VA-#GV=nGSDt?LY3{%nGL28OWB~(>6;jmy&fpzaVam$gO zZmEtAPhgh&?+Oy37EfZ2!70~UAAB$**9LW!eqMzm-l3Xk7G%l=4F0+@X!m$fR^S< z+STlZQ|wICDsH_Y=4JG$stUvV0kD|`$xlca&wV$WijyOYUh1;Mu3Qr;LFK8wF%W9W znRXvd0zEhGmT)UzuSunF&Zy)VIil3YI%WbZ@$J;-MR&zAhh|my?tCb?Z=W;`RC(^{ zIiGL6bX-&G0E(BUQ)U^;-Inbs&!%;SUVZZpmt7{Gk2~z*s;afe2LFd437jQLET+65 zwHd3*OW;OABGTDoUhRNh0i;KoR@PXcDcATz@F;CqT`pX@xdjrh%FR(lt`qt;wAv2T z56zPgotS}*6lY3E7$|2>-18`>u=50L5ueuJ^yb|`0vri=-`fA<>`J30&J%JBvqUO= z5QGB1D`7>gbAG8Ju(8m#nwwtM_b@n=?sAjVE3I_xN?p`-FY=GPILul|?vkDf)1=m# z<)yn6^$95HA7gL^=|}zjaOUS#S|Ci2JRDHK0iJ1+U|$K{As_nv#AZE%2)Y{!V>^gz zaFov8Z%yDEVxB+n#eRU@d-`Q5Q7;IQk2sM zc-6y>QOF;P(q(jJ>$meHW`ev4F8E=20e}7s1K*+eyTc!+LPlnNm0@6_7) zdr2jvEa(S>#GkOL?*O`QBifNcu4x;gZ?+uTh}6}3;qMfu3D{-gBEB*<995ic zum(~byHQ0TQXiMqKM4|L`a<%eT5ZIL%L7Xd7oT~)l2=I=7e%w}}LLJ-Q$1gCS{41ikxZ`ZH zR3d99RF9r(NUJb|nVN~xSi_q0lPuQ(^AM^cqpf}v9fX(iBVb2q^NI{8>@9_=u3K(3 z-v5Pxuml7F!ysP-ZTc$k?bn%USI|CDSAFnm)fMAnDuhrq{E^D(2Mg@812W(-msKu0 zV{=5>v)T1|G&wJ_P>pem*wod_FpF_^Bw#1-dX^R?|>m6Gr$5;3M zkbD)^!>fMwI8WN@Kn4hlqbxfwvMBns$OzJd9}{*%vajv00oF^eFXZU7z(a!4R#h(? z-h&UNm&8YSikaE{eY$v+ghkMb8C0JMUtw}H8{i8JZd6ndgzGN;9d07|lJJ5^h*ck{ z*$KGQYrM|83!}iOx4dUnZQq<=rtbzyw#IZrlesh|LXVLC)->5dB#^K zlIq0U6_En>yi1q6;$TbhQAtPpP6B_--t2;F#$~_t7%Nbx>TD_6;381u4d*lU50cgt z>c+R!4}=vvqOT}#Q%Fzu$20~BmJ);nrzsy4^E?_OVQA9y1ie9$_QUU4P6F|x7(+n#Q1LJo%|>NNF%ngM@yMRl@FdC@oA7e!o>^nH}%VCzjN!73!ll}&!G>_dHa zGV@$1D14vnV!zPnI}-;D-*co{3B9auz~7d{6RLgG@5KENGA@bCCNa_2oEhR43Dl5& z;w!<%p{FENpx+-@bQfb?(ylZ;5jVtJD8^u|oHy|i*()a_-~d-Aaqe9^EZJO|nHTLk z$MAf3DF`X!mk#;bkWcF4V%p6ZJOm;QDDBX5eT^6EN~}Ji@5LTxlj?d0vA_0~EiB*g zOxs_cAzTPRJ+AU){fDt1e+B$pl#Vc1%|pW^F>+fv3;6rSmnR_WAX>bATG4rPHwj|} zNhaZmNgf}GVU{f~2s3B+h`q>|pa;Oio@6B=(JjV-w0T)N`lk=CR^cl-X^_D_fe~l< zw}G0U8>`nhp#r&&)bhdJ0cyMICWzqbAFwv@>EbSNZDJ>yi2eO@3R+r~JF2kn@p3ZB zLPfR9v^2}KtB#p%hYjJhK^N8shpA7%ioV+S5DC0n{%9nsW}pz4V6-|1J{|Fj&Wz#D zJgb+ux-nzSNB|nq6Y?_^9sbG^1_`gA5*#4TJvoH9rVvXTsV$*IVUXIBebC!cxWob7 zBSo6Tg?{x+MJf%^j!_RxHiU=6L3d*6C`fx%ov+|3K{5__N^=R=qmfGo%#sD0rRJ_h z`(otSC~?6>l-Wib5V|*wFfsBUrZJ6mCofZSL9-BoK# z1x0Z#5-#obLehpmEA&W!=b-)#1sXI&%IjPF$_u?07lvaC-JEM*8lUEruqL$z-rL8m z)U2?j=Mw8*zI92ltLWAbq6Deln#fkK{b4=+g8HauPdH8jMBFGkXh3Yad25C_Su7*J zk3Nl4>l$W^PW;TuU1(3*?%PXWY@UzUe`3#^7j%#Dfp&53SXbFQwz(#QOnyccz7h?vIU~AG>^1EfMpb4Nlooa59t*IbicG<&&(hxy1<=zwRhNU_ zZP$mxJr857{xS<($jeh_5-=0+z&o&G)F&&`G-w?=XyzPil<<-6y2+ggAVdOKKlglHgVXG)WKWoPaddn5(z{ zti@9VyuyunV%lD5V>9gD@z{M#K}W9kuCPx}NZt?$&QaHD!nE6kMS4a$qt zzDeJdBg9<-5+Y_!r*DMCs{oVn@gH=ai!$qoh{I8U7Y?58RlzsujZP@kow@9^RB=w~ z-s|@L#`?T`EaT~6%Dm;p|5a*! zb+H|^X(+=v1g9w_$d8Yf&P1(xRJ`-hBOziD?@Jt8_4u-T%I^)XulooK{47)`NZQVw ztnU6bB$s=)ec9YSF7K{}joLTWox<(AZ z<}u=9!fSBhk1a_fOkL>?!C})@S6fz(v)h7{tjS*kQajAs(&>pCwTlDDduKl8oNAIE@L*i0vV%OU;C-NQe$E9lBa zPL(A+Hp1=2vbC_kX&^~^^a3An1cUXFp0FcrRgU`E|O%l>a%I zI{BC;Sr$y#eeNVQ~+xHw=;f}AltL5WFrjN(!2Mq1lN-UA5 zBekOYdMx*F3V9M`6;OB^y<3Z$Q7{~uXW@T zuaCFF@3_Hn8^>QR?|1VnTK}If9_p#|1&w+I4Gul3SwkWlw3WAyA*u8dQ<+5S#nl5A z=Dqb>L9_=dcN)zUj3RYf0&4H$*&{P}?Uu4mvB})cd>yQUWei8tsdO;!e@A5_IRua@ z12<#5u`J=okpQ6e$mTMGa}TQ7pJR05PG-3MC^xs$+yeFel8*&|1%*moD3x}&{k}5Y zJR+HGA_gB>6B9XSgqdr`Tq~2|p-`wTEB+4*aT&@hRo2~g$6c{6t&ZMgxU}V-`qB8k zoWh>=9CC5xu(^7i+rD3D8HPNFtKL|b50s2b`SexGe|3kr$A404bjHx&)ps2GTf_#= z6{qAbbmC&@+6BhHp7gz7Hr=9D zy$LU?OIyPth&o!pU`4p|+H+6kG+6lcL8O<7J=r+_B78wcKuIMy$PHUYC?wd2aIY_v ztHm1+nUXZKUi$!{JyZ0M_lVLuMHnB@S{t_+y zh&=Q7eM(PH;Nf9XXxu$yGZ}|TrgnMd<--M=!du2_Q+|5xNnigo?rdl~w%WoaH&b2l z7u`IrvLr{{+kN?2X1X`O?j~e_We7o)IgKyLqLY_h`>>(T~Rs1MZ*E#y~m_+ zU+Z;B$kc#RD#w`wzyUqx1V?2%cA#bHPirry+Q&u4MhRjF{REDyL{(_$_Kgb|;r0iR z5$0LU^d|L=W%r>g?mi@~aO(Hl2mZ@uBC}`dIMn=MV^vrPrc{b;O6}&1uioq+Q-oDr zQkOXkF!O)o(mbRV8+sMpglU{&husQ<%Vz}5Uh?FEoxbfrv*|;(BSDkz{==~3{^Q}% zWZbmvRUR9i&$I47bO3j0wGg05G&kcT16DzrECNoJ7UNfPtTUBy!AE3+0EW5r6klas z7{~gkj>vmVBbrw7K(*~B)KAC^EhU_m$|_J*VDT3Ub*+}swpD*J3;-*+Mbkpr@IkoL zXn2zCE_L3qj*A0Ml{yhEU*D&W@3AAtZ(^kA)2N7dE8cX+B8r53u;@HC}-&%9-n!Qh*&_|id z69_9gQ2q;D2UaMyAsC#OXAAf(26qS%Al-6k1c^%k$r*QN<|!>fi|uM~Zwz8cP3+qy zfUncTWT3rgUSIXv=_uXgNZLYWmQ^5Nr}A~uH#AA`T3#F&C8Mk3nx^W0SRrsrE#AN| z`(@9spG>_>PH5}WEbwXz6gDpPI(8W=XpL1noTs&n=JzI=&=Jm?J!Yn7Of z66zuNUgo&C>FtT^ewSu(QH8Fdn2|Y#=%hSxe5oci0;S#cm`#T*x|E$> zQM{vWQmG(KmCDMWJ&1MJ3xoMEr~pxi)1^tVM5GleVDyTYua z0KKxxjCCGjubMwq<*pRPfc}sJq)6UK#`a6s`5qo#yQ*}$OFsW4`7BRZr+gGzOh~5g z4fM#(Tf;6OnG79{XEV14SqPMYJYW=29x?Qv`vJg12tJbfFgpl&mJkaoWbf^zSbp8P zlO5^Us`TH>dbz}_O@MEkP>SSuk@#G~)YeAA#9~QE z*o2#yOFDI}w7o}A$RW_;!sHdke;E1lb%EfC)6C1aVHvjpZ)@NGV^v;_%k=R=S&VgP zeC4PKNaD`d+kgnc#_#yk|C)zWBcN;icvCNE%}FIqF4XwLDPHvO*L%^+A{dMHDTdXZ zYQKqZsz5Y@SemjjpD zt|mwCZmth=_?+DUGC>V2C*T;t$XAO7e?*8qbD`=biOmg&K@ysYVKuN?@sMEVU1~Pr zR4**cXqz-~Y!OBI_c+b)(6mPp5jtH`d*Z;$s{rMUf;-iQ+=Lr6nSVVsK^>^nB~#EG z#afu))LAuiw|@Oqz_hqaMwYjeKok?4<>3WH5;JSi9~<9|%hgRmy%9Mo5# zB3a7qQ^M*|(7CdpUsF@C>6Zl3)NCiqj9kR6afdfkOxIy9Ms6O-zOn*CJ-YIS^zKC zen=;%fJdamSCl{W{-GjQF;SLCn1`?ehufg5Wn^EXH= zO+4Rq|A%lq3)B#T>^z$C_$~6GVj}^9)HmKRb_5+PZpx}HnwXYfXQz&6@SO>%8TE&O zI->u3>x@o4Z|1{l=0krzZ{{5(Z{i(~RfdLgD)L5GG)JAJ1=Fh6-vavMRfbqGdD^{m z*jQz73k&p=$^St}e>yPFmSt2Ap|2f?Vm$R=n+g7OgCWmT%7J(@r^{g&Gd5o}t z(x*f?VjQ)aOaDTpPPI+HFD|$s|I$)gHzk!uQg2yvn}yyBVxX zBFKd*vM*gtdK}V{XffX%Q6>ng*3b6`F&v-@oj)*_rL$sd>C*)J2kr5J;#nF)pIa%nFT$5dQ;v1EJE2af(5#?>6%;=VE7Gu62FKd^GJ&AZE=K=>>OLYx{L0Ih_Mo-UO zU)arcKxUG)BC< zrxMwTNw`)8QHA;h<|}GatvkkL(1s^%gIKHcXC!)a>ULPzSZr{O8NWAfhx$A|J%8*D z!nuJmlH%g_ee5$_X5ws{&ypCyk8~j7ln5J|h^{EC>a85`P3h=iD%r(bik!UT^JRPe z!(O@V5U`#1K|RMZ(9HY3a{q(;jHxi6gNV^>QTgsW{sWw&nR&xq@sy`)!;!@VtCfkF z^c{4QOxIA|=!y28=?1?|NF8)UtL^}QKenb5aVO{+=GZ7x3o_JBR$t>UADmh9s=&Y) zD?E#8Yw-2e&;9j$Ahb8dqhgkOeRJ5smi@fTuun+|?;y|H&saA*t`J;k%>N%oS+-?$ z@KO{3kp@lY8d6=47~k(&1^@MkjK~S9GEtLt#GGp9%}r$X#`&Hkn*+1gK_=1U$_?2@ z^xIInvFoTyIE|#>-%ceO=IEixHI`jvuf)Cw5%bov zph$K5{kjwNM+xayQiWz*!f^SiCs2clc zmqAbMW6tozhhDvN0Dxj!e$D-#Dhm?>PZ8@sj3f31xfR0tTB~2-lVq(v4Y^g)fkBk# z2|NA={l7M;e>gPwQPCmwso1?+pPFX8b;fSGycdnpkzk25;JUyyoVstBPZYUbZw&s! z_p)xe^&ZFB8`R8vURB#yJA5Q7 zI*qvcvkRdeV?2^it*a1w2y;$^eCTB;n^E;t>Iw+an-Y; zo;&0#OZfHY)t^_zU#iPawzoNH4{8&`!7`?q_~UoZN1Kc4NYa_&h#yIo{{Tq&(Q7xP z{1wje=CHSVUTX_RCzZ}atCjk*{Z8p+cw}wNxbQ|Ej`T7A0DOB#!57F}_nlly<>qth zzce!Q2brdwXSs7)Sz&qpsU}YxyU=jV9R6&2hnrK}=6|>*9z|0hum1r2{zlighs%=V zU^tXnPmtpEiP4|xm!i;S6dgG%TK%P;qznEJlfCL{{V36=xIK@L-26qzxn;W_7a-OTZ$H&-p@`cS^oP5bo0_>=b=$8cmsPyN)tVPqJwZM%??t|F z5c4a?*uPi)oqsm8e0uHkA|EB+^&6_e7olP0+AAE#X3qUq3U-G-n%*VuJP>_OL+T(J z00Z@1%9?ZZ-SFVK^=iBP&gI;$R+*r`HIllz=8tmjR^!~W&#Swu@d}jUx1?zI<@x;J zdA%k61HTTh{W|VWZ~OjbiS7*@>kLHHzwBx6MXfs~AEO`fSAOW?w?@)ct`_nObYYL{ zmAKqeeRKM@YaH;E?Casw7e1>ibvc&K#l_r!)l}6+7$K)-(Z$ZTO9XM^J}YHW6QPpb zhjm$4b!LLgpIwp4$|v5~PA_(UHYoYsAkoGL{vBNg?oR&z{OZKq_bVKhCLBZN z2dfCsq1IQAUPWK7s@W3*$H7-&JLXt%ecb;5HCa^216xeM^L{19z$q#YRec)#SdP0h zs9nf@UT1ezmycNFRXn5V#@Bp4c~IVQk?`v1={Pb+{wR2SpQoydFTCVLTS}AmMTf;& znkGO$c&|@1j~twAJ<9SuUn{Fb-)i1*cXG-+1DlXDdQ}S&x&_m*nVKe&Aki7r=1)=Z>eE%pYL8IsV}}B$`4)0Q2MWSNTlMmi+7<)scJt>sOZPdPgY0THeaJWp7$@&NAHDW1AlpXrhf9;MFco|)P)F<`m~R*aegC)uL(Y9adf3 z`m^&I-e$_N*?Bi=k|U`zU%Do!|n>j&tK>==wVU0D52Kvgp=-oBWXif+727sC}9L03{qv*kNOEjnC$? zoviz;3-ssv8b5;Gt*G`qg}*ef`_CttZ_B9nf@;PpFejfGxVbu&jRyGCc zBCM0|FVLO)QQdCdKl>`)Rfonm56K;kbVbL#1LbjI<%}z`M!0tsk?I3aHq%o@DEmwW zXGGY~{UTfVB5AAYA?|kYUWTropnb4fze6wkP{uTe47bF$Bg}gfMwXsrEHa)CIdPx4uhJT8t=?D%gc;AR~E#>Z8SiMZM*@Nun zs=(~+pKp5Rv-7m@SuGF=k+tq?T~NY6I{G_)E%P%VEpFr=eD9*fIiT(dw9&niF`7C? z-N8DvPQlV5O`YsH^6*wYApZbh1M*!VOb&*;&TezWbM_s`fFcdNVV*$W@|=j2*+w)8=Kg%)&mqmFJkf+?Aq;_^he~P34l_ zug|_kz0yPc#l2g9YW*eN@V`layf4yS@L5$AyH)UCr8oCh?9r8zqy6Q&_kPTZ;nRPa zbS*4|4h4-XI2iu`y&v;lm!nw!0K0Iv`WX8pKf!uVi>hyiYySXmt^L9LSLik0<&WaB z==$H>AH{kui=6%U0KFGQv7dSOR%~#`_gnnY64W~1vOfiLg8k)hNWhGSVC!(QYqB8q zpB0envdk=(F0pm+RMWs3@?mT6$TA>grIbR-YFS)evX4mo6Lw5&;qARVtsS6vgy+u; zv660GBM8l64(ioksC8WchDi7-c$@0pZg0S}yZ-?6k5-EIQ}`p?LO#UhW5vBpw1@e1?BC+EzSKNA(8t{tNn~ z5gPMaFVdd9EqtEEy)@E$*Q$};#k)l#m*}r&?JR@a`(<{C z@pqZoD=6H3izpnuL0+fgv!-$IvfhGL{>fXx&G7|h=KEGpZ?R6`C2r8c z`-^s-CipZgn%{R&L(HuoZvOxU(yadgrF8avSxECQ^Zlf8K2~#b&YXKn$_V|-NF(vG z&Uk-iS)P>rmaWQr4)uD6wm%ZL=4!JJFS@-PN7}V(eTzAY^@VnptAgOQ3sKdZR`Uwp zZ^3V}Gxvh*G=0-$y7x1|%hpQo+h5i2{?p3HwD=Y3&%qpQaBp(u&#mEKtMgiQa_i&O z;He|F?6nSdFS<1Ns!Ztox-!}NKNdKhfc zzyuIE=CccyU0L6&nMb>-cKug__=FFd@3oS5BCODv8#A_M%0T#yoXtpBL^ih)QJSf- znD7y8SC_I-)?>%B!nFAV)I8Zw)@w?x4As$DRx!E>a{Wh!2pb!CT&vVoy}xEAD>jy@ zt>11>;DC>!p{H-wer0VcO>F4sKw zuhH4=U!$|!y+b|A)CKB~?=H*7wPg7M%2zL2{*~)Txq9L5SxC~phrM<|_ODh~_JMZX zJXdP#OZ!%AL}ASzwW`yo_qVINuFA_ifI|9pWRDct8sAYaD}9sy0FR2G={SgUU~iI3 zW1$x<_4=5DRf9E!t<72({pQN^RqWgKV++!{NzdAHyD8WYJ1)x6XtY`_ z7M*%67K=rq(P`%9yCHT$?5#TVO&t2|)s|g-%Py?5Rk9nKvj?=5c^Vwkk;S1@mDFy3 zkyN*IU$Ehmny9LTt(pm(;2houEWQhuR+%6?&MNqk966(xxlbLhiWiT9!Lxb&m6bf6 zPFd)M@toDd)o0>eJ-+7)miXZ=-y=nVqvklD>;oZmjvO?GXHzF1qM=Iy`^7 znt_fMsbq!ZeRh%jzACPYTkT<;vqaOW2X)_)nUW3oY9k{2E;IZ({x?UcA!Xu)h1cLZ zqPfVNC)zBjq$4**)9_j>g43%{n4Y2LP`m!29|cMG*GsdWf9m~7r%<=fJtVCMmGfSW zh27^Ry!NqikoOC#K}Rt>@Yo_|X=Be0vEsR`xOHT&)Z3M1Wtl$irw$HlnS1j>0~<@3 zyWWFaO?Pq*%{!w!ozXqg;*)8u{{Z0luJyyN!{61GYb%aQ%Hba65gA$6#GwP|7f0@K zA2vNh%%wm6NB;m4sZaettDE*D{{V3F@AJ*&O%`jPJy zYD!Pqm-sJ9DGmCgAgLZ=P;e#Qx_-N9MZ}@(;5o@>~5ICyPiw zfnJ(A*ati=2w~}GF!GUrtFpF|Z@ddu^iaS2DZh&JkxL)4>-3P{y^`Fhjs6Y`EdEC` zZxv_NMh`38uSVHsJ4^n@1o0F`4$2ifIh4`p*=gEOe`n$qUeY6oG0@1vsJJaEws$(r@^|7VA)9PI zB{;J9cy)MjS6XmF>moAlN2=Tq=DWh0+G7X(AbwvrD#&%8@mixlXeN)XfA(MXALzGP z(7X56^&G>%g`G81e`FucT6pi>Bo|gO#~*uQb3X;>x<$}`-anfDt^WY1+5Z4{3H}Sv z^pzDy?P2~aC?cEuIYRvorJv#F_%79Ce}5AFE#I`zy>w^Ai?s>;kLI<5_gAclcrkvL zhrywGO6fh1vfq@;;%>0S%YCPuAzpgl*@7~DC2rA4Ip;gp2%O3Yxj+Cu^jz;u{w2o} zwKW^a`?d?S96l!N($*ga3oZ9O{{XgEsb{r%xCgm*VE8D_G;9X>;7!7EwqWyGtdx21 z>+fCMk&1?y)h`^f1KOK4Z6Ra?`qnr(qsH1U_01Kbw^J%1d5U)G;mtm!(uYqcZTIab zLoGoOomkJLym>5zy0k?bq1AGaa`;_a+NwD3xBYN!f4!Msfv++SuAFXfy0I&5P z{L_yHg`G`jv8`Ejg;RWCUWcZpVE+L2=lHKh()(L*k`k>YJtau*iCO&*On&Qd{tGUS zF@4z``L9QeeeBlj&%I?)Fz}GC(9{PyHNf6Yu)*({{Shr!(!}r@J+s4(d49k%>F9EnXd1CYQcaG;pS+xR`U*d-G9uw_}{&4 zsHb!f<82R!LK?b|VdETa!ViiD*X~+ovMWX3eO2s-cOIY@g1WYf*0EZ9780@u+^mth zyM^&?w=dp5^O}TrU*@*+2VH;N2jUZaRRg$VjlY-8^Z8F%Q~v-xir>~8y$kBci!QwY zbf0;cSE+BkZ{TmWZ&eok(yVH6efBlqlFiiq$gTJl>)JnO`LEJP?yTxZ?yTw$xqgsW zuX6RzU7UU;SP0`*iI14_V^^S~bYq)_e-(O(gGYO>Z(1Pf`~arBO7t4^&v z)8_KNU0Sa7Yu@OrhG_8Ofn3&?SGAF_S1U!Ay%DKH@I;SdzaI4NeAW1(yRNw8zN1E_ z*YHtB1)lp0eNcl&fLrG0sCD#@_-G#WLjG=v-ye|A=Xp2{K zt5fe<`I;^(iq7jqOpLomw_b*pKVw!@xh?u=9l`+@Q}!xnejZjp^)49sL7yqk%h6k3YIq!W3#yK{Y92xVh3d} z2LvOi791VGpdIpsNb{jNbzel_{t4_puV0iX^>bM)ZOE5_>|*3upA4iS$FnuhJUc;MKoU^WZGAl1F~h?33c+y-PjICbWAx zwOJH4aT{|vAtgcDyO#xF@>i7I=d&9#aYdk-YHz5+Z{BkLxmOg@xBE8lTi6x|Aeu&v zFtjuygQIf$Q-3A@0Jk&z-{iCC#($gqklJ)%4f``+@>Z4_m_E^HBj8pS%S$WWklK1T z`#7zyMAkYZsE$){?@fcHY2AZ^&S3aCVuAVnQ`UNd9aR>-Y-R7^)hO;?_FegYq5zeI ztR!ve&aFCyw1Al)<;AE1aW!I$-{h=OZw?(WMOr~+zZ+fbSqC@4 z#eFmTOV!`Rtf7zGvJD;`TCURF4n6^H!6E&Xl{>ww)wFn(o0t~#_V5dOXO)?8_My9V zlLFJ^%{?WbQS_bkV@@&?TUd3^@hc2L&85=K+=zc4ioI1=vM{q@dHY7|(8U`(7#CrV z9t(OV`<11nf!Z73&oL=E~dyAZeBKazDHPf-Sg zy7x7=Z?&uXOaA~()QX5TFbCiFPvo{!?o2W_*8TlO6cFkeThKT2>Ui!{;@8J`{&jDV zB1Yne=*l!%=z(Lg1--iEx1~5;^qI{I?w%m7Px&k__oIXtj^%xmlS_xOSc`naPQ1eO zcODBX74#NzwZ6r_Pt`W*vrHiq(IHj(Yj$nHT3s#Zx8Q3nwFMNUj~5ch~v~3 zM)*JSN5jnMi9hN!zZ-sks9P*&T}M^53Dx}`&iNuAi0Z_{WwM(;6l(f4*BY4C3C0(U zvs+qs;v87A$=pZlj11nL`8BFac%P1{PhB@I;G5EQ(b}ow(MIp=$3GvIvGXJ87`Jg8 zGdG`ldw;W&eTTcKDjw{Al0F`1M7w~UzwRAF=i%%)p*^6RBMrgo?oY4i_HSz>Lyvgs zxYY){e%Zddu~xd$W1TtsF{mF2!9hx-f@J^n))Q@j%D@ zGxd+lSzIj2%c;#e9F#L_H7UaH1##6$`(KxvLj5q!`H!1t@_TxJ>l7zEvn^WUf!_s* zhU2aMAwbOak|zEmtPtY1RUip?J!2PiTqJ4{%cmqA8?m#9S831no28W4CsfUx&dMmh zs`A>~`4b&?6aA_Bqq~|7XtPE@50kngPLrp&;MZdO7BSK>242{mx-acbk|(xE-I#j~ z!SkeJyIH47PMWcKJy!cpS+6LeIOEp>4J5RLEbW}%iopHX{;_njoGoa& zH4_^~Jyd_1GD&#o*nKR&{6~6@o(2fvjA3g>50vQ=4js?Ws!n@A^%j5HKk-FfSzD~N z-KBxbFTLjf0NnhPN9>Q(8yQYq30_B4zZ1oQD{oJ8MpQWa7L(?uu0qauOY5DDhH{l9DG-jS2uU)zk`r#-HJ1zSekA> zR+?tN3x1g&f)K(mLeA6AW+wjt6&)ojFviY2TzQ|6t#CFXCuS5Bua-wd{7!puo!|xS z?M|YlkW&a6*0{Bz?l!->xm9hN-MkmyXtxW4`E@y@*!Zex+aSdo!^g1=q0!iY3cSAf zqo*%n9360}X0DOaMH4m*acDLGWIig4Q5g=-3^HS2ZuQQIr@d3VX4h3z94*C|1ObW1 zp1CH_*0w2`QM$*5VIcB7%2@VWt$q;VHOL>pg_B2CSu62F%$LL{TH2Ff!K*=ErGKHS z{!{*dt+s34bQl8ik0N%?*Yz3uwMEj@!w|$6Jq`0d)YoH--~O8IMf;0be{p|f`-&=9 z{F?9jPGMCJne>ej+SiHO_u;Ookwoe@;)qL#vA|hDCJIYRKRjU(Cz(J%fT$~Q?%PI3KGP-7_J z8D8#jvJq@)1D*c>QQyHd8VqhZZb`4f2r6NghEt~3MbG3e7kUv(n%K*}!`XKvV3T%1 z*?%-~3~e6}u|#e-vy!_?EqJ&*6RT+1dPjDd^*@pw8PX?TQ#sYAl5L%WSo;zU9v78T z+MlH&8|>%&lcIY}6>RM{n%Pu)9>1fyQqKPBb+M@Ql+N{Ya40p$6er+=m~KB+%PXC0 z1X&(HoKJb|#yO_a(zeYRxyM(YXXG_qAl#hD60*nVaoZN+TPo!B9d6$BqX`6+2L_M2UT3>)Wb`TXFp=$ zce{0%K=*l|uGn6%d)I7x)U6M0V?_!31FXHxY_qZ{WP&&7=>vB{HJf=`TZ-mfTh~!Y z8?ln6nknaryd)z(`QD@RBa$2U{l47g>HMUVkz`!Fn8D^VP za3#3T%3hEH^^?r@8F;ayfI@<+`o%o#b_pHZiQ|pX5*$v2*e~Z_s_ZzSc8cRgmZ(K3 zyL-@@9LUUb3ufzWS(5V3k2quLJnf&h($y zPIU*7LCxHnGwg6ptED?0kdNLEB!3d;v*mNr6^*K$Vq~C)b znBVUm%=ZN&v29%G2{K!>2A?BfwN3=^QND2V6C?%OKGNvfB&UByL?wOP+(Ehnm$&g$ z^6i-ATlrl;u#Y~^|MH@MRGO^nLW8@_Snh+joB<7)t9&JDx#n{+AQZR9!TY@?2)p{2tR zvAULDDcRP>_H1CmaVB~1aI$rKVe~R#jOVx<0x}q*HsDh!n?}gwGBk%rEuL+_De7uR zrmCoq1X%1gcsQGDc9bKe#k0UMUj44Cr!yF7weIdH_bo~oq;a-Ov{z`Z(L;FK#deCg zab2RjMRtnRuF+bRsall#@aT#B)K-IO(6+cP{1S5cl*nf)b!areV=*R3Po?`xPeL|WeX{s{6Ch;n(r6h0l zhsEgeeG&9Ww5E@$(9e)PZcRH=52SCT0{DB?9U-lA;hFfhvq~RgZG5aOqAaKq%uSxr zZPG2@-)~_~rJ4AI*d$c%bv-qqAo~EKnS&SCNx--hh!8Ow`$B5et z+Cf2W3C!}zH>2X6rfKP!E8iq|FuxaPzf{(fiuOH{I*!UG$OqMJ*XXle?3Cf2SYT zMXnYjP*;*v{=?Q6dmOfJ-KZKKXtCn$LChxt5-{)aYO{v-2XbCZS#uxInFdOH9nC%& zm?Vkf#1!W+8G1TT^sBcva9&SP>(M;^;Z2?b1xOc;LB&f+R?H_yF#aKIiMZfUonaicXYyH5v-g(mJT3RG!~^zE@n6{w*j~6d!GV63 ze{_EY;S}>phg<_iy*)tx0J~+J^Zx*y{MK^6+alh0_LaXQM}qa=>08q*=0Cg^Z@-I| zX}#kUPN@6|(wDdJ>|#05pMd^+^=VLaG6&s!y&J=o2f z)(q4$RGp0z-WlmFkQv+C`KV|`K#|*E>6;L~5IB46hmlk^V%*-{9N!W02h56GIPglx zVYTm?=(k`lkIG3ct}baheQ%1|Hj=17*Y^rw?=cAZK7X z-s__S=4@`3k}EX*ER`n7?Qw2TvE$UhTf*tJ?x>b$*|r+lzmjg(7A%hc03{%}c6PW` zEhgLT;eKm3UzLKsmtav$E#)(ITt-Z9@le{XT8Q4<_O8S)#3<@w7+E1LEc&O-GfO0c zBI_}?;-ri?S)WP9%hyBu=sOc!3AX9#cZ!ei{z?~prhJpDD8?F|E&9pKdrMy2h%3n| zkE9)m78APRp;#Yg)Q|NdS+5qLZa^FmmNTEyYAkpJAA;FWchVlrpH&Fub|xOn)1sB4 zNS-V~Mp!|TiN{~77D~gRMl)-Xk?&59L7}sP zZO$m&p;68qiH#QUS!ePFM(;C6HEzZiWCfV8R}7Gj<`fHAG5vyzc2nZ-2x5uZ{jjgN zJGRaKD|%{Z&A7}Jja?*N;rA)Bj+3Kf!?A6!z0dH`R!L3L0+ptntZdG(4j?prFKU2C zTjkHfaY?kt^o_nBAxeMgH}Las#Zp9Vg|Xru)BKeU6j$3#AZd2Cxl=$*%x^5efmPKz z!wh%_BXj(|K6ynRx2J5H>2P+OPD9#NTWi|*T^mLbm=`_(r?fEQX(HCst8iW1mLH^h zJ>_QR*eUAYAY@Tn4lQ4DpAVEzuzzWo&76xFRX)oO7e}-`l}I5mHKgJgEtXcdiVq!~ zFKZ?-+w21td{dxkcIm-%X&VqbJ8fju&ULf5Ah9!b_ag`9vnSk!DF^PVp|E8`CfN~l zhYlBB$_Xup6=NTS{otz$PMAUc(2N&zVv~OlHU9vn9^01rIgH`%$f}7N2(?HSb-t%| zT~VGkR(xibLgu#&PX(3@%fzNWuNEu-G`o`VRSlj~7Ap{JO8qJ4{@G1*1i7KMxfWD7 zpMcW8d+y%%T_*(3!-`F;7rD=phLHN)dsmOEmY!^pL^2vU;=_Mptt5RAY5}=4If1P1O|OM^-=7ofNGx%G04_3{Z-FtK^gKAsD>mwru89tCrzpLhik)OQOTJRjwi5HTL{{W>mk9H`|jzy0GNZr7v zRZ)W_A=RT+bu**uUwUJdav!1H;IXpIssnOPXwy`*#^7v0QP#veon^AsycWt$4q2fi zV5*McjWg56%Von}E6W_g6)BSCvW+t0s@pYvH9(RgBZ?YaJKQt3oa_W=%w2Af-v$Ni zP5YK}$@|OKN&CfS_aAEK=g#Qlh898rd**%+6KZ^>o}wcCo#1{e8(*$LgGzuOCx7wv5dxCxheKjKAn)l$e8D}I_($jcjmC;HlAC!opn*O$e}Up z%ZVxiXlcY`Uvg)M1D_9KgUJ{oilzolgMc?6{tE?|=dgWS-C(L{mMGXx%;=}Z(e!yq zg_yAq!s=E@(Udxn-J6TIO~!ncOhkGH%=p;*_JArlTPvLdH-jDYN!39p4#R9E8ip~u zLzy7{Ns~xl1!aR|b4Dh~Zau{YMT-s>3%gU2ZY5xfq@NAq+ee-J5RK48=TtS3x?(#w zcNP5)Sr3(TRYOR^SOyRVpp7$K7%8F*j-MMD8O$n(8xV{-Ty}1NpqOugvQ&UvWWPb=Qow`^^15oF8#UaYg~Xn@*Y1RGfx{{W@p$ENIje)Ya6i8a>z`~2OT zqGZgyz!l_=~M5xa;kCVhb z9Z>ANBNXg0$lVIrO_f0Gznc1wZxkLkY8%>b)<<+7ti&gTnJGWogwVSG09lI0&S}&m zH8?M(I<$Qh0XU-k6~&&ypIy<-yLEgc^6FJ*15F$-gSFYg7;X4p zuB#0-RcLXx#wfvOi7GeA(QW9kL9-e=U37opk21kJSLo4oEX4NQU7DzuwgpPZixxJafpbdn! zOOj>9+8i=N6N|0StE67&Jffm38#ZllUgtF%Y5iUdR>g|P3Ann+ccMO$Eo<$B2?vTb z5>kqn?C>47cT|2z82Q}C3BHZOp|k9U235$XotoqCaQMd%HVH#BV=l*H0kLdt6=L$yBx3 z>}>XTXJhAEn@IR6S+b3mZmn*k&brR z{{ThfP_W1ekLb!4&HJ@2(EVlgBx>8ml&$TtVvg56sLq9XUq;^#)2CZV@^J0Ox$^}& zrif*$qFG<2FmTY6))V9wlAqL*HLTCj5Bs_FJJRX9^PFtp_pPiZz+M&T2UgUHDFg`*$=aVLOI z?AeVn7L#j%!FgRf*cB_!@xR#Rp4PD=cYkN8!k@^I@VQGReMtS&QZxGAUKPu8!S^w>GxV90{E3( zA!L!ww6KikhnDW~+jQ$Cdik6y(ID<@E zqd>kZp_RJ2V`(nNED_?68#%k&GALEVV>@<xf9VgEFwVgv9l)bg(f5)eo)4tehEGRU--@`&%|fo9 z_Sk`IwsVdvX%F>!*6`rw@JcM`pPN?2{tIU|^bYJj8L3ZqGULG|%s zdv(ZplD0FM9`$i>NKP%ij)Uu2GAJT+uX(})M-=O`4CLLh9@Bu;PUhRtioCb1q-&V$m_R*~o@%7@adibe5wuMC+ztF~ zbvKl>R61y`$uVVaVM!Q!+cpP~8p3!dUZZD*9j(VcTB@Fs^5>c34|WGO_^{iLwQZiu z&$htFT;49YBbGg+QSEly_c;UgyVNiO495FM3)tk-$Hih}-ok#Xt&+DTB+nBW z90SJS3>ovjYs#3*(=_14duOec?QX7?ji9~GgjzdYt~`!KRbq%v*r}||vkvID47eMo zZ0jY6%s2O3%ZvDTCP*r#ovmx2BH)(u)l3WLhL(2e7G&E%R9n-&nip*1l1Kgiu^*`Ku1dgTO88z zcm%E5DuaLeWn59q?P|ba!g&|8ertbWAKsmx#cxznH~#?SHDAl-&`^OEz9yz%+n*V;S%F} zjg9zl;Dl|xu1-!lp9N=&XV0sqBY7ehQ(i*XmXHCzBB`NkacrsC9ygqL#eoG-vR6D} zdja3Q!Pmi4wX|@SIo5Mq4a%erf;PzWp@a|z+YrYIE#Oj8-4h{Uj6m!EN5%I-s(2u3WZg?T$g(pWkSmk5( z`M8Mj4Lz&LZ%}4!v5AYf2(Y&=t?*t4%&vvKxgy;h>_PkiOu^7it<3soaZ|cVX3;c6 z;^>Fb7;WaT7lF94s)0<>^)3Jxxq#M07>Bf zN-@dq{C-B>e$qQfR5W)q=x1qUjpnS}`<2G)f)Lc{%&U6H{{RJcoN)dM<07n0!o`CF z{&ymB{!!zjcNHnUD(A&dK&E&GLbg6##`TCJ_cuVdy? z!xQQviYSC+HP#M1^7fWa9GUl#mxxzt}OdSTIuhTb|1xgJPgMm{>_{`P;8;0m&u_Hq4&>G z>WYn-KBqT*(48*^1ub-g(^b0v0J1dv`npYrJTRC3?thn8M@~)L=v#>QjE~43u*k*U z?EqaNM^ho9s;FxVCUx@TW6JAf$4x%Um9f666JvaOoA6Sx+x zf_HBod@j0a^%VY4(*&C`ev9lZeTt`N(S|%NsVx_9vE0G#&)%yHT^{)C+MFBC$k~`U z@ZM7zN_W9T_j(+E9ar^uA%0A@KWdGWP(;W@P%oTGbESq~ozZ60mfN#@lOl^`&r7y* z%QuCEu-}o-B^J4APd=`Y!G~+)=LRxBz5A6j;SDt#5#<{qNNd+eMNr2x9P#VKVpUQ- zlfEi^Eqsl3Sc$eDh)-$);si97D3>Z^=Gt7MI4#jgfqW zW=3}x@lwY8vB5h?Yre{Su(_hS!Og)e*aQ1M94@J6+KWCY^K7{Ao;jy_OZVA2+z;fP zEi)nVji@3*eIo$O`wE`D@&5o>o+iEXh8>w1WHZ%gp?{e9D4{ZJh{WtcxOr-~sAag+ z)7CMU@fo8Sq-;$mcs{9x&ha>ul=HU)vADI(c08`Kep=Wj}|?pEC@D48KL=P`w~H zevG37aes*S{Lo&)WqYTv25SDAk~goqR;H2*Lo=tmA95Zti&53xaO~u zXSVCYbW#gBrp0}f(oRAd5Bdu1uE<&iqSvm=E(edy=jRkr>x4w}8}-D|axztQVUkzYGM+cFvwv%M z98SUyE()7w4J^~s-e?+5?zw*@(elwYO2^F~rfYYIez)*d4;>pN4Qt7Bfswb*_7kLV zw$azab*b;oI>+L=CfO3{FaCsa^n>Yixo@>!`epN4uPRbT@z2}e$uXg}`WHHOwHfN3 z>DxTDMc_9JtlT!w@>Jjxhz;!ex{UVwR@yPTigh;Oz6}rRdI#=-=q*A(#5oDy@P{yEz-*LYOJQ z{*Ior$=|j0&Ko<}+bdiyk(MXTBTaEPleTw8xKphrKUsYK$~8uyY@bf^C9wxZy==Sm zRI-DJKAtxMFQ#_YTISr7$9q)r30)h5nivByHbcn#Rc&;4jkautCPlk*t&P*iBj$9x zB-jT-A@Bv){8OM|F+m#%4F@LjQPR|xS+Bw~&4&CJNdEwde|SP@_v&VwestLhOpwmZsy*~3kDXL_{Is;@lyTd6yxkJmyeJ?f~;>T0r-5# z#WMrgd8bQ5yx;E1ERm&-92~OOlDkQ1=C_D=<`6CzCE+7-D(?oSoBPJ z*~EU)*Wvi}mhd?#=^X29p5|Y`*>sd7-vsyn08gAzN3JQP55V8<&5npvKOmF>LHQyRwhrL&8()w~lS63K!XXR3bELm(TX#Mpa+lg#OCt~L#E z9l}Q6;8gNYDF#<=xHOPk!}Cq9r_8k|zu#1FH} zhq*P^Es2}YJcl(5uc_KiZ|<<)_;q4*U=O}8?o{q*VY@z)1Gx99QqaSQ9@Jp|DaV)x z;JvCr_Dbn^zR%>V+4oQ5^Gi<3uhy&d(o)q`8=L& zaE{BDlk|jpH*?@t+L8Mye$ItDj=7oeDb&?l%=#zm1b25YkXwxwP9R|;iVzxy7Cbgf z1KhGNcY^FY+N^RaGufBQdw1b&6@R2R+dD66)UC+NthPowNy_UJ5KucVe~oLXG?T7rPEK&yr~d_Rj$OKm@;4 z6*$$|xd!2kfyBlMR=WW1U{2Z0V=fjra_gucZ?1Mc7PbpUzO zSx{>?9>XoS?tS4q5ytbGcQpJl+~9ll(!%OTY@9ISzGsAZ{{RfrzpJ{<#eC2Ac>e$l z)Kn7FowPgIpRU#)%i^0P^d+#w{3X${(ly60E>qY{5Vh{~hOLFgihMq)&m=qAyG?$2 z@lY|1cyk&7#o^T~CfFpWk*>Y;wt6m#r7e;Qb~hN50$#xESGe{qv=5S^oy}{uxC;O{ z$G=4lQ0R$zP8PEhCIz01@WFBveIZHEH4^lT97ug*qrkhIC^Q#gFUJm3aDlBhek`IYZ_ZFE7;#({3rNzV{_XKd>sBh z?9p^`$2b~>UjE{xbD(8Ra>inOZzKXeo(NPoqG@R<+;E4!N!@eZytad*Xlj7qcEiTN zxe1*VaN%QPYa+dX8fv22REFM8!yRD!yDsLY!4Z-NZk9=r{{WXw5;G1b8E?+~yO1FV zF|XP^uPnIduN6-AVZQTsnhzfJM=LBbeUNx2JByeKb5U=K=@>q=dMfDt&*$@N9M$$= zr@3A;h06?pwE#}iHG*3n{vmB)ahjK(C1>RE$B0G>j?nDAV7={ebm-Z>Fycl_j3+B` zRKMUe@K+w?pjeKoDtrRiv4vlk_RdXI^9JH%ttXCFB~#Qk8D*Ca`&$vWc6?Ps9Y<34 z<+9mCa(CKLHhLP3kJD>o!d%}QDGO*G9}9m9Uq}d5#0% zed>mKi6S)DY_E>z7lxi~Z7}Ug5yRubO$P~M#qA&Y^NJ|>f~K-jPZ{T7ZdWJpPpG7r zPZM*s?{VBhn6fFGvW`4%IWth4Drq68jx&5RMjd;q!%yg7Gf9L*ZDy0%9o9}SsFypQ z+2Y(khDe_)mqK%S`nW#~l@(-_eo-{17-gM%HrC9k%Z77RinE^(q3K61xbX zGMMaSb(`@;l@--*h2i$b_hEgFxuGlE&9mCq!uMYySngf+T^F>$wb`6_0k!Yn)c*i- zm{V+n-x-XVaN5Vkba?WJODWs3E|&m&Kmanow5R>l)}0AmAR*nB339V;19be9FMbgC zv~w~|`Ye+RvhNF|`Z3(er*9(81H>+kgB70)IQ!b-ZiAXFv}0tj?+M4r$=tWID41=;@|au1s?v< z!MV9e2BlbYMYpyf_Nw@Mo&N8f;FY+7FQ;}f^-ikoyiyJbLq5IySC>$Bb$mneOnfBm zhL$>n0Tyt|%HtzqJ_|P=1xNk^Rx6HH;;iWeW0~s5q>PoecX7I}Ir_q*HoA^pjx$LC zk<8e}`xf`AK~qyvw3(R9Vq>|Ui#cUK_X^(6#IK^YYVCw04!Q#smuRc033FN+&0KO> z>(t2~6K81t&H5DSgdHCUbt{dU?@A`*2VGeEzn@!y-i}(0nBoj<*WllepCo@pl|Sis z@^_w5>!;fKqa$(OmDfk>DqHytsjb|#T=i|DGz7)XO`@YVyBv1E>L z2FTzawP`jS)l;e5B!g(;*I~npK1tEkdI7O>i_5ie(R8_UvCRYzk&oJXyEj~yM^#C= zXL&zoyE~hw)5pJQEsc-6EWCl+GYuo$8M8MA`sS+)0`&osnDWmUX6OF^C65uud*Z%# zRBx18V+%|Cf91NN!ffm>`Z&Id=*s#`bmfK7wl}k}Vo&Cws_7v9kcqZzqYii*ot14p zB@EJ5h6hD%7T1cfkha~+9G8)?k(adw!x@VUueyGa;-HFoT6tZ?Ibg2BUlRvIT7A^rFFiQlU-dLlGB^xY-D0k%Ia!oTel>Rb?;{DZy&`- z!)`6fWTi3ecCrgd$@?$=0G;WvR0>F7Yq4OaBIn#nGHJ*T3Mz(5+IrRzo6Va40BUtz zZA3QV6c5IH6Qy=n&<+7b_;K>_@Imb+DtvixPZ+6#u>!`^NatMfz0XvT znKv*H3x;FNDeZH`?xKD%hihy2s^;^@^Q~3fEbpwp0u`N~0-WesdUL$q<%@|oJyuY( z3vAhiUfFY4S__@Xbvc^iEk6}pIlQZ5Qcy=cLI>Y79`L_~rssX#OaLR6B=N zZB^{%GR*tOA@NleMW)clgm}C80q4Ou$WIT&zxfK9qx;CiywZ)4>ek4iOVSZ}K=20s zAn{u+kEYl(=Ze9(_+L~qvePdftAKCfoFl~0;b&oWu@3dvzqMq`cdJ=#8`3 zEj2{m`ysDAQIYv~bwMnxbE6lyXwOAWOvgR-5#q!!+ZK6c;G=ylEbn;_iNeLh9lSgA z?o6nwYh;0}Ck#0|;+W^MD6Ho>(=pr9V$2J0$$F}InsjMyXw4C8oAj8}%7zy??Q?Ln zXJDfhHFL=uT;1Ya4}XDO8z6C&&2)`?oA$n1V(O8-?mvPOeISSrARO!pBjj}X2ieDa zRN3GlJ%l)77gb#?Q2W)FXM|X0Iegsp=hdES?bDL9m+(%)+%3((cP?2U!>f=l5S01w2fCJL(7wxP}q z@@5PX=k$tuFN+Bx>ZE;+sy>Ztt+YF7EO-q+KXQS@0VPS>y50JGE$7kHfpZktN#qFr zHcV~q=$u>m{(f_lhfrEAymXRD7P!DhskE{has8lfOVm5n?R5GdTfs$9EF?oCabg@i zQ<)wSg}B^aYyenWH8DTpjMUVpcX+k!P1+mb(!$+jazWTbyNYwTlYXja7H+lK*lLE@Dd2n=z&?>ES1c_9!OeT*R8tsi`FdUUa+g} zJ|Sl1+bCVm&34|qPF<(2+tJ@X)LeAnu{V;bZ*x!M^UT(rS?0U2yYNiJY|?WnyD_*& zJFFW$``>AF9fhy%cAZ3Dm;04Xj;<(GgKQmVWymiZKH-vcF!!=NKrBxX2BFd z(@w#@=jF$b6-_a!sEP-%>u*Et{+jmPu(|FaQHlV#XHRC`g&Pph45p?Z4JtWt}}|5X`fCZ9OXtT_JD(07~L~E-s~-DM6)q^h_ewzhqmvS6^Mb{{Y?B zd4}zcym%*;jIZWG#_Hph)NXlWV{5K?sapHC`HV*^VG3Wkoa$MbIBBsgJyGo)hZ(R{ zT_$6WT4}JwVspL#r7+>#BUM=P8u{gq10!lP2?h}3yT`#7-LABFEDsgZohta7Q6&>& z*+%}(C%V{=f^2;#=JbUN&7_7#4LOSpimamt6JdRtd$@VpdD?a9)l)TC#|0}|c3)k` zPhx1Bqbb=;_XZhhqrmvDq3>NQK_SFqu)nnapxYSSA!}WCk(Xzxhx|Xy5?#IZP!E0v z^Xikh5BCL7M|a>5xLO2AVZt>w)2bxz1W$4=(8z9^;88=tJa?zVZR|QJDLP7oo1Eks zOB$vIt~#nWwa)apVK&G)Ln|lf)>X%gX&oW3aN%IQnmJC~MjXnP#LEfdUv0XTb2CA z-b^wMH_b;b-b*xQ()SyzZ;;Bz6U5(3DF--Bl;)=+F&2z{6{4kukw0eUNY_k0j)}Aq zu(`xGOSsr(%X`$K8TL52N>) zc0Zwwpx)YdDyKGZTOGDz5W9~Mp%}3j#vkUr(=#1Btr5-!qTQDTh}Q|&bJ;P68@s!g zNc7CD*|KwHxBhbZdwiMmMTN>Q!y%0oMgA9=I|;L_E&IVw`#`{29FC3In@(HQPcqe$K?JUPRptlb$3ayhr7nE+j&dl5X1qf+t zb|&p1qUv|HOzJ&mTHQx^PAGj}a+n!hMBcCy+EjS1a7!aa`EH~B8Bp|eS8%2ov~hdC zmA&egjB3X3cev~FGB=f9!Qc7wkC+q&S4Q^C+-)(s>M9vbf}bGR_IAZ>JX0fNWzKuB z4;1L8VU6uJu~pslau0&?XFFw49f@K56?x!hcUkM+y}P{Dn3&c`+xkZ2?|RM6zdY7u zr{0a8+k1bC4ynTF0dd;>BYqwOg2h;8G5uJ87f#u?1b5vBfKUkwpCs+X0~kN&1r#+7 zsl^Dw)?pba%iM6t+)XVP_n~5Q7t=q*V8wnbRoYsuP17B;b|!XB^i62qd(DS=LDSk8 z_-zWdJAkg|mmc-n9^@R?VQ|oej#*tJxwzlV%^9b3+e}5fu2`JM)DivbvO$e#Yeqq% z&A-l>BXw!IYnuUS4K^kG)|XUclq1YCf$H-kn7W?K7PB+`khE&Zv?_`^=|f*T-MwcJ z!H0C)#a4MXRJgquwX#uoqu{C8%!GTB6O?hp!z10JS~DtXC83r$+jm@!3XZB{781s_ zp_Q)0E9BUz^F=dV?L5OMMh%rZt zOEsl7syR5>(L1y|{60d%lI;bkTD5%@k!qo&l9DQiJ{Rqzxv=Eq+Lr8^Vq@7giQYJ^ zmzix>a*U4Nm*k6_NhdN-P#$h8TyjEaj?xqF9mOy{yJ7*_L#|50q-Cg*)^-ANQ$*w^_Mc(87`=4_;pMNU}W2fZ{}C^T;%i!#XD5K)#(wj^D{ z>ZiMFX#mNDYEj#VG1nQia91F@n-S9|Suc%EaIJi?AA+YEi1DLpft zv~CCDH@M`tTtEj97gVm36pxZ-;y29Ps3|4sgHIIgcFqW+&klb|$;^sPtT)|#t#l+B zJFX~OVCfth2~K>?I~M-{Yik0j;*+r~mv`L=HM$JGN`d^PBjBm~u@8@&dfVaFW~PT6 zcf))@T&=8h_?dD$LiuW;4TzI7OWB^FFDVfE960e@Xu3Db+lDteuFt8BhqaD-cX8Vf;+r%~VQ|p7w^P*AYb#7!!rD%hQFU2$J{V$#b%9$mqsAg&V!06Op2h_iQ^zfpTPn(o^UjtVHD zBtq|?e0rtuL+3sVG*RjvC$BvGdb=f9G5+P0FZVA}<=FR?o&oz|UmQdB*RG4+ho@(_ zt8M=P(pRo={e^sTdy{~@t;IWW=k}2FN80pX9>PWs`njKF)wWFdt-LIcX~34=$~<#C zmR|;XUy{ENupLp$AZ$tzM8hP6V#3_bl{4xH*zE=(#EvLZ$Fwz5;eVtA7Pmfp@>J8i zSsh{F@^tR{vyZ^0j!$=-dfDOEQ4LLWQB%lsiSEMo9uH|$0kO8SrWebB<>X+`TX(LG zjzJN4hDMo5&6?z)o5~{rv?dbFTi5VWoLIlBv87W_TS-mQ6fzRX-%#6|j%l*gxwele z;$X8`;ET8cqMEK+zy722W5w{YV5`PpIUEMXK3L=)LKyQ5W%-ePe-!i8$#<)cD%e+N z;<3Lkl4*@ZSz+rsNyFsNN$tuQNBtle0*^Qxk*aj1_ipvM}P`O&#^ET@8Y$r zrVx6JX9tH4uy~HChuA-YY3}*AcbY*7-Cony8aDbr4X*(<#lAPyQ2J4z=ei*R;?fkMTNtWZb0~z7e(xZ)Gfdo*E>rPI zx+b;TvAa?~ntH?3_VNWr@3x=g@_}13g<8mXE3n$@H@RCI{p&Y;da#$AJi5=LYr){; zO|4_1x^U(X;GsWce9(M@`n{zMtwYStk7ydm^7oetMKVaE zdosr1+>X{f555poQOx%38rx#xb6Xk3ucg#7V`dDbZXEsT&BcnL#_qD3t=f_r&kib= z{6_vgdHjVWeCEHvsJqoQlP7e^dVJzXZ{y`TUoxE7q0k zSFB#M;Js(Tde4IG{1<6z7QI0@17m&c)sr2_d6qi)mK{gMWSFV`;Q5L9H+6eTkNV_< zY{n+x7;SC}CZ0F^6TKaU^K!#$JAnz9+qc?_*k+j^hdX1)a9DOB+nY;{N$qo5Bf*S} zLm|ON`x{E!O(!x@EPgj^_Kt5QRV752TM!oU%|ASw3uM9t^-W}N55(L^m>9=MTG9qr zlKTGu1=1i0+{WbAlb^G}PFt{Wzy=ZGe=8^?ron>`&=%wT6j28Ggl-Ie+dIf9E-W?iGLMIe+dI zf9E-W?iGLMIR^>fRTmacasL1`*lI_RROx4()9qA|G~6|%%9V#)0{(Nl(nN>99cif?-l`&JE|#v)g$UZ}5BSF0=4mFmjuId&YoC3Z^CtqRd| z_H_Y!Ec0(ZuJxeytwCIt7G~nCb3A&T)gOw3xLc~w#YLr;MJ z0K#A*!HvQKnth{A{EWKF(;w8E(>J<9;?CStjfO8Rk=}Wq?Z{Z)H_d4vvnLLk`dv z_MPle)jCJXE#$d&eV3akg=B%vXJw#S1MyZJZX-J&ZEm|gn4k5AsFL#F!I5$d6_`g4 zM&IDE`Du6@3~jgIh8oO0&56IkVRb_Z?}#djO47%}DEY57R4gQh4=0!9aPl{{WB7LF9;j7lX_zvc&yWidbZgx3(uX z!Esq)D|>Kvt=)O`yQ)7F-sd=cet7PvTT=a;1;-PD4->w>D+jgvtO_?c*mF^bI@tRu zv$>}O@@tP%9{D1sjs2HQ>tDfnF^0Dmn|vF-?n(B+HUov*BXT{7vDQ>SY;rNeFN4IU z!G6rvRKnldH}O@#WJJVxXJso}GbJr`wvijO_#$0$RzogD9IbUtO6G>J-tOkSo=n2b zW#YC?b)&uTf>T2QKE?eKVe}=~i-6BtttNXpK13J7cHZHB)EUFBEBv^a0|K8?MJzEN^pSUj5it)J$p>tT;-Sa&1DF7s_Z zDbl;4WOm89M%QX{=VcTlajrIV_%%&V_qFVVSUWbg-s5Zg75Vi)!BTg$f4Ak^h%wFk zYPdgQS}%po!)-nNMLt;K4{Ke7+}S}xEii2{lt?BIzIHiV+t+@rRA9Flzh|0}p@K1N zfa;IMJGt)bzsxcU=iC+i<79e=iABbQ)A~D!9WC)61jf5Up~s|Mm5I~(II&rlx*Jzu zre^f7JOULxC0QG3+;0?Zxh1>@a9E?LdnxP;I1QRYpq^QLo22H5-M00Efhm;IFuJZ+ zT%B;Yozv!kZ386A+-#}LRE9%v=UZ0hx=MTCrl@duX>LbE`kk9&WRAU~ZJIn6u@@td z?(XqaM2py+Y(dELSjRi2J5#_fSbdFIz24>8diq}VqrK|oo7@G{q2gv=#}#bRK`A95X5LATu**;j54T0G6Ug=4 zWK(>*qiMQNU@RqVba?ron1h()x!?S4s{d@OMpfKE#tFcJ~oa!s1pU(1^8 zuE|;zBtRXT&MUW2P!7d~Xi$>|Fb%9q4ix@_U66NjxuJ+ScDLL=H6x>fCt=h!S0Bw+ z#%SFIOJRJlx;Y895q#6Qn=PDUdyUFt9czQYsXq7QK4?($az!4d_>^gbF80J9wlB<^ zChYQm1x#Xni+y61_-kln^N7lCm0D;V?k&w1GusmI)C^|Kf#VoLk z$oU2HIoK|Z=}L(k4achDaNOnGgscO|?}At!J}PKi*#~#Q7hK=Ct7x9fGqrp`X1qen zL`WLysIeRk54FKl2FV*88-U++wN9s$*k0FJ9F^^&kILhFn~ue;{G56xx221o4{$qcNsF$Q8iPFnpBRkkaE=dIofmF*WT0NHx4iuFGg z>V7NK{8y>?s)kKQ$!!DN#>#hba7CD+plnovY4$tpYpu-riWcR)J21Bw9xu(B70Q*%o0AAU& zMiyK*!ACPL3r*0hqpgaXSGy5prwMDjlT5u9=V7)wmq~$yb55R+I-+ZA=V1-vshcAA-R=nTHMv~G<(;q_5f6M5Y1;q>_p-0r#Y;GsN-qh zWytPRR7V?Wo=>S`atj2l!~%P9GCZ%mP});RJA<)raZfE4#BXJrvB~vDc2#szykL=- z16d#9j1Y_s152-2Yw|;HDaP&z(lFv-Y0WW?k?v!AEhiwU%x%Q@z&0GLhGm__T4+oD z(^{@!KGv^Pw_q;zFGoyAgH@oXd!NnCD-}zyj`TM~vVCGcLf&Pb7U0QpS!S-smX(6G z5ii=}vC-6FKT+!qL}uL9>%E?Pyc8oAVY#l_?S<9@+x-6BRw-V zZoeeyDc>IW!sfd$QLl`|$z0qdG;Y~GN4z=CHz>GhrEA(;Te39$XTg|Z;mgarSi%P*WMDcMBSmra@kf#hm+7C+#fr#)z}{8*dFEBYU~Br3)O|#3$PbpF3JrpyDxhwuNEktZW*nX zOd&caGT{ASyq-4TVbenh(M375Xk`_@mxaUTnyN!3|q zv&*j}S$u}7C&KkVHnMA4MkjF^Cpf%4?%oe(mg=YbpD{grR~4yUqP23##cEZEy1O@m;?fqz zv7S>;$m$oAbWGcLwR8^DEt|az`e1U@_4fV<=a&VkTCG;ARjPPPd3m@BS06#lDZT74!d`0*)ZMF6V8_xiL1aLLZW$lXD zEt>&++?C&Z@Ipz($*yB$0moLI3UirjG=}eLnG|nqAmnC#iIQU5N=Ihe6lx~v2EBoy z?r0ik*-}IwhlFecCwP^zFS{D+I|iw*gJ{RQB4kgfj8AxB^v-+rlZCsM$<1`qO}QBQ zrs^+6iIBWwg~umbP*oK{r)&|`VwHwJs(5=ACdAwk?&Bd^!LihxlicyZyPpL1yZaM{ z?b9VqT4rRPS)HRd(_8W>w4vu^0md84nsrM_YaVbDb^TVO%2)t%WYdMy50!`2nTUKO z?*YK6;H;UkvC*6_IgES|cz!|Ht;IHIJ2n^hDCuN$jg^3hzGJzD_ZD2$Ki&BT$rFT; zyD;XPgYsH+Y3AwG*}BnbvyTHX`4MA|w zGutF*lF?|iUCTwH(P*?`;1DwiAk!M4O<6O9?-xes`$703?_0+RD zfXgMxFSeyYMI#?PAQ#0I^aoNs$<1pvScQvEBAXk45rcaIby zer$T$q-2%ssJwO& zvMX5l^#PA1>s{|%w|mxP`JJ1uR_}7Y!b^SB=EqS`=>sxjBrXRprO5%gensJI=6p0F< z;BWYa6&x+aEQbi>rKg>kC5+*7N?7P_SC-M2w%g(f|!8q*9&d^q!xS~CYo7pHy3(Fi3ng{t8NjE%=gZw!ZIzOg{!k}c? z-H>;W&8>XaUn9+A-+Ij1XL7A?mm=nOa*SPX#Q44`t`fG+)6G3>Qh7B4TwNXwsXf`` zpCFB}wEYI?5D%quzSh#$_${O~TdnOGCBA6Kf6yY(moh=amggsdz<0!PL+^0orn82Xcd6QZK8RZ&Tc1J z%+NcQO4v(NrNfEfhr~~1RmoF_Zs`er4KLi8QrdwN-;+iQ@v?BfVA!mtYtMmN1MNac zqaEP8P_}HF1900WjE63!edF`J7U}n`=G(z!dfgQ4Y_U5c*97XGjRtCMT^6oq@#<|o zd9#PBsN&xh9S=y}qodDaQJxXq%y%YD1H_RyU9WD3&C{z@$E#0}zgC&~*_D;b&;`71 zvL7+ISOF}jA4Gj5a!$BG!%b5qA4qwJt%=@AOy4BfS*{RT&+jO^iO(bpta8faD{W&9 zI*p>vc){^o)3e!i`cL{7u77^#^G7+HIe;=+Oz&u3ss8}K{FS_#{{XgDRI&R>D=KJx z%NTVfJ9e8Pjy}_rY;>#?(#rN2SkNZhzrIy5 zimzi6gT96Ei~j%;wlba3E(bf>n$r|djo9(X=`(2hHdt$Fe4U&=*R?Q3LliLL%683+ zzpuhW^XuC+&2sB?O#o~)dbyf5Pv)Nt%rVHrXEodxlT|luN_ZVx;F}~b^6rR`?>Cq5 zDU~#m31WA_Yx8b>%RIb3L%nbwep>m`9(i>hqQJ2IS9O$6TNLgNs&sFIX4-QNqi22k zuLVhk&U0Mg$O$&LWptZrTp1gr4rT7tA%dG@7=A2Rv0tu1P}1n%6PsYt>-&{dH2oK_ zrgetgvBKlq=VPH`Wf0XiOaNWjt@tWAYs7Hf@3ahN8+WRA(eza-NdyYXPJSuy&6JEe z*xfb9K(@`Z2*_r)@U#vpN_Snj2JTv_pC8FIn)xA&zU+=RUqU}Jr;YB$_Pm1bd8l1g zNk`<_-f3jtRj#X)#3KN)<6U_xQa)!&*0ciQVB^H~zx+?0ldoQDR6CZc{;IK~c47Ia zQ`JUec=tQJB0Xd-$?=^E{*maZ477*Vz<)pDwzSW)$8ICqXUUH{FEe-F=J$Nefz1VR z#c7%(pyS$;d(1s-Rk77nOD@tp5wnL=Kl+5RHLtm+jspApS4r*V@@CCY*_(~GBm<%_ z(Mz<)P-emzgfLr9xn!P4Lf1s*hqPRRs>RIa{<@~;7%&hVpzH(!Bg<*Ky;N^zvR4xl?fz8K? z4bTrT^=Oz6Trs|~N=LR_ZxKg)VLoa{x$TgbI}>o*p|8;<1Pk?&2CH(_`}yQ7`t z_3`gMHvKN~{1u&Ot7G+%eq-fGS&*_G^_f|D!{(*0Xmd{jw@c&h1q%Z6&H11PoqgR! zXtY-I`vrCi?5!4yg48b3?JY{<)uQYxU<=iSV9>N$exm$>`o1eX&wrn8r@=b}kvv$n z%Ns8(6VBwEyNYYraJ{*L6M9FZ&c?be=f>VW;aTxd~oXV+H3FZWSZ!nlMbB_}2xp|i^p_sAsYH2SLu{4~|7~?O*cIO!m zrfrFAqhCU$O12Q2eK?1wK9xPDS=-TXP0Q0z+`NmLOGL#CMA;dT#s!DDi2cibSzz-A z1Xsa+{X)YzvKWo_#9~nqZaIK+9sM`pj%0|-#M`tt=n~NCl{D)yxGc=Z%HB+QeMAmofSD!eGvQ95ao>48Zg(NlanyDyPij}86pX8g;APy!iDWY>s^Z(4lrYBPSg%&ybcvZ?QYR+N z2z%mfUW|~$sHV*BMwsFloAC^un)KomnNXA-gzM?D6LPaFGnuIFT}Br;mnjh)VJqHw z^)`MDu;GnXzrUyt_77+W&EK{$bh;G$OQD_r0A4PPI^Wz@u1&F`=(nsIVgzab03~d} zrL1>Sw~leR7*_JRtCu9A3-01#Sw}L!w7w&|E5uE!B)qC-`<|8=0@#}#k9o`?#AQmU zgC2~j)ioKziNxkrG0b%mIZjgXIP}#Maf>*_5SN$fKXd8-05eW3UpMLuxtKVqm6pg(Svw#c=}5`jiDhtrvJ^ zj2C%^t|o)L9vVXuHJvQMMY!)M1K}=d9c7(asBt#s%5-u3rJCoXiG160-(r2e=UIs{PF2V83gBka`RfCL&Z7DyCyCL4c&< za*HGPHMR9wu`vvk8fX0AS}}m5q&{u z@_&%FEs299p?pvVO*63EHp`}?#bK2q*YzoO5RKue=OKQ4N{2es=7vxl^D33?3Lj`K z`W;+riw?ZTg6QJ})w0yP(&ZqhX5!@wyy8A~Hgf*}N$nZlCCn#pqkfF3#YYfoZgc3x zn)E0iOf~P1qZ0yjhUUo|vR9-pURS&61y!=CjTnr`kEL@S$*3^ZL2sbfiNw>;VU#la z%-@&~W;NHIlQRR^CY2e^W16wu2UjLv)T6u0++cb+F$M86c7S^Op`6X8r$ky{%!AVO zmv5@!hF(ujZWD2p8C*)5jLXo?>8mdD6-^G& z2knZA`9)NLQjXll%<^`hi-Pj>mB;XzBjwU@YaL*dmaNRtSaZb6_;_ZNfKslZK(;DX zQDWu$THbBM3Of{x7$GJp) zfBqxmaH~Ia{EhV!$vWph`<4U@4Ezwx<_|t*kgOP(5{x$8V4aX=pr}_c8s=FX_lGe1 zj4_F7%uhlK!_d}Z!pyfTF(>A1huppTu3Y9-%`wcPXY{&ErchFi%sk%D}3bnCfj3T}oMmA3!Nw zPl9aB7%>I~ID}k1E81~T#}Lpj7}Nqeh1@kRnceitUM>t&AJj0T_uyahyzAmwSb1am16pQp3o%Lk1 zMPqMBYtFzv*t)qiQ(Pxgxd7RHJ?2UqF5w1cbqmP&nN{Vu4>GWF9it%MH3YG$iWLk2w$LMV$l^9h}LMmc>w3`QE4t|kK#<2q9(NmIB+Z5fn~a}ngQ zVU{<70GCE!md+oUbc~zr4yJfZQDor2y&}q-I+x1VZXkh2;yAw@WwXs)U{n|nH9yKK zh4KBX;x(91*tEB$mP-ItrPMM#!Z+4VULcc;>k&EU7t&jNr&OSK@s?9&?j<&ySIz@?! zTQ4O&`a$qGkIRFtf$cfARpuaJ;e*!@pmJ{D(7YdT9prN=+hzl1Ux7F$LxWIH*0eT? zey%lVg)y+F!vWO-pl8h5vA$4L)+pwQlV(C01}gS*6br6kQ*?QCmu&F1C06w*V=Yxe z#`;9?9{&IznUT$1R0eJjxrkL^USOuiw@>k?3%G3@Yto`IFEgnYJ<&-?R}2%o(xHgC{!eT1SHV$pYs;B#N9Lv0Pg;qYuG$BJVaoAQ^2%@IIi5rgSG+1k;fa{3 zR_jwKMCz$qF=Of=7(-YP**Z|;B-;}HWy0*nX8H6wg*vln-7;X*k8)d!?lDR7io+=F z&;HE-y{FL!x{+_mg$Iwn*dHF`7l6JoisJ2?G%H*9jL{Cj?-K-nTk=Yz2S4Z|xU0@b z>Q}IjBpdl+He@<3earD3LG;ET`Sixid13;k@H2yKriZ>5Cl?%rWbdg|9XhFX)+Iy) zV>M$Q7pBy0RcBBg-I5~-2ZUK#-h<5t1I>tYf%|GynP(H!zynxN|)zrKvbLkf# zdSgw@v3oj7ebeF71}kiHTe)_ou)h7~Du=w}rOa0K66tqvEL`rqz?#F3Cgv+v7BoGw zXPc~l+$26LO7)A1S0F=W^rn5L5C8)1Of?IDQiFBHr^LH1&{qyJ_$&G!?xl?Ebjo@| zp)XOh`h&7r)X9lyjgBCf+)R#+p8o(;6Xd_F5O{y^nOGpapYYtJJKTSBZu>9VJY&mZ z@j+In>N#c^*M5*I0!6I_W(BY3Te-9L&*B`v?z)E|y+1I-?FbHW>_c(R1p{Vv0pcZc z_mqizHp~;O{{Z@fTU~#8W0UTnzz+lrlRYA7Vr3~hd_zHq(U{Dm@t1gFLwaHkcLnir zm+3K*r$Q%YraPTfyYM7Ag6%$ttknK?#9gOXPqY@5y21U-^P3;;=JNWl@g*H$=KlcZ z8VQ{l_$EBm#)6nIf)(Wk;hh+bg2SV4NM(w0o#KvS@##Ha_KcslNVats(x{sS(ueJb z?hHHXey(D~IV<+Dl%D?p`0I+&eTU?Led!D6>-Rb;q&SNvrC{}jQlyviQlD{-j)h7p z+4hxMfT1LKDb3S2XD)FB&|^r_x1N!DZuLx6qb zn;iu&N41|2dJujli=z6)eoYOLSWs%Yg>tnTcDNyp{{R;@)%{FSVd8YV)XeBc&i?>% zR~-K8kNPsaSX(|JEOKBsdw7UryB~nho-03+Spf7u$i)R$`_%h~zqpl$X>9C5I6ae* z7^eY^o0ZiN;q+>VniDTg&_+lH6CDv%=r7FfO0up6%4d66e6uP2cNZL_L!aJ%$YMb1 zKD9d^D&v*tIi1VkAsA*3QRtOE5p@_h6Q4$qXAq*wX}O%i$J7~S<`~~Eu@}j!`ibJY zu`}3Whvdbq2nGHRh|=fSzp1nS&e1R6`i^hU{wMrpode{*xUN61m{-c#7UlR~BV{9@0Awpq!+PuyV?a$ZPZ1U#c6Z2e(lUoo?_IE4wQNYfaotuC%1 z>~7`tK7?h$!m@XJm@Y9|j;>_dEaF_u55!jNOYL^{g69Hex5-L9BQ#a*TW-3>!fH~k zmhA5vl|AM-vETc&C77ztSTbm=14JGL$M=0B694 zxV&|#k5}ee*R(-^f?i5ElQY9I#-~FClQy8{D5-2qJ)))8b3V{F3;=q68qmtBX7TI%k= z2u_8A7spt+k9L>qy;XkTT<)v=pEHu*cF4m@r!;)d@imFyfSBsoUKx({{RV= zE&vmoFZC-?s4_8#a5Q9%g0LBlz~69sLe9Z+m;0>NyEYI(DMok1rRQy3FtrT<-*_c) zmsCN~jjp2oO%~E-wXR{P65aNV&^z-J3yb)RJ(uoTdn|p~xiWB1qqeG$74_qPQ3C}kv$$|eEeL^V;UC9$YrW>^c3 z8)tn7$UnL9FYYG+^FaRKTVpBV9PX6I?3d*cSooF}+j5f`g6SV_Wdj+MY~loiF^sC_ zHxdh&Vk>J=C(L@Fb(cgIM>3yCbBVyhZJNb@GL9{95j0iX0!nVRK`5TW{{RHWU6+}J z`lYkQ_ylCOyUPKyqT;Pi`ut0(@FVmjj`De*#DVbmfb;N&E8{dD2cR7T=4;p92URzR zyjMN3h;06YO#TWy&DWb>#12y)XSSd20hagw08ko%oFh`S>WXnPQ#)0G`-LrCUlSJ* z6ONNKV8f26iPM&*NX{db;QYqLT5&j>^_PcNJtgc){@hLjPk0-%?Y0l)03)W1BM&K< zL3k?Kdh|QWn9$sV<5L4zUKF|Ne&xN880gDtWy5Xxe! zebnNh)BRoZCEs=}tR=V)Vld9F)LK0urRG?bTDTl#VUGnp$HSykSBMu7!RCG|^&Ndv zK-^ZZJoKvlMO^%&qL#H5^9Q5^UA;?#k3=ZMI)WWyU!ZXYGc=vNPDHo7v90%%<{i39 zyi6EqjA7b3xXUs_N_t3nX?M{u9C!QdC(gNk(&>jI@iMEBDf%VldTL)0!F5oOvy&8x6;ZK$la?APJ zel`f^{An%fzcZcUCjs(-WWlF5W*Zoam6~qaL>iK+f?zJJB@pKBjZHIIE!{Imi=4vK zj@K(wtlULuwxGDJakw<>CAv4A;$*eP_1<)uVP+35-zsVQ5!6)~VEd|uk32&4&iFUd1vPZ8 zVOGPhyo2{J!pVaWKeQo0s8u%snU{qcmbBlURKHiY62tUo=`GK2 z50ocs(YaV(ksl=vs11GVmXj} zMu$kZn0k$C9^?Z25Ka?nASX!Hd7?I%5eLr5t=|p75lk?4Ys)dW2Uwwe_hW(o0I?2F zDA9&@Y|2_L>DemGdMW@Gmsq9?+xHAGYeH-|Ck1*RTI(3!26$d#TZCz6N0dARZ+(H}#aq1Sm=lbq!J_O|=H@ zR1Q5%b%>T`8FXV(iIQAgPG3e7C|6L?)(k^LAL@VZVYzuM8(Hmc7w1k2V5bn9s(|VZ zfhghyYY=8HXcimBW&A*Rf^jkGShy}Nx1W@OXm>uMW+#(^AS6k1|dpg zh?}CrOjlA`hYs0&mL8I{WOGvw6s+b9hH-NHa`pF>L{pX}kg?HF77SG{L^ku;I@AkS z)(oLT=d`4%-?@K?Bt0w`8|q~W%{wp&A9&edk(N=w>vD|_3gc61jz0vl0na?f*M(`% z_q3n`jq*VfZ8Ux$EjO}p3pA8xv@NpjaFYR7yF1Qxk=Z|KV}%&z1tDo46Qe+EAzO_n z%^anF=t%rsg|bT%}wud!?>B2TyX>lTEuYYFk;txn;s28Yb5nEx2=hwnI z*LYFOE1^05fxie-#Yw>z&g@HyTx_YGID>Cku}T`X;x(@hhx0jbBeksx2^WPt_nFQYlRNx_ zw#jGhP~Wxw;F-->kC@cTbid!(nG0I3Ie>QX9KlA%SzxxKi+8D88eF277Oa1Fi@=K4 zXr`?98BE*D1iR|#aPog2?aEUWtEFj^BU2@a1BrrBSb-{SFLLwfW(qY8!C91SiAH0Y zW%+|CX^lp74zr+-8GB38Z>kSXkAwFZg{ndCc$}~^U1(j1@ICY@tp zd74>Tjv1^Wyv(XBIFM3grlsnHEQMXXc?ke|df_U#3cTsQu-Ux{Nubp`p1 z%4_*!0MYgae3j?|WPm?KK9)P~BlraQYe4#Dud*-a2P(VhV>7{*iXSm1XFo9NO2HD5 z_2=5Q5L94)|cg9!zCuZ5N!H7lmSk?ST^hO0->Q9renb!S9;dXwboo83?T*P%^ zxA!zkoY2ioALmmzH)5_Jt%^bbm|>}~co*Q5X4GJ^t{cL$cb@UfDqJf8Sa*M7IH0N# zg7oep7z4h3qoi^8%)F^fKF0q5b9gpf0{$VZxF@GCgXRecbgDVze-oxgU{U*oLPkc9 z8UFwvj+Er8z93ZW+br9F^yU8m%ZO*L8k>sa>Hw^RTTslGT{g2C*wsCzUMnwR7>sA8 zV@^tfg$0M!_uG%7r=y7Ymn!4d8;QiI5S7g4C6P?NB2zSklNG>4J97D$9mW3unU$iH zY<*4g39?6% z7v3F?@Gru`AC`8Ho>%^AH^D!xquJ|ePq?sVI7jM!y(K@&LYohN`yYe&oo7ONAN2^o z=6IP0cc$#)rv5xnnofR$>IzW%N7%$k$&O!_;!-NPB);%Nh!gUqWfkiq;u+`c{{SnE zp3MIMw6C09H~E~i?ilr%scn*(_?9L&2tw=<vnwj#>C8>X!sV`QV>p(p5#B2to*7^QU({HngLdNo0IF68KKPXeLeXPRIsX77+e^q& z^Nq~h&$MFy0J_5xQsspf=5q`zIz?sGU^84tVauw%Agkq3qG?#fq8%j9W7O-E9tM{{w(ev0CAJ5C)Tn3XPzhNJ1C!C8Ni0)}NB z{W7uu&gXBb`^Zu4#+3x=Uar&dbh8_R_e-n)|Fg5)%x(J(p zG40EihsdA%lkTyf;vt9O5|`K{psy)~;d_5DLvQ^KC3z0xV7A+`qA;9alU^Y0PxI(c zFL>xO`LOwvh`$$a*ZT^tP&6PHu_gPm#Ab2(T8xmowZ0$h-R#3HmbWYLL1)h|?qmtr z3CK|!$U-uNV$IDtPq-A z-F6y_(^jzdhz%3rhLYaknFM1TeaqmAzQSn^{au$5-fee8R!88D+VYudK1|c<{jmgOd|1`1qK0-c>=lqTV9*mGc~xDvmr% z`XNok>yBH8nT zH{Kc?IQ@|yhq?VnlIhkm`d^t{Y3(V|J5%NwjPd9KMafU{JNk**X!mQRtXq)NPOK+mwSvy^_yOGhJqMvC^g^UiLWSGnk6p{{RxN-2F?!`Bw)pr%ba=)L8nzQ*xo} zzeXFmqBQ0?CC8*l?2mbnMN|lJnREsjoXVYlnW3Z9O!vuouR(Tnn%|{mRx2g-#ME5k zVevklAo7hlC2kY{0AK-esN!8cB1AIN7^t-gbBBoMnC2(6x(lWat}mKk$DY#Od`TK3 z=l=jDt;ZFg)ObEc{{T?CqtEri+@B_j}=4xf7~gx^R5?tC0m;L>5OfDujbW;>!U@i7VR#EWO6{YAxlcICklvQ`C zUL}H;((YwAVG?COikZNc!&E`91+`9Mg;n{({fL+7x^okNU#Irr#B9OD%q_)JG1-Vd z?Ee7R3UsC64+Z0u^B0B#&rJUSd_yUr^PZDJ=7AV1W!F9>oUJXz^c=590BHl@{J$^; z!XBK<#J$E>pque8Uc3BC#NmLfdJ&vVyiG~FF_CBAmlej*48CS#{IYgPF}kpChxsvP z7qoJ{B_kb27>;WAmToefPGvO~iF=!nh9%zRTm~jlIheYM9Q@0tNmt(mGS3pMsBoWb zFw>}ma8>gLphpdN6HHzpZw48!+_RSLyr_+@mS4}?FYjI0zBBT{+kL?riyHhRKf*VD zIFtjHGa&x}a5A3QY}Y3Y1roOAI7P4gL3ZHeJH$&ET8nK{#BGme66s><5DGIaWUm+?=6+t-Nw}`G(c6x2(BS*kDmbI3-l~onO6bd`w`8|rjA>bK+QgT zf7shf94!9tDGytsxvc*Hca$DSbh>!n*^C?g487wmAhiXz(xpyw=&tj`UrLu@@Si<^ zObO!{8Y2}b%Me)_Qj-KYX0B)q+)eK?*jzO?m(ld06qSFsGXTd>>VU6I!JJJy&6AZz zP;cJ@2n`w^fa1k{`^ki z{jpuBvcK3I1 zap{Wm(1)Xma}!R)yi5o0G$V`NFSFf*95-Z@^vu;A*XD`WPcl}q`+gbx$5d@nzc+X0 z6gzD2d@)&XU3??WG1hs?)FwIGw&6zx3#n>zCuRI#8O>m#2qTzwFkOd|zOXA>g~=_0Y&DP5z8b#n^t7G9B0rDEG3 zR<=5OK|W=njWDbmydwyR3B)sqeIU`P^-UovB>7AYO767}XzI(AbcbSA%|zG-1kCJY zEZm3F`-oObd_t_K6&ktB7J7Au4FWCcRW>*S#-cyz>4UpGn?th7nO6Jcp zz@5G-cS2wd)x$8 z*T+ZWeV_oe&f-`SYv``t^V@n}~&Cr6e_b>kd zioZrWh8j$clIw*^Rm+uyjufaBnG-vvEeDOLw`JgdFhAO$AZW@!* zxk50Rh-5AAm?n&_5HZv7+5Z3{9gx`6#v{@>s3AskiG4K%k;P1kmF9bMm2g}cnAib=X(mS#S4ejW zHR*v+CvsX6^jMfe@{^>Gc&ETQ_$LyN$Up!_1x()LuRLItmGDM%_?aM(dP3Y{05D82 z(;UP0hC+nq9*(Bt#CIw=O;3dEo`>?LepM`8^KIr96|4B}>iTuKVPb%w^kAwIYGxBL zdxtnEOf2qE!dLzE;lwt5!tC1Mc6ZbV?b%g)Utc-%5y^iBxMe<2TKrAIb8%+aBGKvH zdF;*5TRuin*1SQ)$|FRzTjZ9E+O{0`B}yid9cGEq*U0i*oK6{eG4m?hmp7-Z$RW&W$j)M7aC*DL9-PF?NZEk*fLvl> z)U%93#wTJ0-k&D_~L+iFX&?UOf!l z22BL1_b|}^0A-vua_ElVm>c33l6HR%oZNYF=%()FTqlW3w5Y+TVg=VRAxt^BL9Mm7 z+xULsqA?a8yT-3`6Q&MjH4xn1rd4yzemD9`JMbHCoJ=fC^YNH0$796>H8v87#wKyo z%-4uBWO-%;AiV?;U2;m%2AKS!^`8{FYjU!B$E3p(Z)n*uOCiI|7fnK7f#-1gQ;3}6 z3T5aLM^W1X%hzM|H1uLZnTGgq#t%&uHV*osn|iLuC?V+rCSkXqLUa0@cAZm?O*72q zH!m*~1LxN7(E5zU1u+*fHqdd*kJPtt zLY_t_aQ+YRWChf!hR7TeU{Y?-+;KaJcFSK1TyESTqX8=S_V#7|3)6Dud_ z1zJ=be&s3NT%_q@CT=(p&f%$6K8D;u4MZ*uW%WgNb;CNz6S7?<#4i!_mxYW6+{@ zXJU4BIO1#XGxTAKhgmc-wFn@Oi_tKRWd(ah}^ni(o>satuRRONER8kE;FhFnN2{Y;JVH{xlMn9$1w7|9-Sa4g5- zaJW0igh{^%%&K}LpVz7@U;Qi;#<{76oUR$%NBYANNR*r~l2GX^p{sgj9TL#jl)Oc0 z=f_z_8mXtuwqbfYnI&gvVsqvUOQ_;h3iQ<3Q&C>=>Q6JDO3!IpnGqG&(E0@8peZ_i zP|YP_Xox@vS@f*-l{xhxgxl%F7W;89l>ADbb|ZA4{4HrQVfy7UOKk z1xv<#cTpqgxS1Y|*Numvw-}nlRX$xj>;6IwxrQ8ck5FaTxi;x# z#e)Xs!xrOB&PmG?ykodU9wPi@aP)S9u39qd&!e7(JBmZvVB>M)q{y8hirSzEh|g*d z*(e7v3>`9LmI~3ilw1LML)I+5dR|wQ%JE`w_D6cpgSG;_5X34SX-h#WZN*<$qVV!dTCvFjqc`CnvjHo@@YiC89 zx`*>~I#ASFX7)>S+VuOv9E@y~ikf+p41_pT2w>DQoMsL^Gyed^)UQQHabXXyPie14 zY@Q~FFft(6L*ff75~^#`u2r)MuS?A48q8c`9zbQ76o*gzk=m2h`y!olfG+P<>m9~8 zo_>Z1Xhb9>nu1(+rezp#k7UN&yNXm_w`rBv*3sict2cDZ!iqg0RI4tOre{%@h~)S5c{M?T)>rH^fKYW19XL7J{H=F2)z6X>!R`9T=5ysX_>(?<w}>7g~N%B+ILG1Q`@?XFQ+HW6a~$bbh%R!wKeE7iC;d0 z?FJKE!d<$=bDoffXET_~^ka`rW(_{`h{qkJ&O6R74AM@QKN8{R(PkXVEzCIEc59=% zFig%zQuSOM_5~?@vb==lV%^V#!RR()gx?h7VJR^LWo$#gwJI)K3Z>*=T?41EHAPec z*e42>t>0p3x1!-SBFFid35-E`Rzy;Zb%s;joydN*nG%eLVXXNUe=n(r5BJhjjSc?*INUw}b^idA!^6h0bA=fDi?<7KKYo}7{{V-WYs>lG6GKmS z==Oq@Ilx#y2N5S#FRm}HW$TJL@>k)$k%62xT#rmJm1s7@rc(0O%pRp>uIPsPLp0N^ zOz}$5c}E^hxPa6ac70NxYl_>m2mx!T@?_AKi@te-SKgfKtQksEjvD!K?=}XHGbU6M zNuFi2;$()LOuB4nX^3q#o@1Ixmu_z$#frjvy&(X-YdM1+VbV0&sd*Uekz{{APr$dV zJWij(7?IcQFLGLn_wIF5VbyWub%U56f&$1D%iBF*c2$E%N4)noCdvkEnr7zeVQP5v zxH6@}8>sqd-9#HR4bEZdxu~e)Q!09HbDu(G^g72=+tT50600I*u` zEmlODRLW@$#HiW`OV4FNTba(9eVUY3(x-s6(RAdDc|{3pCF_DwoIXNi!xWnW4uHsJ z;5S(`uAPX!F{?ra(PEa4cx!#4zew>OAi9Bk&;7V5m>kYiL?F|snat$R@RhlYI%d-^ zBcGG;D;?9V-TrE8KY%!=+-EtY`(k`eVc9AQEOu^aUXkK-ICO;zj&NNg8!(O_N^z4e z8L+8_149IIrhKY*G+LKyDyhBhRq2v3!J{70LtOpcfQ37r0t;<|TuoA=NH!!za@cnPlq{c*+=dVsZ%4#HqP+3sXbMEaWtrGJua^ z@I;l`1cI_Qr6VeKC zD@|?1MkNa6Zwb`)eFFIwu(2g(SHwLo81=tPoH?9&6JD8vh$@wt&!EASbrW8fQj)ZrUv2C2MsE| zeqz9=NVnW=+5=@*lp@0t$(1_yiL)vo4Hc!-pt%W)y5x;- zr)>{g@Ft5D0x8wxQNea-fmXe@4zE#QLjBSEvTcY%9A=aNm?1&K#Im%OY-G!z_k_?8 zg{@YX3@wr6~}n7(7~$8!9vY(=~50Qgl0+MtX!pGX8oXy z2C>o(YbehUGQQc-Uh!D7%MCOM&(AY9;kjXc>0A3D`dgU4d^W!%>U`6O^GE)~zRI1= z{G{n>SRcsY4KU2*5V$XiV+}BE#K+Tbxbqx>DIJXMCtevtGS6OLv>SDdt;4X8<2Q6h z04k7RR!-o`tfIK!Zt4dA04f0|C!lA={WFIz{{Vg9rHDEmr&-k|Tlk%v$ht{+Px5-8 z5ab6KR9c(1rJ^ekrNxetEMA7HaVeE4%oRaP%O#A*kRI~~PQVIyj#ycyP0LsJ%pJdE zGQ%gaeh8gee=SeOS!j5JDd42~x3%)50QfBAg$heEcyEiWb--7zTbbppqTQ3E zyka}H)ZzxD(hi0G2wbYUMRDH^6&ttpflZu`mCYrb8Gd55X%PX3NKzd_XqvdKs)c!btC5~GaY%-xwIjK?_Cb=IT1 zodn@>Fmx6}U-upZs1%G0`@-<_W^xqSb1~8a$8{sbwus+|X6SC;c|ctz)B+Z9L8dq3 zjrE`cM9RB!;Qp9TakuIOXFbiy+pPR>6=$?UffNkN-N^-9+BJNT2eNBCc9|=+-HQmW zg-o#c57e`Wcb5oxZ|Z&ETlY9p&12#J0I8_{--*-y4jvtqPn8k92l2VTo@M_30RHCH z`CrsN!Hg}>^UOAf*gtdlN%0&XIy-cKu3_ueg8KD=2!S**a70<*}SI^MDPkvMC=0q z-+h2^vT?4qp+tLxvL^^NS+6<;;=F~oEV{h#$RUYl^02dvjf!_1bk zQ0L4Qg4nwo>8Eo^X)^6OpM!rkTPkIlSIiQ_xjM^z8LZ(YRUFh>F>t%EU@xR9B8#tf z$mR$Y7}ocP91VIb;WD>Z2d{VeoJL3LCB1r5Cak_q_^Hh!M`}3n%yw5Lr-QdX@o!Tc zdL?l%Zw^L*rqgU+!m`B$xxN`g0<(Q&xk>Cw?uNoZONV{2G0XQqiXzP0c3NpXZk;)^!B#3VvcuFc3=~Xz?(0JZXqkxpPRQ85sSM_Yc6BK81Y3Nz9~Q36TTI zo5}b}1y+h-U3UFOwkoAvh5btjqN&m?_bFaeY2G`WKu2n7C51ht&ScG%(k7+>P0@?1 zN+p<$EH(w&Ej&x$L3QO!Oo>dqjQ9a-GY<&v7>-(-D{~ITDWssjmitX#?QzjBTEw)+ zre1`)sb}JsRLHF8S=5)gP7oRe0bFEb@iNgiRVdaDk?tkApo?HGjB$6FTZ^sE3#N5Y zp-S*Ie{(bgn;Jm4wKo>&PT&WJ5U^slo%d5j&88A4>3ixd)sPmTEaP@S0pAS_IlFXV zVJ-=(OFkw@2ynQX*>>laTXhg@7^ulx0{P7DaBdgV%ogIitgtra<($HaUX-=h9lsfX ze)^mis8yChdAs|K4=T$rB11DN<@e*(&D*C!C5WgNko9JwDwSAqeM=dpbQ8p08L7WR zOOIk;)igTA?#gu06<#I?TQXi^Vj*zxScbYX?pOXp47_W#l+n|H7&Jx8{mjYcSbj)k zD0$#x)+`SZ3Ab=H3NodKMhv*n!XFQIP}q0B8~GS-99*T~ByqcbNxzk)dEO)+t+H&?62 z(8F1|gbZ>ns*8^Kn#|?Yj!8jsvzRt6Be1b$H-$b7{j4_tb}|vcDVCn;79Z}>$|XI$ z9mg4~)@a3-Q|(8Nlf6F^_cAW{z=G9U7=iZ~Rcu=IQv9#e zk^!b&vmi_J0~jmREL8VnIkN1X*Tr?1L1qkpKB>Jv7qGk@VVQ93#b%geRAu1X*o3>y z6axlZ`GsV?rd@8=)YS10Jf|dA;=4?ajd4+7gIfxGn_P)7B$GJ=k-z7i7>U zd%#L#9hmG9aftR2*G#kEU?p?8)@jFF`M|8UeyG$2ZtLR=wM_f7_ zOs{K&!+>_QK5vq;ONYSKAX=8i7SRq5{DC zUCgTQt#6k(_QwTHZWJi}7W+f7KGknlv%K_z@pj)=1}iWr=ZoEX3R{=l3^|~bxdhzG>QjZVhAJ+?Xdk}>Y{KYcYImI22aL={ zZJW|(I1VkPBl4tz!y zp^gMPH>!pJI0L-z9|+J=Jtqa7v)X$8AA#%W)rtIj!$E?(9snScAVvb+B7VHG+e;->X7 zA+v;WQ29;CSqDJj5h=!{RSU~+L0C7|Tnefl+3d=JcNL=Q`$X30awmn47`w{>V)`c% znT+VI-^{|;1j)r5ajkF!X|JB@0cPM;w*GIs;NUm z&;5?0s4**`8n)2z%-r&|QE+cOLyWMxAD1XYT*cDKDO}kH<5ys)Cp_FqgoRNRZB{Hj zAX5l99th(j?F^{lYG?ldVm&XFX2`{t4{&fayHbO!!(&?p{nGGFt*u8$HK060y&myL z9^?*C3s>e2>_3^Lx@?_O5+&ArcO~e6py>Gtfa(yG>de!=*Z_>Uw#qrPL6s z&P9nGOmT(C4cT^)2TZ4kE2(=OabPjtFr~b%uGKtvj7V3V=aaJ7I$Dx zMFf0iVI*CjXZtY{fy-#_VD20~BYkP`nO@84o#PC7Tjc)$We40{DP>5&WI9;i#1V5D z1@$8QM|YbzmpjS{;q&AO*VG7<1U|_9S}Em|M>PQt6soWgpy1!JKF&NqoH+ST-C!|i zd()1;D(Nur@TK4#;WSH3?OiDp6`E#y@hO}JUfzGieF0RrY2;b)sk;yKa-S&0!6{^~`4ErMx{SXhzF;|>EmtfNQ{{Z78cxHW&O=!e8nf8dI zI)<1Q3~w(GY=x0J&{pstQxe=8E*(P}?FyPuyJw^<7f?mXuYxwQdc|DI%x&6dA|MT_ zn5ES0M>uHjv0BCIxQ1HJ;v6M7x#BlPs@Z%apbFtPnvDq0XzIC$*!#pNuJ*179s0QH z3kJ23HNt#M8Sy10S`4KeP3tXzJ{+=HMNQyp}YA(-Z%RwZKK+I6i?MClJ9-Y)=j4=A5>g?pkz z)}N9AAnoX#T~Elv#t&=d3s!xIYsk`(c=;aj?k6@#u8su+zo|Hx6IlNMvJaqf7mTMn zQtFAS-!u0y&{jbJ?5GR3^1wA#p{baJsl<-UoFq#$PcafgoxVLI*3GGTj$ipmpcZ3e1c6!7;-a*>Ys&|wT>5mQFqbF9@6M_r|<$Y@elhjN^O6lKt%Qq zriEP;GMQ+f{i)X)H;w&N`Mr!fw?6sm)xp@h zFCvlQ@f}UZ6vH(bc45|Nw1}c%mpveznb;_+%B(Nu4G&;6^lCblwM|l3t2M5Qa~8d_ zV%aFlCE_kbR>1V#p8WGE0Af0zH(E1sEuZI+quJ(E>l*5d`?(KDFeC=FD4;Z;Q#3Va zh|{%?M!Rm8@R-}V-gO7oYrLke^f@;JClvGSMJ6QUE81e#Gwm^#78I@vY~5>`Ol)nK zY8jgKkBN?=bb|~SEc5+q+H1saF0U$l%tYp3%(-^;O}^_AUEzyMWupo5Ubi&rHN(8S zNDsNASSKO5Nol%;te#t?wd?Jl+IF!1QA`+1occ7fj}uUCDNt^r`y$=*IwJxjzz+w} zn16zD?m@O6lmlaaCOS&N4OE;v@SX|Ba7{cATOHjqKZwppR`U`SaD*qFe^G=R)T|_O zsyNNdq7svfbv0F)d@$O4kttEgxXjt_QtN61oaBMYAj4H{F3{(gLEEx+|v@E z9oFTD;MxBG#vpjrW#;0W{LHFaVOOs*;La>tXzTCfh87JZ`%6`qJHJsegJyzW405hs zum@*W9~lQ55U>x5L&?O?HPsGA8jEba^C%2%Q0T~1vVJ!l#D5oj0NF;*`S7vTsLDu zdpkfyFtO3e(Z7hvvMqntP~~!$40KSuRlSLTYiGO{24KA~OLFcV zE5$muy9;QQ!vTa@?EwX|e@6@BtatbxYAYL_2tl?M{_{U;eIHoaIhMQPEL3k$S7{(+UiVx5R|=pfuZsU0|Qh3Vq33*W56Jyl}8bvnQ;qgOf2q48i)r{ zmk20pKGS6H988MGbKW|jFyg?gzL51+$E*`z$`5Ii0*!Yk!MGpxTQ2;F7r-aHvrTu4 z62tyMHaL`yve+`-0c;Z7NKxxu)$cKW)!iMn*!(bkbT9=I@WQ&h>a=<+LK)G2#~v;? zm3QbF!c-iP)UP-9miDPF6sRvjhs8b0CQ#4}rT8FeWe(ax`XM^tOdY!w^V7VxkP-o; zEm6#=HF`{S@DWV1Uvl>djw;>tZfau(-m#cIF&7&_m zF$xlZWG?e|(mVqw*xo+97X*MvD#C_)+^jRQ9#L&>cXH&D(MHo-j5}J4tU4&zyXK;2 z!4GkJJ+EnOQyp?W;$t@rL|=r;8ZTZvb`cVRld2+*jVbr?(?!moybcwUF=7% z%2%^0+U%iW3&F$&uBb;Ap&SQ5{h0_6``F)}%W z{+Cg@?LN}q;1BX-S2GqpTt?aQMhB*$-*a1H;$p9_qmJ>*70t?dho>n8#+T0SJWZ%* zsvRZ(O87$>*8c#;P+i%kP*Lu95O8p=Ns~Pe(C$PFUKQ?3;zb7i$*EzF>5a*Hpq@+W zIH&0e;SVmQ4t<#WU>^-AVr)YTp-t$g;VLMw)>zbef?d%Uw){dsUn^L>H!2}rt1^d& z;Tl<{INYi#4SP(go#t6)?X_ZEuPm&3Vz$?;KrSLLuJIf*Y2qrjLaxQVg0IweGO5~N z`~Lt5yq(4qHHxfCpOa5^IEgo)5c4NP9D~GcgE)lsBB7Q*kmSJFRD;OR)LWZ`aWd?I zT~HLCew&;>XxtXBQ35x^QlwsxNqt=_;u=~DWCBrZw7D+4TtF^>An+N00p2SuGN(qC za2}nA3Y5gOHxHR=4(=kb(wx3iFs7B1mGo&ZPaSYR`TP04Y8$&pm^Epbq zE3X|5qn71e^gy6nE!HO9<@v4ZyFOD|_jA)~`2^M-GKd{k zYD*EK_u3BWFIGa7ws2`Nedf6Q=+{yRUG@p=$F%?kgKVlb*hUDZJZzHs59a|J(Y>J{X1qOSM&CDU3o(GcKKIMJh_=j2} zYTYim*D}i;Fo?Vrq#bjZ3L9KdZerNp7M$STIq4}3CF<6w)D-61&76j}DgOYd1o)dl zp~+5gEw4ZEJ0L2ITum-fqzX+gAWCsJie2ViS4o8>#mDM3^BYPfSD5kE=Mzx_ccz5F zBWL@{9)<(CcNpiK*gfwgJO#xZFu=ZFWJ~YUeD}s~51U^hDHi1?J;or!O*U}NDoyF; zR#JQoTopjkh=%Hcyf5XSmZLA_d>HH~*vp8)qOY;@nMG@O&@80$7(?*0Qp|8?*=N)t zMyRFyO>dfZt;R4_WVk%Ba;L=H)r@#oZt|S=+PDCgKxx10aT7SoN@z118FXJ0W3Jk| zrlTRZ3O|Xg9}GgNiauORltZNt^(_im_8pjv3^#G%={!JwAh#0=$Rr3=X?O0c;-%0U zJ%6)))85=|kH)>dE5xf&cUBfDGBjjj6)J!jEiXH-X}6|nj!C3eEWo>ZK|eGmH%MjV zodf>>980+gD?z)b6E0qdf^m>F;!~uDVas<37yyW(ryVL)!0PG0k`KPv7f18O+^UDo zMHQ+nI9{$nz%R4J8@sJBdky9Qy*R5aNN{$P05%Hc#O@@;?ax`t9h4STyd18j(bAKb zBJYnAJhoG86yE`O%rZ>lC+to3sParT-GvES*V^V18zL@6O<+H`j(Ez&6dO=b4xR&d%TKF{zoTTwPd@M02u{BwF z`J}ma0=(5hHaQ?30f55MqK?p+5u)S)%|Z0s18wOO8)R#lL*-CDhD)f;&y9$~Uk>%m zDdg)Z9OD>#MZc5prR)WJPn)BkT#a438@IYHyC6*)ZY23i@S)qao61Bcn z+BFW`(A$uCO#|a%3T@J5Nfza+g5r2P%xLBzFhoH_-9_e+SkYC>PsE`zRR!6|)m}VB z)D3ks2ZO!D3y@OiZOs$f;6#(5MRu7pRL80Z3XMw1$XzrU@ixblKYqhY$Ka$J6kEIKf&pMhJtA{FN}l+`*h|}pXr*JlxkJL5aT}RD5IxQ z3&RJ2&#bLYGPD#nHCy`x$~1;~f5q`GhDnpm(=vgFh-o0tGMmeJc1R9|>;m>Lh|560 zFf*Zjv94phm@qMOyW`pg;R-P}F8J#XxIx0H?iF1nnC#Y_OZ`-*hSS$s=jNzN07V>| z#aJsW;(XRXYg|uk)pu6SFIlvL8+r9wHE=g9yXTANXM|Zl-sNIu;I7c{YI}}!d zO7IG6h7J|IsDG(-%(iS~^|E1p#$R^@{f(TG&-J zYTn&>gS8h!kjT5vrgA8vj>_{$Q*Lin7<&httGlGoXzWD>v;;YA>JwT9&LbQ&?k>!Z z^47%<#33(8z`kM&C~j>Vvu?4kGjB0;cl0doQ&Q{4pq9;?&IAugnc^Xn61hd?PpE7@ zSv|i78UpxCh|*)>I=}&RM^Kj}g(Fi<&ya}TLzlK@Fv0Wvzbcp{_g*~%w?j4otLZrn ze9j;m33*r|rftPHFCcaeX>7o6&wr%d8+F)rggF8lez+GU?xjSwHTd%|)?V_-Bx4FA z1Cq-KCV90VcAzHQ>mVdkD>oIf*a@1wx9pA84M8v%VU|{m$@?q7{}*Vjh|}9u?m- zmj3{+Lhr226|qXj@7R+OzpRSQlT{VU)e5xD-d5r@dXlO_s; z3M^umy+DaQm!oKGVf-STv2d~N4xAD;)J5pzY%S&~aBDA43WoO7R~gp;8VP>-TBFZH zTf9GZ-YG@KO9=?6jfMzJ>aDjvn?Ng}CN&U;{3rMu+%? zQnf28aSwG+CQYejG8Q9_AGuNH#~wy@qqX{&PALJfmAJIbo{?haTJAX}UFL%mvN~r( zacRV7S&tCg87C{;w8VF2Z6byPLvw4^ViNdbW#1*0W%!Gg7kM4askWcQq*LB9oT~FO z3p(hA7C0@94fvB%{{UlXzbZN@jtJ-hpmh|sCGj^~<(!@v_3xR%PuYiEiJAkJS%%v@ zOHdh^IpSpB%*F~xG%a%~v$)pgX+y-(&BHdwN>xDLNx3qpDNg=6OCxa9v-)<;a1FMl zU;PmP@SNkFPah%&jjs~14NJIYV`;-C_o;p$8U-W0W?*|qa=De6j(SVnO*)DLnDWd! zTa?Xcw5-6O(~*Hpbpej5E9h4pIhO0LGJmsZ?lhDd$C4x$BOOuHc4#^ja!f-PMN|!8 z+L=lTl54E|G#epOdeIE6$Yv?cPRy$V&8huY)y4cFKESJ zIE~ST*JkQfQp}}ZISReyIynIYyF5f;JIZ}jm9sS84i53}*b7_~w(K5nGNspRZA^~_ zd&*H#h%@w0_b5?imRWuvnQ*8IX{h<6%BTwI25ea>rgCNN0*ylM@|!^I^vV;kG>T^k&ZSO>QA*%b#(3#l*fMt76h897?lumg-`v zh|Y#?JWNP{@U}`~I%Y&$1BeC+FT}8Rx78`nxfd*V4+3pc?t;lgRhSL+IrulxbD}Xy zWvekcm;V4MTL&O(1kmK45aXx3ql4aA!S4gC6Pw$pbDwCm=i(*=cEP=JN9&j_pQHL1^`YcURSIze)=2PL@8MY{=xI^@bX zEFw#(Hu#)F{mJpJEP+O|s2$+wdQ%up!gSvPJCegZG+n-;L8@U; z$kLmaS3KOc$-5A_4?C*-%qsF?7gcMuPvKFQXKF}ue1s8}0bIXQ=U;$=sDlb_A#w44 z$x)2`5BV<3@qfv5{U7o;iPDT=fm>`}%tn=ZFh+myKU0wu81v{ZZZVX(aBSy#i&1j( z-Y;oIk+R2PMT~Bv_n!p9=2}Hd6>HL|^{8ntB_Ll(Rugdzn3r~7sSB}Q_#sgzK$x<$ zXmi8N5TWN6T`E{Lc&;gTn3!G)YYBeqKszIT0NTb*vHt%6@;!Uqoj;kBrFlaLEF8`w ztI;8@_v)7Sh!(p~bW4EbGL5UdlqtPftO&YQ?zYUZCab^xD>i$?G1mS1k`m5kXZ%N zHXULiCQ&r=2`_2V##u0V#r(?9NxdJ6;@R3kL-tv5Ds7fd0@jFOQ2^>3dzMb81|Q6n zUo~=*$XoJ;T0Q2exT`cx$X6$+adnHTRSqMUyrED+Uhu>A*JvBm(gN+E*BXqM6|cO= z6x#=+!OZPA3)O$8%*~j{e-SxvcPKw|Yo#qIM=%5}pX~Ag+>*3ECzd621vwV4`WzospMtPQnLBso(EK znlD8_xFwvb(@U+~t(y{1g%7xlaIP-6W|cRZ&RF3%PrpViQ1cXj3z+65oA(?lH@>op z8D-Eg8k!}_87^fymJyqg-$`?cYP++^iU$hV4=?>o0o8d4Hs0M4l!U>;SEUx@>Qx*yu zlA`yV&Bb6>k=!$1#W`>67cs+y+oFc=COGWiH|;5S%+%>Mn5D@pX@+H)a6HSEbd_c+<@uv|8k@6#ayvG2 zDqx_57f&yWmRQj(6(gVWJ>L>KUVht|3Ngw61u&a<@hqX84}45OHw#272y*ltzVH;w z5FmUI0m&Nq+2UGbwTLTjf)k+WM&O9BuVevXjTq{oHxHTvk5Oi4i z647v2$S&PQW>zmK%1s-q`K5jkX%Nh8k(a8i380Rl$SGkWv0p`ave?Dbf%*J92;8iZPm+u_# zdbMx7Siq1`$#0DGjUi&)J-!Hq`s^)ZZ+j1jIZ+$E7CU8`_rF}lA1v!NT-Vr@4W7>7 z{H)FN*IAJDouRvq+()G9i@lfpo#0(gH5r)!39-n>hNtA8C?W-wJ4P6+*wCt!)$oRj zr(F1n{;Y7*Sn5j;it|c=Zr}$L=hw6VGJyGg4}>|aqv9RPtH|XtS5nFs8ZwBBg%>T> z`U=-}7z*fr)F$(W$L;_F^irthl&vLWSHR*_jpcFJs}9<*s@pMa2F&JAP&&Y+U{Aww z+AJYK<19GTxSXgW$6AWBP#tWzMHiIrVlTU{DhSGu+$O7}sO@3OFfzHp_#r{?0jN08 zt2c>d#1{4!hK%50XMW&=XT&>S6RAERsQSRb7?4-QAlR_G-~ju=6!bI)N}Q%%ft*25 zJ+O)1aW|4UeWAR!9`FHqeK}7{7coqWC3l5 z15wHpMl3Y^LHuE*ihZs`sRm%JdKY<@SVIwYQy5^rSquFTb6`irZ+8aqF*TMvPNGYq zANq|?p$~v8Wwp;uRG=c5YS8`V?twZaL#`T+1u3K37q9sUNDBcn$9app38brj)By{c ztAvpaW)qh+Dkt$oqi~|G7_}EF3lf%K^C@O4%QAe%3a7j&7Ws;C@|I%Y9T7{skti?;S_X&tuvK_yMkBVM>+L1kN^DDDR1#%UN;LDGlZad+#oB zFOIV4hT?~qt87l5u`7sM2-8zlmhhhBq3s@vju<@4KM9By_Vg0RSUTBSH9B8&Gf~hT z9A{D~<5KWq3w+gvAce0L2kd&n-tbCS07jG%d`;76f&)q1nn7O^PG#c^&1|*LCP4@} zs0}fL!cjJUBsSo<#N_1R7nBjhsqHUG@^&(hTwH3@mFIU?IB_szkB`VDS0GN$C{h|-mIVBzp?y<0J7Z&)qBY2sy zCu}RHNHujUC^H7IM@;k)AEW>Qq<-&ZYQyR{n{_#Rdc!YCR_Y?&VBGOA4 zk4fz@Y-`g=UedTPyy+-%U|n9Muf6(2W&m+R$vVmrDo-G^7ank6#eLvfQ=*>a98~D5 zUJm%oW>a2f)!#`@sz6ZXdE6Z(7$*;S*?A)4cnoxhcDI363goi5jR62m8#1iQHMSUV z9L?g&GLJqXOa^Ev$o;1@s7sbW*2la|VF$H!cku#i8(1Evh@5na4_ceWN(d#{=54;J z&)^w{s=Lpz)GZv#-XgUNv1UY?1#2;9Petmh#pkR`0jTRWd=>91g#`nM@O6a1nvbRR zoUm;r+`pB7e3tSW<<+6B3qd{#Ru!tY}eFf1K&^#GWv zGP~;T5o=6a7GE_AjxS#;7x{(*svfPfUv2$C0uc@y09Ruejjil@Vh3s_k|5(rMCyyw z!`V0DTF$b_hD=>|10ju%0I9ik5i|>wa7y$N$Q#xze8M8?5OwVYKouH83~y;#eZ;lySD#m}4a~*bJ@XVL z-F*#{lNFZ?exZgRnWRg#nUo;1bY?KMX`;>m(tz@jMub(@NgIzd(LaGCfB zY)Ozz**&o16pd<8+LzFr0c6S^!S~E_$x+Q`**|jB_dn?YbN(?bwRu0N6A3?!K<=7) zOOzeI>L|XzKkWR`33O7q#QjQnCwe}k*-5K6VWb$s;_dr`fjk#0)8cjd!`|$<*fg-; zk7diVW|S~6Q>zOAyRQ%SBU{ zM%^T(hz-{r{MU(B@rnFkdro4o^<+PBQB#S#$5zjYQ@DW#Mjd&0V!NZV_S`9>YYTf| zr$uurvFwAw)kb2gXx7MWufk(J3>R{>8;&b4*AT(oBXIyxgLg|GcK~+ zquH6IXV4U#A!C_ccN-b~a?80+XDOeVhFxF{$}!d-F20Pi)lkhbRmHCn##^}CieM0# z6Qr_mFumGWnGpuDfoI}9P17YI`OYqHo2~ z44RNliqY{>=F8p=JE34~iO!4T_b(RLHdNaCH2Xpqs4KLU+vSwxD|9t7!3;tn0_Ob- z1_(%&_keB^TS)5Zss&rX!l49HGP!PRm@(O|O#F_JT^0 z0s%#@UJX=s8Hs&!Pq8WWYRD3(F{*>0I$`F&nVHl$l~Ocyzp9OfD^mOE?GTY})%QD5 z?JI6IY6X~OOMJ&VX`e7Jqb?%z0p!f4-Y~J{l~svFHgF>AK((MKDqf`Rf(&ksNv^`n z5HX<9H#m)D`FVlG%59eqUOpy=0jj_z!;tDwmK#;>tF1v)tu_veQM!e_N?my%$ZQ!b zi}PO46;bmYNyhSXZtLNxT3wuTm`R1v@_ha!3&-k4Xm#dWF=0q5=`NOT2`q{hAyH{O z!?7z$ZfD9brluD-uQPXo1$R(V{HDzXdCPx(bgaCSKI0z8#d&*l=&L7Z3i!DL~i9!a7v1p-0PscBd;?832@YEyz=yg zdL^GS106cbM_fx2ui}F9%y1*uDP;~NxB8aaOr4Q9N=yhSdKnsoQjU#79l*GjN1+pV zxM~zN=pZ$6rPigHVeOchP#G`_FI3NHJYo4Ia+j^dFijp*y<9o0DBrR~#%ilCTaAD( zw8!>9yd_sHTgzr5@4-}i)T>z7;oocPbsiNNWh( z7So}qpMr~%%3fcoU0sRRa*LZZ9LfP;adxbGBNkmH<{Br2O~VfZc<{^C0=Gb*zD)bZ zdsz==;w;6jdx4d+*cfVy1yc-V`ew;jU}0PE_+>;}fH10iPT0W-w{Lhnx}&(k5#s1w z$IPU+3s0MVpmvxdA6+Wia{mCb%8Zl+__?x#cR!!JtNH%t_WuC5YJWd@{eSKn6!Oo1 z^(`ngwEe$Up$T87tyk^=RA_gXbeEQxk;>RyTbVph>~xM|vcZAO6wM=(flr7d(ShD6 z%c|o7l)AHS#YR;7fjP!{!J2y7gd0rufSCkK9AJ5-UeTk(-5W4n>pclm^@8p5HU3Gr55J?_am(!&End+70O3FK;r{@}f5l5b$}4R)zacMqE7S6!3Tm?5 zk!DEI`QwQlA`GJQ0_(;CP`6z+PrFeBXR`kQnSlQQz{?nGtg=J*IS~7zgS-o1OTmfl zae)qHIxvdF3-bstS25fS-xAU9I}o~Y+8Ghi16!HdnA})+m})`X{{To7^5l*uMhL~= z5Aua>&5kq8`&6|HWwD6b+4v{P)G^sn;{&$bLdY7h*M0D3w8>Wk%~X9pASaG)3%|<5 z!V0-$;5pF}Tm-|i9P45~pzgXP!DwZO205KT3DPVbFR&j4cE~Uad#7i)THAQP z36gBh&~Ydi=FZnl$0$L!d_`Cw>i+;LU7-qdeBhZc5*z?)5Np{kEm402HpE*5a(FpvSjv!_-)@hnjxN~eK)rdDlF%!5 zV0$$R9a|d0fOL@kp0P~8zJYku6Bu5!!g>rzMvUZCtxCzVIXgHIi%khfpult@_3R^~ zk%rIQw z!R&|19%2VGfJ^35xlw3&1k7cyfUBK9GR*Xsqwy(^)P(s3+s50xG zh4$NJD%)C?^{4GAol19$*p|g0yE~lrBMsuQ$-@#t-fnb{%1?MX4QQ3VQCJkh^1Z?i z6=aA>O2Tw$AaGpd=^2JhW@ry|^9N`P*1E^3M|Q+^=ixJ1iGcDLe3c6u0!X2!wqR@2 ziIM>!-lm8Ig;*4YeZ;w@#mOA^h3<^SLr7}Q!9()|RID*9b;NBfV^PTfW3IBqbG@Lm z-~|5vVNpZZJdoOP8L3r#EIUfN;=R?FvN9epzjOHM-NSUtAbciA z0kQai=Uy)}BY2M&FC`Z+K!91iZ~m$pdppbE!dL*0jUAu_FbUuqE%jY-6)R!M;J&9z`=Y zC;dYvX5NElVYDvgLH8>aI-O^7YWw;{<_hgHCdMJW{`rGzYWSGS_kqBe6&7nRu~oRJ zzi@o!Klr$JxPVy|*{#5?E1`T{)9(W*b$X5;xyl8KIyc`?Vx%w}6$Dn*JsF&DF)oD+ zC{BUElD)(DUAJ?e2YFKDc2rgUOlsD?4P1EEGdEH@u3YC89Up*!D(R>ut8Dzl6wS_QRA=~prro6+wouvS%-Js@a%^bH*hg0k`;K-T z6AaJ}vzyd8_Cf4CaEvf3#`D5=QG|$`+kA|)GP@TlGGkcc2@40Tr%>~JGV z?HSZKn4@Uc#Kbr>D+ArkSL-yv#xKOj^1(7yJokfWLp>V{Ru)$7Zi3wk?-pTTUrOlz z0A^MrKH58Li5p?C7aaNN>e@4`h1L^ttw+ierxcB8Q9+DUZ3C1Ku#c9-)+bcB9mW+; z2-UIa1p&?4BKQYjLPtY%!-{)HpbSg>%W{FR&zA+kCQSr0PNQm(VdaB#juEPfyt+3l z6P^G@g>{iI4QKL`X|ABb?0{I8n7B78pxRNDPyx1KFQj%t%rH)1#Ht>JQZW-YNK*671$ulD7=x zipM?W3z0fa6z-z(h2>$W^9Ra^{Cu>?(0fIF2upClTVOd{78Eqltezmc#|+FH?3wk{ zEmgmGjN}RHHt`Bwlp?EWUxlfNus~wb{SuZ$?TMRXn&jy%*0v2wVzSEGRHE)(YkF#^ zW1|P|7Ib*oh)1m9Q?p!iADq}%)R_bXQLs%noI@X>ddx+z;x}k1prdoZizwYpN*3B? zac3k5pQu3NsKvZXiYwD2b15u$g?->xyrUe<+%su59yypTTrCjVUF&@!;sy-3d7f%% zj7kZI3Eo(b6a64#t25Dcb&g@*LxeIiJ;p=lI)jOqbU7)^ua_6RZT(q8cI3EikSoh> zxPs_*k=Hex_nEv_cG(fQTvhlmejp{P@J<0Xzy(ALkGs+vWx#;HQz=#Fp??u`7MfG6 z7J445+16eRR*ZU{V~2@m_{Em?F}5r0dh}vaMD$=YsLd)c6dbG|4F(q8*AQM3WOCiP zgY9%3Wx0y$y6ap(wLp7ifgMeEba525c4ffV_Ypkrh!D1{Kd2%-$2Via5r`Ut>yi@P zVvIhZ@+;1O@&5oL6$bDx$NMOJ6WAgeqXz9$_j7iyb6Wsd;EzGB8f zh!qq-6Qqxcb@Phl?u|$cWYqvuRWpm$bun4iR&SWF!L7_Pm323WKyZnqz+NSEm@^pk zXsDSbQXOX_iNtb5#BIheB%ybPEwc0ZO6NXzSD$z%J08&rqnNs=;Wl)vMw(;vFU$mi z=KhfEp!SZ6SG=jGvd5@=vZ~9Lw=C4`lmP0$2<23R)T~qiWLc3Q(u3Q{`Gzf6?IotS zx`?Yoqy<@qqQZF*O!0}X4j*a%0GBXdT+;+vp#srZ?MtR{`+F=@ zV~WwJ?9RFhxQw|2NB1k0lvoKts)EC80TBS&-Df#h=8?m9=byQ);vl9RrEV`>xUSc5 z!acCQ(-Bm`fSvq5Qq+Q&h9U}I!-Ffihvi%zU+T~rT6;gk+UFgn)xfhCcyfoCpBvHpoiO5Wot zvR`%U1V7s3#*^RB4-W{4EwY#{6DFmRkEEv)R~UvSCvH+F@iG9$z zecVev+;yLL_SY2>Yj|jendtF>kb6{$rYf6|jCHUI;n*rk}wA&7RYc zGuj=vCZOn*;vnQa%apBkk47J!i826jtHB7%`_KlKZ?!zpeQb1dx5$};yxl3(9l(67N; zjI>pUZa>KJ0hX$ZOS3Oaj&WjWZVGXRaF*ebYfh%6lDCnngUJjv&Z8DR#?6)C zAc-p$jrR=hTM8k@EWUC}EUreq{9;={<0_qs^8&sl6&76CNnRyRyd~ShQtF{<#HMy2 z@XQ)`m0L$xV`mHfteHf7KFiI&~tEoU8NFdmT)Q80BTw+Zv{E4sp$ zgXUKCJ)^TFW+I*mZ6>*vY~krN=^Q{+#aRTYS1k@^1mu8gFl^~E%^T)1+5~p^iM_~#bX&d%FDuwU$6(m@qmMNO~I9re{f<$IvKH;�Zc$QkF+(6E_oLmT*#%0DbOS-1!MKcC@ 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 literal 0 HcmV?d00001 diff --git a/v3/as_demos/monitor/monitor_pico.py b/v3/as_demos/monitor/monitor_pico.py index 7277754..eba169e 100644 --- a/v3/as_demos/monitor/monitor_pico.py +++ b/v3/as_demos/monitor/monitor_pico.py @@ -83,10 +83,13 @@ def _cb(_): SOON = const(0) LATE = const(1) MAX = const(2) +WIDTH = const(3) # Modes. Pulses and reports only occur if an outage exceeds the threshold. # SOON: pulse early when timer times out. Report at outage end. # LATE: pulse when outage ends. Report at outage end. # MAX: pulse when outage exceeds prior maximum. Report only in that instance. +# WIDTH: for measuring time between arbitrary points in code. When duration +# between 0x40 and 0x60 exceeds previosu max, pulse and report. # native reduced latency to 10μs but killed the hog detector: timer never timed out. # Also locked up Pico so ctrl-c did not interrupt. @@ -121,23 +124,33 @@ def read(): vb and print("Awaiting communication.") h_max = 0 # Max hog duration (ms) - h_start = 0 # Absolute hog start time + h_start = -1 # Absolute hog start time: invalidate. while True: if x := read(): # Get an initial 0 on UART + tarr = ticks_ms() # Arrival time if x == 0x7A: # Init: program under test has restarted vb and print("Got communication.") h_max = 0 # Restart timing - h_start = 0 + h_start = -1 for pin in pins: pin[0](0) # Clear pin pin[1] = 0 # and instance counter continue - if x == 0x40: # hog_detect task has started. - t = ticks_ms() # Arrival time + if mode == WIDTH: + if x == 0x40: # Leading edge on ident 0 + h_start = tarr + elif x == 0x60 and h_start != -1: # Trailing edge + dt = ticks_diff(tarr, h_start) + if dt > h_max: + h_max = dt + print(f"Max width {dt}ms") + pin_t(1) + pin_t(0) + elif x == 0x40: # hog_detect task has started. if mode == SOON: # Pulse on absence of activity tim.init(period=t_ms, mode=Timer.ONE_SHOT, callback=_cb) - if h_start: # There was a prior trigger - dt = ticks_diff(t, h_start) + if h_start != -1: # There was a prior trigger + dt = ticks_diff(tarr, h_start) if dt > t_ms: # Delay exceeds threshold if mode != MAX: print(f"Hog {dt}ms") @@ -150,10 +163,11 @@ def read(): if mode == MAX: pin_t(1) pin_t(0) - h_start = t + h_start = tarr p = pins[x & 0x1F] # Key: 0x40 (ord('@')) is pin ID 0 if x & 0x20: # Going down - p[1] -= 1 + if p[1] > 0: # Might have restarted this script with a running client. + p[1] -= 1 # or might have sent trig(False) before True. if not p[1]: # Instance count is zero p[0](0) else: diff --git a/v3/as_demos/monitor/tests/full_test.py b/v3/as_demos/monitor/tests/full_test.py index 45ac9e3..47950a5 100644 --- a/v3/as_demos/monitor/tests/full_test.py +++ b/v3/as_demos/monitor/tests/full_test.py @@ -9,7 +9,7 @@ from machine import Pin, UART, SPI import monitor -monitor.reserve(4) +trig = monitor.trigger(4) # Define interface to use monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz #monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz @@ -24,7 +24,7 @@ async def main(): monitor.init() asyncio.create_task(monitor.hog_detect()) # Watch for gc dropouts on ID0 while True: - monitor.trigger(4) + trig() try: await asyncio.wait_for_ms(forever(), 100) # 100ms pulse on ID1 except asyncio.TimeoutError: # Mandatory error trapping diff --git a/v3/as_demos/monitor/tests/latency.py b/v3/as_demos/monitor/tests/latency.py index 5f7906b..cbb8f30 100644 --- a/v3/as_demos/monitor/tests/latency.py +++ b/v3/as_demos/monitor/tests/latency.py @@ -11,7 +11,7 @@ # Pin on host: modify for other platforms test_pin = Pin('X6', Pin.OUT) -monitor.reserve(2) +trig = monitor.trigger(2) # Define interface to use monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz @@ -21,7 +21,7 @@ async def pulse(pin): pin(1) # Pulse pin pin(0) - monitor.trigger(2) # Pulse Pico pin ident 2 + trig() # Pulse Pico pin ident 2 await asyncio.sleep_ms(30) async def main(): diff --git a/v3/as_demos/monitor/tests/quick_test.py b/v3/as_demos/monitor/tests/quick_test.py index 7d31d3b..5b74d67 100644 --- a/v3/as_demos/monitor/tests/quick_test.py +++ b/v3/as_demos/monitor/tests/quick_test.py @@ -13,7 +13,7 @@ monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz # monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz -monitor.reserve(4) # ident for trigger +trig = monitor.trigger(4) @monitor.asyn(1) async def foo(t): @@ -22,7 +22,7 @@ async def foo(t): @monitor.asyn(2) async def hog(): await asyncio.sleep(5) - monitor.trigger(4) # Hog start + trig() # Hog start time.sleep_ms(500) @monitor.asyn(3) diff --git a/v3/as_demos/monitor/tests/syn_test.py b/v3/as_demos/monitor/tests/syn_test.py index 8e75b07..c9f5ddd 100644 --- a/v3/as_demos/monitor/tests/syn_test.py +++ b/v3/as_demos/monitor/tests/syn_test.py @@ -13,8 +13,6 @@ monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz # monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz -monitor.reserve(4, 5) # Reserve trigger and mon_call idents only - class Foo: def __init__(self): @@ -41,8 +39,9 @@ async def main(): asyncio.create_task(monitor.hog_detect()) # Make 10ms waitx gaps visible foo1 = Foo() foo2 = Foo() + trig = monitor.trigger(5) while True: - monitor.trigger(5) # Mark start with pulse on ident 5 + trig() # Mark start with pulse on ident 5 # Create two instances of .pause separated by 50ms asyncio.create_task(foo1.pause()) await asyncio.sleep_ms(50) From 18014e5495dc2a2d3a39fecb91970fc27027108b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 16 Oct 2021 13:46:07 +0100 Subject: [PATCH 104/305] monitor: Fixes and improvements to synchronous monitoring. --- v3/as_demos/monitor/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index c6ed1eb..1a3ae81 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -485,6 +485,6 @@ monitor.validation(False) The device under test is on the right, linked to the Pico board by means of a UART. -![Image](./monitor_hw.jpg) +![Image](./monitor_hw.JPG) I can supply a schematic and PCB details if anyone is interested. From 0e866f3326b39e76f1599f86f7f9176a4f1c2e26 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 17 Oct 2021 13:38:54 +0100 Subject: [PATCH 105/305] monitor: Revise README. Enable defeat of validation. --- v3/as_demos/monitor/README.md | 336 +++++++++++++++++++--------- v3/as_demos/monitor/monitor.py | 11 + v3/as_demos/monitor/monitor_pico.py | 4 +- 3 files changed, 242 insertions(+), 109 deletions(-) diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 1a3ae81..3519801 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -1,22 +1,22 @@ -# 1. A uasyncio monitor +# 1. A monitor for realtime MicroPython code -This library provides a means of examining the behaviour of a running -`uasyncio` system. The device under test is linked to a Raspberry Pico. The -latter displays the behaviour of the host by pin changes and/or optional print -statements. A logic analyser or scope provides an insight into the way an -asynchronous application is working; valuable informtion can also be gleaned at -the Pico command line. +This library provides a means of examining the behaviour of a running system. +It was initially designed to characterise `uasyncio` programs but may also find +use to study any code whose behaviour may change dynamically such as threaded +code or applications using interrupts. -Communication with the Pico may be by UART or SPI, and is uni-directional from -system under test to Pico. If a UART is used only one GPIO pin is used. SPI -requires three - `mosi`, `sck` and `cs/`. +The device under test (DUT) is linked to a Raspberry Pico. The latter displays +the behaviour of the DUT by pin changes and optional print statements. A logic +analyser or scope provides a view of the realtime behaviour of the code. +Valuable information can also be gleaned at the Pico command line. Where an application runs multiple concurrent tasks it can be difficult to identify a task which is hogging CPU time. Long blocking periods can also occur when several tasks each block for a period. If, on occasion, these are scheduled in succession, the times will add. The monitor issues a trigger pulse -when the blocking period exceeds a threshold. With a logic analyser the system -state at the time of the transient event may be examined. +when the blocking period exceeds a threshold. The threshold can be a fixed time +or the current maximum blocking period. A logic analyser enables the state at +the time of the transient event to be examined. The following image shows the `quick_test.py` code being monitored at the point when a task hogs the CPU. The top line 00 shows the "hog detect" trigger. Line @@ -29,31 +29,110 @@ detect" trigger 100ms after hogging starts. ![Image](./monitor.jpg) The following image shows brief (<4ms) hogging while `quick_test.py` ran. The -likely cause is garbage collection on the Pyboard D host. The monitor was able -to demostrate that this never exceeded 5ms. +likely cause is garbage collection on the Pyboard D DUT. The monitor was able +to demonstrate that this never exceeded 5ms. ![Image](./monitor_gc.jpg) -### Status +## 1.1 Concepts + +Communication with the Pico may be by UART or SPI, and is uni-directional from +DUT to Pico. If a UART is used only one GPIO pin is needed. SPI requires three +- `mosi`, `sck` and `cs/`. + +The Pico runs the following: +```python +from monitor_pico import run +run() # or run(device="spi") +``` +Debug lines are inserted at key points in the DUT code. These cause state +changes on Pico pins. All debug lines are associated with an `ident` which is a +number where `0 <= ident <= 21`. The `ident` value defines a Pico GPIO pin +according to the mapping in [section 5.1](./README.md#51-pico-pin-mapping). + +For example the following will cause a pulse on GPIO6. +```python +import monitor +trig1 = monitor.trigger(1) # Create a trigger on ident 1 + +async def test(): + while True: + await asyncio.sleep_ms(100) + trig1() # Pulse appears now +``` +In `uasyncio` programs a decorator is inserted prior to a coroutine definition. +This causes a Pico pin to go high for the duration every time that coro runs. +Other mechanisms are provided, with special support for measuring cpu hogging. + +The Pico can output a trigger pulse on GPIO28 which may be used to trigger a +scope or logic analyser. This can be configured to occur when excessive latency +arises or when a segment of code runs unusually slowly. This enables the cause +of the problem to be identified. + +## 1.2 Pre-requisites + +The DUT and the Pico must run firmware V1.17 or later. + +## 1.3 Installation + +The file `monitor.py` must be copied to the DUT filesystem. `monitor_pico.py` +is copied to the Pico. + +## 1.4 UART connection + +Wiring: + +| DUT | GPIO | Pin | +|:---:|:----:|:---:| +| Gnd | Gnd | 3 | +| txd | 1 | 2 | -4th Oct 2021 Please regard this as "all new". Many functions have been renamed, -error checking has been improved and code made more efficient. +The DUT is configured to use a UART by passing an initialised UART with 1MHz +baudrate to `monitor.set_device`: -## 1.1 Pre-requisites +```python +from machine import UART +import monitor +monitor.set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz. +``` +The Pico `run()` command assumes a UART by default. + +## 1.5 SPI connection + +Wiring: + +| DUT | GPIO | Pin | +|:-----:|:----:|:---:| +| Gnd | Gnd | 3 | +| mosi | 0 | 1 | +| sck | 1 | 2 | +| cs | 2 | 4 | + +The DUT is configured to use SPI by passing an initialised SPI instance and a +`cs/` Pin instance to `set_device`: +```python +from machine import Pin, SPI +import monitor +monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X6', Pin.OUT)) # Device under test SPI +``` +The SPI instance must have default args; the one exception being baudrate which +may be any value. I have tested up to 30MHz but there is no benefit in running +above 1MHz. Hard or soft SPI may be used. It should be possible to share the +bus with other devices, although I haven't tested this. -The device being monitored must run firmware V1.17 or later. The `uasyncio` -version should be V3 (included in the firmware). The file `monitor.py` should -be copied to the target, and `monitor_pico` to the Pico. +The Pico should be started with +```python +monitor_pico.run(device="spi") +``` -## 1.2 Quick start guide +## 1.6 Quick start -For UART based monitoring, ensure that the host and Pico `gnd` pins are linked. -Connect the host's `txd` to the Pico pin 2 (UART(0) `rxd`). On the Pico issue: +This example assumes a UART connection. On the Pico issue: ```python from monitor_pico import run run() ``` -Adapt the following to match the UART to be used on the host and run it. +Adapt the following to match the UART to be used on the DUT and run it. ```python import uasyncio as asyncio from machine import UART # Using a UART for monitoring @@ -80,36 +159,43 @@ A square wave of period 200ms should be observed on Pico GPIO 4 (pin 6). Example script `quick_test.py` provides a usage example. It may be adapted to use a UART or SPI interface: see commented-out code. -### 1.2.1 Interface selection set_device() +# 2. Monitoring -An application to be monitored needs setup code to initialise the interface. -This comprises a call to `monitor.set_device` with an initialised UART or SPI -device. The Pico must be set up to match the interface chosen on the host: see -[section 4](./README.md#4-the-pico-code). - -In the case of a UART an initialised UART with 1MHz baudrate is passed: +An application to be monitored should first define the interface: ```python -from machine import UART +from machine import UART # Using a UART for monitoring import monitor monitor.set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz. ``` -In the case of SPI initialised SPI and cs/ Pin instances are passed: +or ```python from machine import Pin, SPI import monitor -monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X6', Pin.OUT)) # Device under test SPI +# Pass a configured SPI interface and a cs/ Pin instance. +monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) ``` -The SPI instance must have default args; the one exception being baudrate which -may be any value. I have tested up to 30MHz but there is no benefit in running -above 1MHz. Hard or soft SPI may be used. It should be possible to share the -bus with other devices, although I haven't tested this. +The pin used for `cs/` is arbitrary. -### 1.2.2 Monitoring - -On startup, after defining the interface, an application should issue: +Each time the application runs it should issue: ```python -monitor.init() +def main(): + monitor.init() + # rest of application code ``` +This ensures that the Pico code assumes a known state, even if a prior run +crashed, was interrupted or failed. + +## 2.1 Validation of idents + +Re-using idents would lead to confusing behaviour. If an ident is out of range +or is assigned to more than one coroutine an error message is printed and +execution terminates. See [section 7.3](./README.md#73-validation) for a +special case where validation must be defeated. + +# 3. Monitoring uasyncio code + +## 3.1 Monitoring coroutines + Coroutines to be monitored are prefixed with the `@monitor.asyn` decorator: ```python @monitor.asyn(2, 3) @@ -119,10 +205,10 @@ async def my_coro(): The decorator positional args are as follows: 1. `n` A unique `ident` in range `0 <= ident <= 21` for the code being monitored. Determines the pin number on the Pico. See - [Pico Pin mapping](./README.md#3-pico-pin-mapping). + [section 5.1](./README.md#51-pico-pin-mapping). 2. `max_instances=1` Defines the maximum number of concurrent instances of the task to be independently monitored (default 1). - 3. `verbose=True` If `False` suppress the warning which is printed on the host + 3. `verbose=True` If `False` suppress the warning which is printed on the DUT if the instance count exceeds `max_instances`. Whenever the coroutine runs, a pin on the Pico will go high, and when the code @@ -135,24 +221,26 @@ In the example above, when `my_coro` starts, the pin defined by `ident==2` running, a second instance of `my_coro` is launched, the next pin (GPIO 6) will go high. Pins will go low when the relevant instance terminates, is cancelled, or times out. If more instances are started than were specified to the -decorator, a warning will be printed on the host. All excess instances will be +decorator, a warning will be printed on the DUT. All excess instances will be associated with the final pin (`pins[ident + max_instances - 1]`) which will only go low when all instances associated with that pin have terminated. Consequently if `max_instances=1` and multiple instances are launched, a -warning will appear on the host; the pin will go high when the first instance +warning will appear on the DUT; the pin will go high when the first instance starts and will not go low until all have ended. The purpose of the warning is because the existence of multiple instances may be unexpected behaviour in the application under test. -## 1.3 Detecting CPU hogging +## 3.2 Detecting CPU hogging A common cause of problems in asynchronous code is the case where a task blocks for a period, hogging the CPU, stalling the scheduler and preventing other -tasks from running. Determining the task responsible can be difficult. +tasks from running. Determining the task responsible can be difficult, +especially as excessive latency may only occur when several greedy tasks are +scheduled in succession. -The pin state only indicates that the task is running. A pin state of 1 does -not imply CPU hogging. Thus +The Pico pin state only indicates that the task is running. A high pin does not +imply CPU hogging. Thus ```python @monitor.asyn(3) async def long_time(): @@ -162,9 +250,9 @@ will cause the pin to go high for 30s, even though the task is consuming no resources for that period. To provide a clue about CPU hogging, a `hog_detect` coroutine is provided. This -has `ident=0` and, if used, is monitored on GPIO 3. It loops, yielding to the +has `ident=0` and, if used, is monitored on GPIO3. It loops, yielding to the scheduler. It will therefore be scheduled in round-robin fashion at speed. If -long gaps appear in the pulses on GPIO 3, other tasks are hogging the CPU. +long gaps appear in the pulses on GPIO3, other tasks are hogging the CPU. Usage of this is optional. To use, issue ```python import uasyncio as asyncio @@ -177,21 +265,16 @@ To aid in detecting the gaps in execution, the Pico code implements a timer. This is retriggered by activity on `ident=0`. If it times out, a brief high going pulse is produced on GPIO 28, along with the console message "Hog". The pulse can be used to trigger a scope or logic analyser. The duration of the -timer may be adjusted. Other modes of hog detection are also supported. See +timer may be adjusted. Other modes of hog detection are also supported, notably +producing a trigger pulse only when the prior maximum was exceeded. See [section 4](./README.md~4-the-pico-code). -## 1.4 Validation of idents - -Re-using idents would lead to confusing behaviour. If an ident is out of range -or is assigned to more than one coroutine an error message is printed and -execution terminates. See [section 7](./README.md#7-validation) for a special -case where validation must be defeated. +# 4. Monitoring arbitrary code -# 2. Monitoring synchronous code +The following features may be used to characterise synchronous or asynchronous +applications by causing Pico pin changes at specific points in code execution. -In the context of an asynchronous application there may be a need to view the -timing of synchronous code, or simply to create a trigger pulse at one or more -known points in the code. The following are provided: +The following are provided: * A `sync` decorator for synchronous functions or methods: like `async` it monitors every call to the function. * A `mon_call` context manager enables function monitoring to be restricted to @@ -199,7 +282,7 @@ known points in the code. The following are provided: * A `trigger` function which issues a brief pulse on the Pico or can set and clear the pin on demand. -## 2.1 The sync decorator +## 4.1 The sync decorator This works as per the `@async` decorator, but with no `max_instances` arg. The following example will activate GPIO 26 (associated with ident 20) for the @@ -210,7 +293,7 @@ def sync_func(): pass ``` -## 2.2 The mon_call context manager +## 4.2 The mon_call context manager This may be used to monitor a function only when called from specific points in the code. Validation of idents is looser here because a context manager is @@ -229,7 +312,7 @@ with monitor.mon_call(22): It is advisable not to use the context manager with a function having the `mon_func` decorator. The behaviour of pins and reports are confusing. -## 2.3 The trigger timing marker +## 4.3 The trigger timing marker The `trigger` closure is intended for timing blocks of code. A closure instance is created by passing the ident. If the instance is run with no args a brief @@ -249,12 +332,33 @@ def bar(): # code omitted trig(False) # set pin low ``` +## 4.4 Timing of code segments + +It can be useful to time the execution of a specific block of code especially +if the time varies. It is possible to cause a message to be printed and a +trigger pulse to be generated whenever the execution time exceeds the prior +maximum. The scope or logic analyser may be triggered by this pulse allowing +the state of other parts of the system to be checked. + +This is done by re-purposing ident 0 as follows: +```python +trig = monitor.trigger(0) +def foo(): + # code omitted + trig(True) # Start of code block + # code omitted + trig(False) +``` +See [section 5.5](./README.md#55-timing-of-code-segments) for the Pico usage +and demo `syn_time.py`. + +# 5. Pico -# 3. Pico Pin mapping +# 5.1 Pico pin mapping The Pico GPIO numbers used by idents start at 3 and have a gap where the Pico uses GPIO's for particular purposes. This is the mapping between `ident` GPIO -no. and Pico PCB pin. Pins for the timer and the UART/SPI link are also +no. and Pico PCB pin. Pins for the trigger and the UART/SPI link are also identified: | ident | GPIO | pin | @@ -284,29 +388,28 @@ identified: | 19 | 22 | 29 | | 20 | 26 | 31 | | 21 | 27 | 32 | -| timer | 28 | 34 | - -For a UART interface the host's UART `txd` pin should be connected to Pico GPIO -1 (pin 2). - -For SPI the host's `mosi` goes to GPIO 0 (pin 1), and `sck` to GPIO 1 (pin 2). -The host's CS Pin is connected to GPIO 2 (pin 4). - -There must be a link between `Gnd` pins on the host and Pico. +| trigger | 28 | 34 | -# 4. The Pico code +## 5.2 The Pico code Monitoring via the UART with default behaviour is started as follows: ```python from monitor_pico import run run() ``` -By default the Pico does not produce console output when tasks start and end. -The timer has a period of 100ms - pin 28 will pulse if ident 0 is inactive for -over 100ms. These behaviours can be modified by the following `run` args: - 1. `period=100` Define the hog_detect timer period in ms. +By default the Pico retriggers a timer every time ident 0 becomes active. If +the timer times out, a pulse appears on GPIO28 which may be used to trigger a +scope or logic analyser. This is intended for use with the `hog_detect` coro, +with the pulse occurring when excessive latency is encountered. + +## 5.3 The Pico run function + +Arguments to `run()` can select the interface and modify the default behaviour. + 1. `period=100` Define the hog_detect timer period in ms. A 2-tuple may also + be passed for specialised reporting, see below. 2. `verbose=()` A list or tuple of `ident` values which should produce console - output. + output. A passed ident will produce console output each time that task starts + or ends. 3. `device="uart"` Set to `"spi"` for an SPI interface. 4. `vb=True` By default the Pico issues console messages reporting on initial communication status, repeated each time the application under test restarts. @@ -326,7 +429,7 @@ maximum, "Max hog Nms" is also issued. This means that if the application under test terminates, throws an exception or crashes, "Timeout" will be issued. -## 4.1 Advanced hog detection +## 5.4 Advanced hog detection The detection of rare instances of high latency is a key requirement and other modes are available. There are two aims: providing information to users lacking @@ -356,14 +459,25 @@ Running the following produce instructive console output: from monitor_pico import run, MAX run((1, MAX)) ``` +## 5.5 Timing of code segments + +This may be done by issuing: +```python +from monitor_pico import run, WIDTH +run((20, WIDTH)) # Ignore widths < 20ms. +``` +Assuming that ident 0 is used as described in +[section 4.4](./README.md#44-timing-of-code-segments) a trigger pulse on GPIO28 +will occur each time the time taken exceeds both 20ms and its prior maximum. A +message with the actual width is also printed whenever this occurs. -# 5. Test and demo scripts +# 6. Test and demo scripts `quick_test.py` Primarily tests deliberate CPU hogging. Discussed in section 1. `full_test.py` Tests task timeout and cancellation, also the handling of multiple task instances. If the Pico is run with `run((1, MAX))` it reveals -the maximum time the host hogs the CPU. On a Pyboard D I measured 5ms. +the maximum time the DUT hogs the CPU. On a Pyboard D I measured 5ms. The sequence here is a trigger is issued on ident 4. The task on ident 1 is started, but times out after 100ms. 100ms later, five instances of the task on @@ -375,7 +489,7 @@ only goes low when the last of these three instances is cancelled. ![Image](./tests/full_test.jpg) `latency.py` Measures latency between the start of a monitored task and the -Pico pin going high. In the image below the sequence starts when the host +Pico pin going high. In the image below the sequence starts when the DUT pulses a pin (ident 6). The Pico pin monitoring the task then goes high (ident 1 after ~20μs). Then the trigger on ident 2 occurs 112μs after the pin pulse. @@ -391,7 +505,12 @@ in `hog_detect` show the periods of deliberate CPU hogging. ![Image](./tests/syn_test.jpg) -# 6. Performance and design notes +`syn_time.py` Demonstrates timing of a specific code segment with a trigger +pulse being generated every time the period exceeds its prior maximum. + +# 7. Internals + +## 7.1 Performance and design notes Using a UART the latency between a monitored coroutine starting to run and the Pico pin going high is about 23μs. With SPI I measured -12μs. This isn't as @@ -415,19 +534,7 @@ fast in the context of uasyncio). It also ensures that tasks like `hog_detect`, which can be scheduled at a high rate, can't overflow the UART buffer. The 1Mbps rate seems widely supported. -## 6.1 ESP8266 note - -tl;dr ESP8266 applications can be monitored using the transmit-only UART 1. - -I was expecting problems: on boot the ESP8266 transmits data on both UARTs at -75Kbaud. A bit at this baudrate corresponds to 13.3 bits at 1Mbaud. A receiving -UART will see a transmitted 1 as 13 consecutive 1 bits. Lacking a start bit, it -will ignore them. An incoming 0 will be interpreted as a framing error because -of the absence of a stop bit. In practice the Pico UART returns `b'\x00'` when -this occurs, which `monitor.py` ignores. When monitored the ESP8266 behaves -identically to other platforms and can be rebooted at will. - -## 6.2 How it works +## 7.2 How it works This is for anyone wanting to modify the code. Each ident is associated with two bytes, `0x40 + ident` and `0x60 + ident`. These are upper and lower case @@ -446,13 +553,13 @@ When a character arrives, the `ident` value is recovered. If it is uppercase the pin goes high and the instance count is incremented. If it is lowercase the instance count is decremented: if it becomes 0 the pin goes low. -The `init` function on the host sends `b"z"` to the Pico. This sets each pin +The `init` function on the DUT sends `b"z"` to the Pico. This sets each pin in `pins` low and clears its instance counter (the program under test may have previously failed, leaving instance counters non-zero). The Pico also clears variables used to measure hogging. In the case of SPI communication, before sending the `b"z"`, a 0 character is sent with `cs/` high. The Pico implements a basic SPI slave using the PIO. This may have been left in an invalid state by -a crashing host. The slave is designed to reset to a "ready" state if it +a crashing DUT. The slave is designed to reset to a "ready" state if it receives any character with `cs/` high. The ident `@` (0x40) is assumed to be used by the `hog_detect()` function. When @@ -469,7 +576,7 @@ In the following, `thresh` is the time passed to `run()` in `period[0]`. This project was inspired by [this GitHub thread](https://github.com/micropython/micropython/issues/7456). -# 7. Validation +## 7.3 Validation The `monitor` module attempts to protect against inadvertent multiple use of an `ident`. There are use patterns which are incompatible with this, notably where @@ -480,10 +587,25 @@ import monitor monitor.validation(False) ``` +## 7.4 ESP8266 note + +ESP8266 applications can be monitored using the transmit-only UART 1. + +I was expecting problems: on boot the ESP8266 transmits data on both UARTs at +75Kbaud. In practice `monitor_pico.py` ignores this data for the following +reasons. + +A bit at 75Kbaud corresponds to 13.3 bits at 1Mbaud. The receiving UART will +see a transmitted 1 as 13 consecutive 1 bits. In the absence of a start bit, it +will ignore the idle level. An incoming 0 will be interpreted as a framing +error because of the absence of a stop bit. In practice the Pico UART returns +`b'\x00'` when this occurs; `monitor.py` ignores such characters. A monitored +ESP8266 behaves identically to other platforms and can be rebooted at will. + # 8. A hardware implementation -The device under test is on the right, linked to the Pico board by means of a -UART. +I expect to use this a great deal, so I designed a PCB. In the image below the +device under test is on the right, linked to the Pico board by means of a UART. ![Image](./monitor_hw.JPG) diff --git a/v3/as_demos/monitor/monitor.py b/v3/as_demos/monitor/monitor.py index ef13a2e..17f443b 100644 --- a/v3/as_demos/monitor/monitor.py +++ b/v3/as_demos/monitor/monitor.py @@ -44,11 +44,13 @@ def clear_sm(): # Set Pico SM to its initial state else: _quit("set_device: invalid args.") + # Justification for validation even when decorating a method # /mnt/qnap2/data/Projects/Python/AssortedTechniques/decorators _available = set(range(0, 22)) # Valid idents are 0..21 _do_validate = True + def _validate(ident, num=1): if _do_validate: if ident >= 0 and ident + num < 22: @@ -60,10 +62,12 @@ def _validate(ident, num=1): else: _quit("error - ident {:02d} out of range.".format(ident)) + def validation(do=True): global _do_validate _do_validate = do + # asynchronous monitor def asyn(n, max_instances=1, verbose=True): def decorator(coro): @@ -93,12 +97,14 @@ async def wrapped_coro(*args, **kwargs): return decorator + # If SPI, clears the state machine in case prior test resulted in the DUT # crashing. It does this by sending a byte with CS\ False (high). def init(): _ifrst() # Reset interface. Does nothing if UART. _write(b"z") # Clear Pico's instance counters etc. + # Optionally run this to show up periods of blocking behaviour async def hog_detect(s=(b"\x40", b"\x60")): while True: @@ -106,6 +112,7 @@ async def hog_detect(s=(b"\x40", b"\x60")): _write(v) await asyncio.sleep_ms(0) + # Monitor a synchronous function definition def sync(n): def decorator(func): @@ -123,6 +130,7 @@ def wrapped_func(*args, **kwargs): return decorator + # Monitor a function call class mon_call: _cm_idents = set() # Idents used by this CM @@ -142,12 +150,14 @@ def __exit__(self, type, value, traceback): _write(self.vend) return False # Don't silence exceptions + # Either cause pico ident n to produce a brief (~80μs) pulse or turn it # on or off on demand. def trigger(n): _validate(n) on = int.to_bytes(0x40 + n, 1, "big") off = int.to_bytes(0x60 + n, 1, "big") + def wrapped(state=None): if state is None: _write(on) @@ -155,4 +165,5 @@ def wrapped(state=None): _write(off) else: _write(on if state else off) + return wrapped diff --git a/v3/as_demos/monitor/monitor_pico.py b/v3/as_demos/monitor/monitor_pico.py index eba169e..bfce0a0 100644 --- a/v3/as_demos/monitor/monitor_pico.py +++ b/v3/as_demos/monitor/monitor_pico.py @@ -100,7 +100,7 @@ def run(period=100, verbose=(), device="uart", vb=True): mode = SOON else: t_ms, mode = period - if mode not in (SOON, LATE, MAX): + if mode not in (SOON, LATE, MAX, WIDTH): raise ValueError("Invalid mode.") for x in verbose: pins[x][2] = True @@ -141,7 +141,7 @@ def read(): h_start = tarr elif x == 0x60 and h_start != -1: # Trailing edge dt = ticks_diff(tarr, h_start) - if dt > h_max: + if dt > t_ms and dt > h_max: h_max = dt print(f"Max width {dt}ms") pin_t(1) From dbb46bcc7ed838c8a528b98585f129d108886eb7 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 19 Oct 2021 13:59:41 +0100 Subject: [PATCH 106/305] monitor: Fix bug. README imrovements. --- v3/as_demos/monitor/README.md | 88 ++++++++++++------------- v3/as_demos/monitor/monitor.py | 58 ++++++++-------- v3/as_demos/monitor/tests/looping.py | 58 ++++++++++++++++ v3/as_demos/monitor/tests/syn_test.py | 4 +- v3/as_demos/monitor/tests/syn_time.jpg | Bin 0 -> 74885 bytes v3/as_demos/monitor/tests/syn_time.py | 41 ++++++++++++ 6 files changed, 173 insertions(+), 76 deletions(-) create mode 100644 v3/as_demos/monitor/tests/looping.py create mode 100644 v3/as_demos/monitor/tests/syn_time.jpg create mode 100644 v3/as_demos/monitor/tests/syn_time.py diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md index 3519801..78cb29c 100644 --- a/v3/as_demos/monitor/README.md +++ b/v3/as_demos/monitor/README.md @@ -18,13 +18,9 @@ when the blocking period exceeds a threshold. The threshold can be a fixed time or the current maximum blocking period. A logic analyser enables the state at the time of the transient event to be examined. -The following image shows the `quick_test.py` code being monitored at the point -when a task hogs the CPU. The top line 00 shows the "hog detect" trigger. Line -01 shows the fast running `hog_detect` task which cannot run at the time of the -trigger because another task is hogging the CPU. Lines 02 and 04 show the `foo` -and `bar` tasks. Line 03 shows the `hog` task and line 05 is a trigger issued -by `hog()` when it starts monopolising the CPU. The Pico issues the "hog -detect" trigger 100ms after hogging starts. +This image shows the detection of CPU hogging. A trigger pulse is generated +100ms after hogging caused the scheduler to be unable to schedule tasks. It is +discussed in more detail in [section 6](./README.md#6-test-and-demo-scripts). ![Image](./monitor.jpg) @@ -37,8 +33,8 @@ to demonstrate that this never exceeded 5ms. ## 1.1 Concepts Communication with the Pico may be by UART or SPI, and is uni-directional from -DUT to Pico. If a UART is used only one GPIO pin is needed. SPI requires three -- `mosi`, `sck` and `cs/`. +DUT to Pico. If a UART is used only one GPIO pin is needed. SPI requires three, +namely `mosi`, `sck` and `cs/`. The Pico runs the following: ```python @@ -189,8 +185,7 @@ crashed, was interrupted or failed. Re-using idents would lead to confusing behaviour. If an ident is out of range or is assigned to more than one coroutine an error message is printed and -execution terminates. See [section 7.3](./README.md#73-validation) for a -special case where validation must be defeated. +execution terminates. # 3. Monitoring uasyncio code @@ -210,6 +205,9 @@ The decorator positional args are as follows: task to be independently monitored (default 1). 3. `verbose=True` If `False` suppress the warning which is printed on the DUT if the instance count exceeds `max_instances`. + 4. `looping=False` Set `True` if the decorator is called repeatedly e.g. + decorating a nested function or method. The `True` value ensures validation of + the ident occurs once only when the decorator first runs. Whenever the coroutine runs, a pin on the Pico will go high, and when the code terminates it will go low. This enables the behaviour of the system to be @@ -261,13 +259,13 @@ import monitor asyncio.create_task(monitor.hog_detect()) # code omitted ``` -To aid in detecting the gaps in execution, the Pico code implements a timer. -This is retriggered by activity on `ident=0`. If it times out, a brief high -going pulse is produced on GPIO 28, along with the console message "Hog". The -pulse can be used to trigger a scope or logic analyser. The duration of the -timer may be adjusted. Other modes of hog detection are also supported, notably -producing a trigger pulse only when the prior maximum was exceeded. See -[section 4](./README.md~4-the-pico-code). +To aid in detecting the gaps in execution, in its default mode the Pico code +implements a timer. This is retriggered by activity on `ident=0`. If it times +out, a brief high going pulse is produced on GPIO 28, along with the console +message "Hog". The pulse can be used to trigger a scope or logic analyser. The +duration of the timer may be adjusted. Other modes of hog detection are also +supported, notably producing a trigger pulse only when the prior maximum was +exceeded. See [section 5](./README.md#5-Pico). # 4. Monitoring arbitrary code @@ -292,13 +290,17 @@ duration of every call to `sync_func()`: def sync_func(): pass ``` +Decorator args: + 1. `ident` + 2. `looping=False` Set `True` if the decorator is called repeatedly e.g. in a + nested function or method. The `True` value ensures validation of the ident + occurs once only when the decorator first runs. ## 4.2 The mon_call context manager This may be used to monitor a function only when called from specific points in -the code. Validation of idents is looser here because a context manager is -often used in a looping construct: it seems impractical to distinguish this -case from that where two context managers are instantiated with the same ID. +the code. Since context managers may be used in a looping construct the ident +is only checked for conflicts when the CM is first instantiated. Usage: ```python @@ -319,8 +321,7 @@ is created by passing the ident. If the instance is run with no args a brief (~80μs) pulse will occur on the Pico pin. If `True` is passed, the pin will go high until `False` is passed. -The closure should be instantiated once only. If instantiated in a loop the -ident will fail the check on re-use. +The closure should be instantiated once only in the outermost scope. ```python trig = monitor.trigger(10) # Associate trig with ident 10. @@ -335,10 +336,10 @@ def bar(): ## 4.4 Timing of code segments It can be useful to time the execution of a specific block of code especially -if the time varies. It is possible to cause a message to be printed and a -trigger pulse to be generated whenever the execution time exceeds the prior -maximum. The scope or logic analyser may be triggered by this pulse allowing -the state of other parts of the system to be checked. +if the duration varies in real time. It is possible to cause a message to be +printed and a trigger pulse to be generated whenever the execution time exceeds +the prior maximum. A scope or logic analyser may be triggered by this pulse +allowing the state of other components of the system to be checked. This is done by re-purposing ident 0 as follows: ```python @@ -467,13 +468,21 @@ from monitor_pico import run, WIDTH run((20, WIDTH)) # Ignore widths < 20ms. ``` Assuming that ident 0 is used as described in -[section 4.4](./README.md#44-timing-of-code-segments) a trigger pulse on GPIO28 +[section 5.5](./README.md#55-timing-of-code-segments) a trigger pulse on GPIO28 will occur each time the time taken exceeds both 20ms and its prior maximum. A message with the actual width is also printed whenever this occurs. # 6. Test and demo scripts -`quick_test.py` Primarily tests deliberate CPU hogging. Discussed in section 1. +The following image shows the `quick_test.py` code being monitored at the point +when a task hogs the CPU. The top line 00 shows the "hog detect" trigger. Line +01 shows the fast running `hog_detect` task which cannot run at the time of the +trigger because another task is hogging the CPU. Lines 02 and 04 show the `foo` +and `bar` tasks. Line 03 shows the `hog` task and line 05 is a trigger issued +by `hog()` when it starts monopolising the CPU. The Pico issues the "hog +detect" trigger 100ms after hogging starts. + +![Image](./monitor.jpg) `full_test.py` Tests task timeout and cancellation, also the handling of multiple task instances. If the Pico is run with `run((1, MAX))` it reveals @@ -508,6 +517,8 @@ in `hog_detect` show the periods of deliberate CPU hogging. `syn_time.py` Demonstrates timing of a specific code segment with a trigger pulse being generated every time the period exceeds its prior maximum. +![Image](./tests/syn_time.jpg) + # 7. Internals ## 7.1 Performance and design notes @@ -573,21 +584,7 @@ In the following, `thresh` is the time passed to `run()` in `period[0]`. * `MAX` Trigger occurs if period exceeds `thresh` and also exceeds the prior maximum. -This project was inspired by -[this GitHub thread](https://github.com/micropython/micropython/issues/7456). - -## 7.3 Validation - -The `monitor` module attempts to protect against inadvertent multiple use of an -`ident`. There are use patterns which are incompatible with this, notably where -a decorated function or coroutine is instantiated in a looping construct. To -cater for such cases validation can be defeated. This is done by issuing: -```python -import monitor -monitor.validation(False) -``` - -## 7.4 ESP8266 note +## 7.3 ESP8266 note ESP8266 applications can be monitored using the transmit-only UART 1. @@ -610,3 +607,6 @@ device under test is on the right, linked to the Pico board by means of a UART. ![Image](./monitor_hw.JPG) I can supply a schematic and PCB details if anyone is interested. + +This project was inspired by +[this GitHub thread](https://github.com/micropython/micropython/issues/7456). diff --git a/v3/as_demos/monitor/monitor.py b/v3/as_demos/monitor/monitor.py index 17f443b..db82578 100644 --- a/v3/as_demos/monitor/monitor.py +++ b/v3/as_demos/monitor/monitor.py @@ -45,33 +45,34 @@ def clear_sm(): # Set Pico SM to its initial state _quit("set_device: invalid args.") -# Justification for validation even when decorating a method # /mnt/qnap2/data/Projects/Python/AssortedTechniques/decorators _available = set(range(0, 22)) # Valid idents are 0..21 -_do_validate = True - - -def _validate(ident, num=1): - if _do_validate: - if ident >= 0 and ident + num < 22: - try: - for x in range(ident, ident + num): +# Looping: some idents may be repeatedly instantiated. This can occur +# if decorator is run in looping code. A CM is likely to be used in a +# loop. In these cases only validate on first use. +_loopers = set() + + +def _validate(ident, num=1, looping=False): + if ident >= 0 and ident + num < 22: + try: + for x in range(ident, ident + num): + if looping: + if x not in _loopers: + _available.remove(x) + _loopers.add(x) + else: _available.remove(x) - except KeyError: - _quit("error - ident {:02d} already allocated.".format(x)) - else: - _quit("error - ident {:02d} out of range.".format(ident)) - - -def validation(do=True): - global _do_validate - _do_validate = do + except KeyError: + _quit("error - ident {:02d} already allocated.".format(x)) + else: + _quit("error - ident {:02d} out of range.".format(ident)) # asynchronous monitor -def asyn(n, max_instances=1, verbose=True): +def asyn(n, max_instances=1, verbose=True, looping=False): def decorator(coro): - _validate(n, max_instances) + _validate(n, max_instances, looping) instance = 0 async def wrapped_coro(*args, **kwargs): @@ -114,11 +115,11 @@ async def hog_detect(s=(b"\x40", b"\x60")): # Monitor a synchronous function definition -def sync(n): +def sync(ident, looping=False): def decorator(func): - _validate(n) - vstart = int.to_bytes(0x40 + n, 1, "big") - vend = int.to_bytes(0x60 + n, 1, "big") + _validate(ident, 1, looping) + vstart = int.to_bytes(0x40 + ident, 1, "big") + vend = int.to_bytes(0x60 + ident, 1, "big") def wrapped_func(*args, **kwargs): _write(vstart) @@ -133,12 +134,9 @@ def wrapped_func(*args, **kwargs): # Monitor a function call class mon_call: - _cm_idents = set() # Idents used by this CM - def __init__(self, n): - if n not in self._cm_idents: # ID can't clash with other objects - _validate(n) # but could have two CM's with same ident - self._cm_idents.add(n) + # looping: a CM may be instantiated many times + _validate(n, 1, True) self.vstart = int.to_bytes(0x40 + n, 1, "big") self.vend = int.to_bytes(0x60 + n, 1, "big") @@ -152,7 +150,7 @@ def __exit__(self, type, value, traceback): # Either cause pico ident n to produce a brief (~80μs) pulse or turn it -# on or off on demand. +# on or off on demand. No looping: docs suggest instantiating at start. def trigger(n): _validate(n) on = int.to_bytes(0x40 + n, 1, "big") diff --git a/v3/as_demos/monitor/tests/looping.py b/v3/as_demos/monitor/tests/looping.py new file mode 100644 index 0000000..dd33459 --- /dev/null +++ b/v3/as_demos/monitor/tests/looping.py @@ -0,0 +1,58 @@ +# syn_test.py +# Tests the monitoring synchronous code and of an async method. + +# Copyright (c) 2021 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +import time +from machine import Pin, UART, SPI +import monitor + +# Define interface to use +monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz +# monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz +trig = monitor.trigger(5) + + +class Foo: + def __init__(self): + pass + + @monitor.asyn(1, 2) # ident 1/2 high + async def pause(self): + while True: + trig() + self.wait1() # ident 3 10ms pulse + await asyncio.sleep_ms(100) + with monitor.mon_call(4): # ident 4 10ms pulse + self.wait2() + await asyncio.sleep_ms(100) + # ident 1/2 low + + async def bar(self): + @monitor.asyn(3, looping = True) + async def wait1(): + await asyncio.sleep_ms(100) + @monitor.sync(4, True) + def wait2(): + time.sleep_ms(10) + trig() + await wait1() + trig() + wait2() + + +async def main(): + monitor.init() + asyncio.create_task(monitor.hog_detect()) # Make 10ms waitx gaps visible + foo = Foo() + while True: + await foo.bar() + await asyncio.sleep_ms(100) + await foo.pause() + +try: + asyncio.run(main()) +finally: + asyncio.new_event_loop() diff --git a/v3/as_demos/monitor/tests/syn_test.py b/v3/as_demos/monitor/tests/syn_test.py index c9f5ddd..19e4d72 100644 --- a/v3/as_demos/monitor/tests/syn_test.py +++ b/v3/as_demos/monitor/tests/syn_test.py @@ -12,6 +12,7 @@ # Define interface to use monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz # monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz +trig = monitor.trigger(5) class Foo: @@ -27,7 +28,7 @@ async def pause(self): await asyncio.sleep_ms(100) # ident 1/2 low - @monitor.sync(3) # Decorator so ident not reserved + @monitor.sync(3) def wait1(self): time.sleep_ms(10) @@ -39,7 +40,6 @@ async def main(): asyncio.create_task(monitor.hog_detect()) # Make 10ms waitx gaps visible foo1 = Foo() foo2 = Foo() - trig = monitor.trigger(5) while True: trig() # Mark start with pulse on ident 5 # Create two instances of .pause separated by 50ms diff --git a/v3/as_demos/monitor/tests/syn_time.jpg b/v3/as_demos/monitor/tests/syn_time.jpg new file mode 100644 index 0000000000000000000000000000000000000000..88c1805deb868b804e4877b0fd38b0fe9466cdd4 GIT binary patch literal 74885 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 1MHz + +trig0 = monitor.trigger(0) +twait = 20 + +async def test(): + while True: + await asyncio.sleep_ms(100) + trig0(True) + await asyncio.sleep_ms(twait) + trig0(False) + +async def lengthen(): + global twait + while twait < 200: + twait += 1 + await asyncio.sleep(1) + +async def main(): + monitor.init() + asyncio.create_task(lengthen()) + await test() + +try: + asyncio.run(main()) +finally: + asyncio.new_event_loop() From 9e7cf80910376c91bb0e8d8de56d864fed153215 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 21 Oct 2021 18:05:24 +0100 Subject: [PATCH 107/305] monitor: Now moved to new location. --- v3/README.md | 8 +- v3/as_demos/monitor/README.md | 612 ------------------------ v3/as_demos/monitor/monitor.jpg | Bin 90535 -> 0 bytes v3/as_demos/monitor/monitor.py | 167 ------- v3/as_demos/monitor/monitor_gc.jpg | Bin 68891 -> 0 bytes v3/as_demos/monitor/monitor_hw.JPG | Bin 259855 -> 0 bytes v3/as_demos/monitor/monitor_pico.py | 177 ------- v3/as_demos/monitor/tests/full_test.jpg | Bin 69423 -> 0 bytes v3/as_demos/monitor/tests/full_test.py | 47 -- v3/as_demos/monitor/tests/latency.jpg | Bin 76931 -> 0 bytes v3/as_demos/monitor/tests/latency.py | 36 -- v3/as_demos/monitor/tests/looping.py | 58 --- v3/as_demos/monitor/tests/quick_test.py | 44 -- v3/as_demos/monitor/tests/syn_test.jpg | Bin 78274 -> 0 bytes v3/as_demos/monitor/tests/syn_test.py | 54 --- v3/as_demos/monitor/tests/syn_time.jpg | Bin 74885 -> 0 bytes v3/as_demos/monitor/tests/syn_time.py | 41 -- 17 files changed, 4 insertions(+), 1240 deletions(-) delete mode 100644 v3/as_demos/monitor/README.md delete mode 100644 v3/as_demos/monitor/monitor.jpg delete mode 100644 v3/as_demos/monitor/monitor.py delete mode 100644 v3/as_demos/monitor/monitor_gc.jpg delete mode 100644 v3/as_demos/monitor/monitor_hw.JPG delete mode 100644 v3/as_demos/monitor/monitor_pico.py delete mode 100644 v3/as_demos/monitor/tests/full_test.jpg delete mode 100644 v3/as_demos/monitor/tests/full_test.py delete mode 100644 v3/as_demos/monitor/tests/latency.jpg delete mode 100644 v3/as_demos/monitor/tests/latency.py delete mode 100644 v3/as_demos/monitor/tests/looping.py delete mode 100644 v3/as_demos/monitor/tests/quick_test.py delete mode 100644 v3/as_demos/monitor/tests/syn_test.jpg delete mode 100644 v3/as_demos/monitor/tests/syn_test.py delete mode 100644 v3/as_demos/monitor/tests/syn_time.jpg delete mode 100644 v3/as_demos/monitor/tests/syn_time.py diff --git a/v3/README.md b/v3/README.md index 2b0949f..27b73f5 100644 --- a/v3/README.md +++ b/v3/README.md @@ -48,11 +48,11 @@ useful in their own right: ### A monitor -This [monitor](./as_demos/monitor/README.md) enables a running `uasyncio` -application to be monitored using a Pi Pico, ideally with a scope or logic -analyser. +This [monitor](https://github.com/peterhinch/micropython-monitor) enables a +running `uasyncio` application to be monitored using a Pi Pico, ideally with a +scope or logic analyser. -![Image](./as_demos/monitor/tests/syn_test.jpg) +![Image](https://github.com/peterhinch/micropython-monitor/raw/master/images/monitor.jpg) # 2. V3 Overview diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md deleted file mode 100644 index 78cb29c..0000000 --- a/v3/as_demos/monitor/README.md +++ /dev/null @@ -1,612 +0,0 @@ -# 1. A monitor for realtime MicroPython code - -This library provides a means of examining the behaviour of a running system. -It was initially designed to characterise `uasyncio` programs but may also find -use to study any code whose behaviour may change dynamically such as threaded -code or applications using interrupts. - -The device under test (DUT) is linked to a Raspberry Pico. The latter displays -the behaviour of the DUT by pin changes and optional print statements. A logic -analyser or scope provides a view of the realtime behaviour of the code. -Valuable information can also be gleaned at the Pico command line. - -Where an application runs multiple concurrent tasks it can be difficult to -identify a task which is hogging CPU time. Long blocking periods can also occur -when several tasks each block for a period. If, on occasion, these are -scheduled in succession, the times will add. The monitor issues a trigger pulse -when the blocking period exceeds a threshold. The threshold can be a fixed time -or the current maximum blocking period. A logic analyser enables the state at -the time of the transient event to be examined. - -This image shows the detection of CPU hogging. A trigger pulse is generated -100ms after hogging caused the scheduler to be unable to schedule tasks. It is -discussed in more detail in [section 6](./README.md#6-test-and-demo-scripts). - -![Image](./monitor.jpg) - -The following image shows brief (<4ms) hogging while `quick_test.py` ran. The -likely cause is garbage collection on the Pyboard D DUT. The monitor was able -to demonstrate that this never exceeded 5ms. - -![Image](./monitor_gc.jpg) - -## 1.1 Concepts - -Communication with the Pico may be by UART or SPI, and is uni-directional from -DUT to Pico. If a UART is used only one GPIO pin is needed. SPI requires three, -namely `mosi`, `sck` and `cs/`. - -The Pico runs the following: -```python -from monitor_pico import run -run() # or run(device="spi") -``` -Debug lines are inserted at key points in the DUT code. These cause state -changes on Pico pins. All debug lines are associated with an `ident` which is a -number where `0 <= ident <= 21`. The `ident` value defines a Pico GPIO pin -according to the mapping in [section 5.1](./README.md#51-pico-pin-mapping). - -For example the following will cause a pulse on GPIO6. -```python -import monitor -trig1 = monitor.trigger(1) # Create a trigger on ident 1 - -async def test(): - while True: - await asyncio.sleep_ms(100) - trig1() # Pulse appears now -``` -In `uasyncio` programs a decorator is inserted prior to a coroutine definition. -This causes a Pico pin to go high for the duration every time that coro runs. -Other mechanisms are provided, with special support for measuring cpu hogging. - -The Pico can output a trigger pulse on GPIO28 which may be used to trigger a -scope or logic analyser. This can be configured to occur when excessive latency -arises or when a segment of code runs unusually slowly. This enables the cause -of the problem to be identified. - -## 1.2 Pre-requisites - -The DUT and the Pico must run firmware V1.17 or later. - -## 1.3 Installation - -The file `monitor.py` must be copied to the DUT filesystem. `monitor_pico.py` -is copied to the Pico. - -## 1.4 UART connection - -Wiring: - -| DUT | GPIO | Pin | -|:---:|:----:|:---:| -| Gnd | Gnd | 3 | -| txd | 1 | 2 | - -The DUT is configured to use a UART by passing an initialised UART with 1MHz -baudrate to `monitor.set_device`: - -```python -from machine import UART -import monitor -monitor.set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz. -``` -The Pico `run()` command assumes a UART by default. - -## 1.5 SPI connection - -Wiring: - -| DUT | GPIO | Pin | -|:-----:|:----:|:---:| -| Gnd | Gnd | 3 | -| mosi | 0 | 1 | -| sck | 1 | 2 | -| cs | 2 | 4 | - -The DUT is configured to use SPI by passing an initialised SPI instance and a -`cs/` Pin instance to `set_device`: -```python -from machine import Pin, SPI -import monitor -monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X6', Pin.OUT)) # Device under test SPI -``` -The SPI instance must have default args; the one exception being baudrate which -may be any value. I have tested up to 30MHz but there is no benefit in running -above 1MHz. Hard or soft SPI may be used. It should be possible to share the -bus with other devices, although I haven't tested this. - -The Pico should be started with -```python -monitor_pico.run(device="spi") -``` - -## 1.6 Quick start - -This example assumes a UART connection. On the Pico issue: -```python -from monitor_pico import run -run() -``` -Adapt the following to match the UART to be used on the DUT and run it. -```python -import uasyncio as asyncio -from machine import UART # Using a UART for monitoring -import monitor -monitor.set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz. - -@monitor.asyn(1) # Assign ident 1 to foo (GPIO 4) -async def foo(): - await asyncio.sleep_ms(100) - -async def main(): - monitor.init() # Initialise Pico state at the start of every run - while True: - await foo() # Pico GPIO4 will go high for duration - await asyncio.sleep_ms(100) - -try: - asyncio.run(main()) -finally: - asyncio.new_event_loop() -``` -A square wave of period 200ms should be observed on Pico GPIO 4 (pin 6). - -Example script `quick_test.py` provides a usage example. It may be adapted to -use a UART or SPI interface: see commented-out code. - -# 2. Monitoring - -An application to be monitored should first define the interface: -```python -from machine import UART # Using a UART for monitoring -import monitor -monitor.set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz. -``` -or -```python -from machine import Pin, SPI -import monitor -# Pass a configured SPI interface and a cs/ Pin instance. -monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) -``` -The pin used for `cs/` is arbitrary. - -Each time the application runs it should issue: -```python -def main(): - monitor.init() - # rest of application code -``` -This ensures that the Pico code assumes a known state, even if a prior run -crashed, was interrupted or failed. - -## 2.1 Validation of idents - -Re-using idents would lead to confusing behaviour. If an ident is out of range -or is assigned to more than one coroutine an error message is printed and -execution terminates. - -# 3. Monitoring uasyncio code - -## 3.1 Monitoring coroutines - -Coroutines to be monitored are prefixed with the `@monitor.asyn` decorator: -```python -@monitor.asyn(2, 3) -async def my_coro(): - # code -``` -The decorator positional args are as follows: - 1. `n` A unique `ident` in range `0 <= ident <= 21` for the code being - monitored. Determines the pin number on the Pico. See - [section 5.1](./README.md#51-pico-pin-mapping). - 2. `max_instances=1` Defines the maximum number of concurrent instances of the - task to be independently monitored (default 1). - 3. `verbose=True` If `False` suppress the warning which is printed on the DUT - if the instance count exceeds `max_instances`. - 4. `looping=False` Set `True` if the decorator is called repeatedly e.g. - decorating a nested function or method. The `True` value ensures validation of - the ident occurs once only when the decorator first runs. - -Whenever the coroutine runs, a pin on the Pico will go high, and when the code -terminates it will go low. This enables the behaviour of the system to be -viewed on a logic analyser or via console output on the Pico. This behavior -works whether the code terminates normally, is cancelled or has a timeout. - -In the example above, when `my_coro` starts, the pin defined by `ident==2` -(GPIO 5) will go high. When it ends, the pin will go low. If, while it is -running, a second instance of `my_coro` is launched, the next pin (GPIO 6) will -go high. Pins will go low when the relevant instance terminates, is cancelled, -or times out. If more instances are started than were specified to the -decorator, a warning will be printed on the DUT. All excess instances will be -associated with the final pin (`pins[ident + max_instances - 1]`) which will -only go low when all instances associated with that pin have terminated. - -Consequently if `max_instances=1` and multiple instances are launched, a -warning will appear on the DUT; the pin will go high when the first instance -starts and will not go low until all have ended. The purpose of the warning is -because the existence of multiple instances may be unexpected behaviour in the -application under test. - -## 3.2 Detecting CPU hogging - -A common cause of problems in asynchronous code is the case where a task blocks -for a period, hogging the CPU, stalling the scheduler and preventing other -tasks from running. Determining the task responsible can be difficult, -especially as excessive latency may only occur when several greedy tasks are -scheduled in succession. - -The Pico pin state only indicates that the task is running. A high pin does not -imply CPU hogging. Thus -```python -@monitor.asyn(3) -async def long_time(): - await asyncio.sleep(30) -``` -will cause the pin to go high for 30s, even though the task is consuming no -resources for that period. - -To provide a clue about CPU hogging, a `hog_detect` coroutine is provided. This -has `ident=0` and, if used, is monitored on GPIO3. It loops, yielding to the -scheduler. It will therefore be scheduled in round-robin fashion at speed. If -long gaps appear in the pulses on GPIO3, other tasks are hogging the CPU. -Usage of this is optional. To use, issue -```python -import uasyncio as asyncio -import monitor -# code omitted -asyncio.create_task(monitor.hog_detect()) -# code omitted -``` -To aid in detecting the gaps in execution, in its default mode the Pico code -implements a timer. This is retriggered by activity on `ident=0`. If it times -out, a brief high going pulse is produced on GPIO 28, along with the console -message "Hog". The pulse can be used to trigger a scope or logic analyser. The -duration of the timer may be adjusted. Other modes of hog detection are also -supported, notably producing a trigger pulse only when the prior maximum was -exceeded. See [section 5](./README.md#5-Pico). - -# 4. Monitoring arbitrary code - -The following features may be used to characterise synchronous or asynchronous -applications by causing Pico pin changes at specific points in code execution. - -The following are provided: - * A `sync` decorator for synchronous functions or methods: like `async` it - monitors every call to the function. - * A `mon_call` context manager enables function monitoring to be restricted to - specific calls. - * A `trigger` function which issues a brief pulse on the Pico or can set and - clear the pin on demand. - -## 4.1 The sync decorator - -This works as per the `@async` decorator, but with no `max_instances` arg. The -following example will activate GPIO 26 (associated with ident 20) for the -duration of every call to `sync_func()`: -```python -@monitor.sync(20) -def sync_func(): - pass -``` -Decorator args: - 1. `ident` - 2. `looping=False` Set `True` if the decorator is called repeatedly e.g. in a - nested function or method. The `True` value ensures validation of the ident - occurs once only when the decorator first runs. - -## 4.2 The mon_call context manager - -This may be used to monitor a function only when called from specific points in -the code. Since context managers may be used in a looping construct the ident -is only checked for conflicts when the CM is first instantiated. - -Usage: -```python -def another_sync_func(): - pass - -with monitor.mon_call(22): - another_sync_func() -``` - -It is advisable not to use the context manager with a function having the -`mon_func` decorator. The behaviour of pins and reports are confusing. - -## 4.3 The trigger timing marker - -The `trigger` closure is intended for timing blocks of code. A closure instance -is created by passing the ident. If the instance is run with no args a brief -(~80μs) pulse will occur on the Pico pin. If `True` is passed, the pin will go -high until `False` is passed. - -The closure should be instantiated once only in the outermost scope. -```python -trig = monitor.trigger(10) # Associate trig with ident 10. - -def foo(): - trig() # Pulse ident 10, GPIO 13 - -def bar(): - trig(True) # set pin high - # code omitted - trig(False) # set pin low -``` -## 4.4 Timing of code segments - -It can be useful to time the execution of a specific block of code especially -if the duration varies in real time. It is possible to cause a message to be -printed and a trigger pulse to be generated whenever the execution time exceeds -the prior maximum. A scope or logic analyser may be triggered by this pulse -allowing the state of other components of the system to be checked. - -This is done by re-purposing ident 0 as follows: -```python -trig = monitor.trigger(0) -def foo(): - # code omitted - trig(True) # Start of code block - # code omitted - trig(False) -``` -See [section 5.5](./README.md#55-timing-of-code-segments) for the Pico usage -and demo `syn_time.py`. - -# 5. Pico - -# 5.1 Pico pin mapping - -The Pico GPIO numbers used by idents start at 3 and have a gap where the Pico -uses GPIO's for particular purposes. This is the mapping between `ident` GPIO -no. and Pico PCB pin. Pins for the trigger and the UART/SPI link are also -identified: - -| ident | GPIO | pin | -|:-------:|:----:|:----:| -| nc/mosi | 0 | 1 | -| rxd/sck | 1 | 2 | -| nc/cs/ | 2 | 4 | -| 0 | 3 | 5 | -| 1 | 4 | 6 | -| 2 | 5 | 7 | -| 3 | 6 | 9 | -| 4 | 7 | 10 | -| 5 | 8 | 11 | -| 6 | 9 | 12 | -| 7 | 10 | 14 | -| 8 | 11 | 15 | -| 9 | 12 | 16 | -| 10 | 13 | 17 | -| 11 | 14 | 19 | -| 12 | 15 | 20 | -| 13 | 16 | 21 | -| 14 | 17 | 22 | -| 15 | 18 | 24 | -| 16 | 19 | 25 | -| 17 | 20 | 26 | -| 18 | 21 | 27 | -| 19 | 22 | 29 | -| 20 | 26 | 31 | -| 21 | 27 | 32 | -| trigger | 28 | 34 | - -## 5.2 The Pico code - -Monitoring via the UART with default behaviour is started as follows: -```python -from monitor_pico import run -run() -``` -By default the Pico retriggers a timer every time ident 0 becomes active. If -the timer times out, a pulse appears on GPIO28 which may be used to trigger a -scope or logic analyser. This is intended for use with the `hog_detect` coro, -with the pulse occurring when excessive latency is encountered. - -## 5.3 The Pico run function - -Arguments to `run()` can select the interface and modify the default behaviour. - 1. `period=100` Define the hog_detect timer period in ms. A 2-tuple may also - be passed for specialised reporting, see below. - 2. `verbose=()` A list or tuple of `ident` values which should produce console - output. A passed ident will produce console output each time that task starts - or ends. - 3. `device="uart"` Set to `"spi"` for an SPI interface. - 4. `vb=True` By default the Pico issues console messages reporting on initial - communication status, repeated each time the application under test restarts. - Set `False` to disable these messages. - -Thus to run such that idents 4 and 7 produce console output, with hogging -reported if blocking is for more than 60ms, issue -```python -from monitor_pico import run -run(60, (4, 7)) -``` -Hog reporting is as follows. If ident 0 is inactive for more than the specified -time, "Timeout" is issued. If ident 0 occurs after this, "Hog Nms" is issued -where N is the duration of the outage. If the outage is longer than the prior -maximum, "Max hog Nms" is also issued. - -This means that if the application under test terminates, throws an exception -or crashes, "Timeout" will be issued. - -## 5.4 Advanced hog detection - -The detection of rare instances of high latency is a key requirement and other -modes are available. There are two aims: providing information to users lacking -test equipment and enhancing the ability to detect infrequent cases. Modes -affect the timing of the trigger pulse and the frequency of reports. - -Modes are invoked by passing a 2-tuple as the `period` arg. - * `period[0]` The period (ms): outages shorter than this time will be ignored. - * `period[1]` is the mode: constants `SOON`, `LATE` and `MAX` are exported. - -The mode has the following effect on the trigger pulse: - * `SOON` Default behaviour: pulse occurs early at time `period[0]` ms after - the last trigger. - * `LATE` Pulse occurs when the outage ends. - * `MAX` Pulse occurs when the outage ends and its duration exceeds the prior - maximum. - -The mode also affects reporting. The effect of mode is as follows: - * `SOON` Default behaviour as described in section 4. - * `LATE` As above, but no "Timeout" message: reporting occurs at the end of an - outage only. - * `MAX` Report at end of outage but only when prior maximum exceeded. This - ensures worst-case is not missed. - -Running the following produce instructive console output: -```python -from monitor_pico import run, MAX -run((1, MAX)) -``` -## 5.5 Timing of code segments - -This may be done by issuing: -```python -from monitor_pico import run, WIDTH -run((20, WIDTH)) # Ignore widths < 20ms. -``` -Assuming that ident 0 is used as described in -[section 5.5](./README.md#55-timing-of-code-segments) a trigger pulse on GPIO28 -will occur each time the time taken exceeds both 20ms and its prior maximum. A -message with the actual width is also printed whenever this occurs. - -# 6. Test and demo scripts - -The following image shows the `quick_test.py` code being monitored at the point -when a task hogs the CPU. The top line 00 shows the "hog detect" trigger. Line -01 shows the fast running `hog_detect` task which cannot run at the time of the -trigger because another task is hogging the CPU. Lines 02 and 04 show the `foo` -and `bar` tasks. Line 03 shows the `hog` task and line 05 is a trigger issued -by `hog()` when it starts monopolising the CPU. The Pico issues the "hog -detect" trigger 100ms after hogging starts. - -![Image](./monitor.jpg) - -`full_test.py` Tests task timeout and cancellation, also the handling of -multiple task instances. If the Pico is run with `run((1, MAX))` it reveals -the maximum time the DUT hogs the CPU. On a Pyboard D I measured 5ms. - -The sequence here is a trigger is issued on ident 4. The task on ident 1 is -started, but times out after 100ms. 100ms later, five instances of the task on -ident 1 are started, at 100ms intervals. They are then cancelled at 100ms -intervals. Because 3 idents are allocated for multiple instances, these show up -on idents 1, 2, and 3 with ident 3 representing 3 instances. Ident 3 therefore -only goes low when the last of these three instances is cancelled. - -![Image](./tests/full_test.jpg) - -`latency.py` Measures latency between the start of a monitored task and the -Pico pin going high. In the image below the sequence starts when the DUT -pulses a pin (ident 6). The Pico pin monitoring the task then goes high (ident -1 after ~20μs). Then the trigger on ident 2 occurs 112μs after the pin pulse. - -![Image](./tests/latency.jpg) - -`syn_test.py` Demonstrates two instances of a bound method along with the ways -of monitoring synchronous code. The trigger on ident 5 marks the start of the -sequence. The `foo1.pause` method on ident 1 starts and runs `foo1.wait1` on -ident 3. 100ms after this ends, `foo.wait2` on ident 4 is triggered. 100ms -after this ends, `foo1.pause` on ident 1 ends. The second instance of `.pause` -(`foo2.pause`) on ident 2 repeats this sequence shifted by 50ms. The 10ms gaps -in `hog_detect` show the periods of deliberate CPU hogging. - -![Image](./tests/syn_test.jpg) - -`syn_time.py` Demonstrates timing of a specific code segment with a trigger -pulse being generated every time the period exceeds its prior maximum. - -![Image](./tests/syn_time.jpg) - -# 7. Internals - -## 7.1 Performance and design notes - -Using a UART the latency between a monitored coroutine starting to run and the -Pico pin going high is about 23μs. With SPI I measured -12μs. This isn't as -absurd as it sounds: a negative latency is the effect of the decorator which -sends the character before the coroutine starts. These values are small in the -context of `uasyncio`: scheduling delays are on the order of 150μs or greater -depending on the platform. See `tests/latency.py` for a way to measure latency. - -The use of decorators eases debugging: they are readily turned on and off by -commenting out. - -The Pico was chosen for extremely low cost. It has plenty of GPIO pins and no -underlying OS to introduce timing uncertainties. The PIO enables a simple SPI -slave. - -Symbols transmitted by the UART are printable ASCII characters to ease -debugging. A single byte protocol simplifies and speeds the Pico code. - -The baudrate of 1Mbps was chosen to minimise latency (10μs per character is -fast in the context of uasyncio). It also ensures that tasks like `hog_detect`, -which can be scheduled at a high rate, can't overflow the UART buffer. The -1Mbps rate seems widely supported. - -## 7.2 How it works - -This is for anyone wanting to modify the code. Each ident is associated with -two bytes, `0x40 + ident` and `0x60 + ident`. These are upper and lower case -printable ASCII characters (aside from ident 0 which is `@` paired with the -backtick character). When an ident becomes active (e.g. at the start of a -coroutine), uppercase is transmitted, when it becomes inactive lowercase is -sent. - -The Pico maintains a list `pins` indexed by `ident`. Each entry is a 3-list -comprising: - * The `Pin` object associated with that ident. - * An instance counter. - * A `verbose` boolean defaulting `False`. - -When a character arrives, the `ident` value is recovered. If it is uppercase -the pin goes high and the instance count is incremented. If it is lowercase the -instance count is decremented: if it becomes 0 the pin goes low. - -The `init` function on the DUT sends `b"z"` to the Pico. This sets each pin -in `pins` low and clears its instance counter (the program under test may have -previously failed, leaving instance counters non-zero). The Pico also clears -variables used to measure hogging. In the case of SPI communication, before -sending the `b"z"`, a 0 character is sent with `cs/` high. The Pico implements -a basic SPI slave using the PIO. This may have been left in an invalid state by -a crashing DUT. The slave is designed to reset to a "ready" state if it -receives any character with `cs/` high. - -The ident `@` (0x40) is assumed to be used by the `hog_detect()` function. When -the Pico receives it, processing occurs to aid in hog detection and creating a -trigger on GPIO28. Behaviour depends on the mode passed to the `run()` command. -In the following, `thresh` is the time passed to `run()` in `period[0]`. - * `SOON` This retriggers a timer with period `thresh`. Timeout causes a - trigger. - * `LATE` Trigger occurs if the period since the last `@` exceeds `thresh`. The - trigger happens when the next `@` is received. - * `MAX` Trigger occurs if period exceeds `thresh` and also exceeds the prior - maximum. - -## 7.3 ESP8266 note - -ESP8266 applications can be monitored using the transmit-only UART 1. - -I was expecting problems: on boot the ESP8266 transmits data on both UARTs at -75Kbaud. In practice `monitor_pico.py` ignores this data for the following -reasons. - -A bit at 75Kbaud corresponds to 13.3 bits at 1Mbaud. The receiving UART will -see a transmitted 1 as 13 consecutive 1 bits. In the absence of a start bit, it -will ignore the idle level. An incoming 0 will be interpreted as a framing -error because of the absence of a stop bit. In practice the Pico UART returns -`b'\x00'` when this occurs; `monitor.py` ignores such characters. A monitored -ESP8266 behaves identically to other platforms and can be rebooted at will. - -# 8. A hardware implementation - -I expect to use this a great deal, so I designed a PCB. In the image below the -device under test is on the right, linked to the Pico board by means of a UART. - -![Image](./monitor_hw.JPG) - -I can supply a schematic and PCB details if anyone is interested. - -This project was inspired by -[this GitHub thread](https://github.com/micropython/micropython/issues/7456). diff --git a/v3/as_demos/monitor/monitor.jpg b/v3/as_demos/monitor/monitor.jpg deleted file mode 100644 index 6a7f20a79b0b7c511df1cb803bbc993bdc2af52d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90535 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

N(#hG(?d=Y`oG^5}J<7@yc8J94yR# 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 NDRb}Kzw-W?{11D$j647U diff --git a/v3/as_demos/monitor/monitor.py b/v3/as_demos/monitor/monitor.py deleted file mode 100644 index db82578..0000000 --- a/v3/as_demos/monitor/monitor.py +++ /dev/null @@ -1,167 +0,0 @@ -# monitor.py -# Monitor an asynchronous program by sending single bytes down an interface. - -# Copyright (c) 2021 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -import uasyncio as asyncio -from machine import UART, SPI, Pin -from time import sleep_us -from sys import exit - -# Quit with an error message rather than throw. -def _quit(s): - print("Monitor " + s) - exit(0) - - -_write = lambda _: _quit("must run set_device") -_ifrst = lambda: None # Reset interface. If UART do nothing. - -# For UART pass initialised UART. Baudrate must be 1_000_000. -# For SPI pass initialised instance SPI. Can be any baudrate, but -# must be default in other respects. -def set_device(dev, cspin=None): - global _write - global _ifrst - if isinstance(dev, UART) and cspin is None: # UART - _write = dev.write - elif isinstance(dev, SPI) and isinstance(cspin, Pin): - cspin(1) - - def spiwrite(data): - cspin(0) - dev.write(data) - cspin(1) - - _write = spiwrite - - def clear_sm(): # Set Pico SM to its initial state - cspin(1) - dev.write(b"\0") # SM is now waiting for CS low. - - _ifrst = clear_sm - else: - _quit("set_device: invalid args.") - - -# /mnt/qnap2/data/Projects/Python/AssortedTechniques/decorators -_available = set(range(0, 22)) # Valid idents are 0..21 -# Looping: some idents may be repeatedly instantiated. This can occur -# if decorator is run in looping code. A CM is likely to be used in a -# loop. In these cases only validate on first use. -_loopers = set() - - -def _validate(ident, num=1, looping=False): - if ident >= 0 and ident + num < 22: - try: - for x in range(ident, ident + num): - if looping: - if x not in _loopers: - _available.remove(x) - _loopers.add(x) - else: - _available.remove(x) - except KeyError: - _quit("error - ident {:02d} already allocated.".format(x)) - else: - _quit("error - ident {:02d} out of range.".format(ident)) - - -# asynchronous monitor -def asyn(n, max_instances=1, verbose=True, looping=False): - def decorator(coro): - _validate(n, max_instances, looping) - instance = 0 - - async def wrapped_coro(*args, **kwargs): - nonlocal instance - d = 0x40 + n + min(instance, max_instances - 1) - v = int.to_bytes(d, 1, "big") - instance += 1 - if verbose and instance > max_instances: # Warning only. - print("Monitor ident: {:02d} instances: {}.".format(n, instance)) - _write(v) - try: - res = await coro(*args, **kwargs) - except asyncio.CancelledError: - raise # Other exceptions produce traceback. - finally: - d |= 0x20 - v = int.to_bytes(d, 1, "big") - _write(v) - instance -= 1 - return res - - return wrapped_coro - - return decorator - - -# If SPI, clears the state machine in case prior test resulted in the DUT -# crashing. It does this by sending a byte with CS\ False (high). -def init(): - _ifrst() # Reset interface. Does nothing if UART. - _write(b"z") # Clear Pico's instance counters etc. - - -# Optionally run this to show up periods of blocking behaviour -async def hog_detect(s=(b"\x40", b"\x60")): - while True: - for v in s: - _write(v) - await asyncio.sleep_ms(0) - - -# Monitor a synchronous function definition -def sync(ident, looping=False): - def decorator(func): - _validate(ident, 1, looping) - vstart = int.to_bytes(0x40 + ident, 1, "big") - vend = int.to_bytes(0x60 + ident, 1, "big") - - def wrapped_func(*args, **kwargs): - _write(vstart) - res = func(*args, **kwargs) - _write(vend) - return res - - return wrapped_func - - return decorator - - -# Monitor a function call -class mon_call: - def __init__(self, n): - # looping: a CM may be instantiated many times - _validate(n, 1, True) - self.vstart = int.to_bytes(0x40 + n, 1, "big") - self.vend = int.to_bytes(0x60 + n, 1, "big") - - def __enter__(self): - _write(self.vstart) - return self - - def __exit__(self, type, value, traceback): - _write(self.vend) - return False # Don't silence exceptions - - -# Either cause pico ident n to produce a brief (~80μs) pulse or turn it -# on or off on demand. No looping: docs suggest instantiating at start. -def trigger(n): - _validate(n) - on = int.to_bytes(0x40 + n, 1, "big") - off = int.to_bytes(0x60 + n, 1, "big") - - def wrapped(state=None): - if state is None: - _write(on) - sleep_us(20) - _write(off) - else: - _write(on if state else off) - - return wrapped diff --git a/v3/as_demos/monitor/monitor_gc.jpg b/v3/as_demos/monitor/monitor_gc.jpg deleted file mode 100644 index 3e667657af6d8f57afe2ba105e96ae3fcbbd6fba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68891 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 diff --git a/v3/as_demos/monitor/monitor_hw.JPG b/v3/as_demos/monitor/monitor_hw.JPG deleted file mode 100644 index 8cca56e2feb15e17e625b064415b8ddd551a5c34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 259855 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�Zc$QkF+(6E_oLmT*#%0DbOS-1!MKcC@ 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 diff --git a/v3/as_demos/monitor/monitor_pico.py b/v3/as_demos/monitor/monitor_pico.py deleted file mode 100644 index bfce0a0..0000000 --- a/v3/as_demos/monitor/monitor_pico.py +++ /dev/null @@ -1,177 +0,0 @@ -# monitor_pico.py -# Runs on a Raspberry Pico board to receive data from monitor.py - -# Copyright (c) 2021 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -# Device gets a single ASCII byte defining the pin number and whether -# to increment (uppercase) or decrement (lowercase) the use count. -# Pin goes high if use count > 0 else low. -# incoming numbers are 0..21 which map onto 22 GPIO pins - -import rp2 -from machine import UART, Pin, Timer, freq -from time import ticks_ms, ticks_diff - -freq(250_000_000) - -# ****** SPI support ****** -@rp2.asm_pio(autopush=True, in_shiftdir=rp2.PIO.SHIFT_LEFT, push_thresh=8) -def spi_in(): - label("escape") - set(x, 0) - mov(isr, x) # Zero after DUT crash - wrap_target() - wait(1, pins, 2) # CS/ False - wait(0, pins, 2) # CS/ True - set(x, 7) - label("bit") - wait(0, pins, 1) - wait(1, pins, 1) - in_(pins, 1) - jmp(pin, "escape") # DUT crashed. On restart it sends a char with CS high. - jmp(x_dec, "bit") # Post decrement - wrap() - - -class PIOSPI: - def __init__(self): - self._sm = rp2.StateMachine( - 0, - spi_in, - in_shiftdir=rp2.PIO.SHIFT_LEFT, - push_thresh=8, - in_base=Pin(0), - jmp_pin=Pin(2, Pin.IN, Pin.PULL_UP), - ) - self._sm.active(1) - - # Blocking read of 1 char. Returns ord(ch). If DUT crashes, worst case - # is where CS is left low. SM will hang until user restarts. On restart - # the app - def read(self): - return self._sm.get() & 0xFF - - -# ****** Define pins ****** - -# Valid GPIO pins -# GPIO 0,1,2 are for interface so pins are 3..22, 26..27 -PIN_NOS = list(range(3, 23)) + list(range(26, 28)) - -# Index is incoming ID -# contents [Pin, instance_count, verbose] -pins = [] -for pin_no in PIN_NOS: - pins.append([Pin(pin_no, Pin.OUT), 0, False]) - -# ****** Timing ***** - -pin_t = Pin(28, Pin.OUT) - - -def _cb(_): - pin_t(1) - print("Timeout.") - pin_t(0) - - -tim = Timer() - -# ****** Monitor ****** - -SOON = const(0) -LATE = const(1) -MAX = const(2) -WIDTH = const(3) -# Modes. Pulses and reports only occur if an outage exceeds the threshold. -# SOON: pulse early when timer times out. Report at outage end. -# LATE: pulse when outage ends. Report at outage end. -# MAX: pulse when outage exceeds prior maximum. Report only in that instance. -# WIDTH: for measuring time between arbitrary points in code. When duration -# between 0x40 and 0x60 exceeds previosu max, pulse and report. - -# native reduced latency to 10μs but killed the hog detector: timer never timed out. -# Also locked up Pico so ctrl-c did not interrupt. -# @micropython.native -def run(period=100, verbose=(), device="uart", vb=True): - if isinstance(period, int): - t_ms = period - mode = SOON - else: - t_ms, mode = period - if mode not in (SOON, LATE, MAX, WIDTH): - raise ValueError("Invalid mode.") - for x in verbose: - pins[x][2] = True - # A device must support a blocking read. - if device == "uart": - uart = UART(0, 1_000_000) # rx on GPIO 1 - - def read(): - while not uart.any(): # Prevent UART timeouts - pass - return ord(uart.read(1)) - - elif device == "spi": - pio = PIOSPI() - - def read(): - return pio.read() - - else: - raise ValueError("Unsupported device:", device) - - vb and print("Awaiting communication.") - h_max = 0 # Max hog duration (ms) - h_start = -1 # Absolute hog start time: invalidate. - while True: - if x := read(): # Get an initial 0 on UART - tarr = ticks_ms() # Arrival time - if x == 0x7A: # Init: program under test has restarted - vb and print("Got communication.") - h_max = 0 # Restart timing - h_start = -1 - for pin in pins: - pin[0](0) # Clear pin - pin[1] = 0 # and instance counter - continue - if mode == WIDTH: - if x == 0x40: # Leading edge on ident 0 - h_start = tarr - elif x == 0x60 and h_start != -1: # Trailing edge - dt = ticks_diff(tarr, h_start) - if dt > t_ms and dt > h_max: - h_max = dt - print(f"Max width {dt}ms") - pin_t(1) - pin_t(0) - elif x == 0x40: # hog_detect task has started. - if mode == SOON: # Pulse on absence of activity - tim.init(period=t_ms, mode=Timer.ONE_SHOT, callback=_cb) - if h_start != -1: # There was a prior trigger - dt = ticks_diff(tarr, h_start) - if dt > t_ms: # Delay exceeds threshold - if mode != MAX: - print(f"Hog {dt}ms") - if mode == LATE: - pin_t(1) - pin_t(0) - if dt > h_max: - h_max = dt - print(f"Max hog {dt}ms") - if mode == MAX: - pin_t(1) - pin_t(0) - h_start = tarr - p = pins[x & 0x1F] # Key: 0x40 (ord('@')) is pin ID 0 - if x & 0x20: # Going down - if p[1] > 0: # Might have restarted this script with a running client. - p[1] -= 1 # or might have sent trig(False) before True. - if not p[1]: # Instance count is zero - p[0](0) - else: - p[0](1) - p[1] += 1 - if p[2]: - print(f"ident {i} count {p[1]}") diff --git a/v3/as_demos/monitor/tests/full_test.jpg b/v3/as_demos/monitor/tests/full_test.jpg deleted file mode 100644 index 95ed14a9618cddc92d5f64de9c9697746115f335..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69423 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 E0bEcmMF0Q* diff --git a/v3/as_demos/monitor/tests/full_test.py b/v3/as_demos/monitor/tests/full_test.py deleted file mode 100644 index 47950a5..0000000 --- a/v3/as_demos/monitor/tests/full_test.py +++ /dev/null @@ -1,47 +0,0 @@ -# full_test.py - -# Copyright (c) 2021 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -# Tests monitoring of timeout, task cancellation and multiple instances. - -import uasyncio as asyncio -from machine import Pin, UART, SPI -import monitor - -trig = monitor.trigger(4) -# Define interface to use -monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz -#monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz - -@monitor.asyn(1, 3) -async def forever(): - while True: - await asyncio.sleep_ms(100) - - -async def main(): - monitor.init() - asyncio.create_task(monitor.hog_detect()) # Watch for gc dropouts on ID0 - while True: - trig() - try: - await asyncio.wait_for_ms(forever(), 100) # 100ms pulse on ID1 - except asyncio.TimeoutError: # Mandatory error trapping - pass - # Task has now timed out - await asyncio.sleep_ms(100) - tasks = [] - for _ in range(5): # ID 1, 2, 3 go high, then 500ms pause - tasks.append(asyncio.create_task(forever())) - await asyncio.sleep_ms(100) - while tasks: # ID 3, 2, 1 go low - tasks.pop().cancel() - await asyncio.sleep_ms(100) - await asyncio.sleep_ms(100) - - -try: - asyncio.run(main()) -finally: - asyncio.new_event_loop() diff --git a/v3/as_demos/monitor/tests/latency.jpg b/v3/as_demos/monitor/tests/latency.jpg deleted file mode 100644 index 4cdea2c8564b2d3fe101d65db9b3d75ed2662090..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76931 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

lj})I9Ps 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 z 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 1MHz - -@monitor.asyn(1) -async def pulse(pin): - pin(1) # Pulse pin - pin(0) - trig() # Pulse Pico pin ident 2 - await asyncio.sleep_ms(30) - -async def main(): - monitor.init() - while True: - await pulse(test_pin) - await asyncio.sleep_ms(100) - -try: - asyncio.run(main()) -finally: - asyncio.new_event_loop() diff --git a/v3/as_demos/monitor/tests/looping.py b/v3/as_demos/monitor/tests/looping.py deleted file mode 100644 index dd33459..0000000 --- a/v3/as_demos/monitor/tests/looping.py +++ /dev/null @@ -1,58 +0,0 @@ -# syn_test.py -# Tests the monitoring synchronous code and of an async method. - -# Copyright (c) 2021 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -import uasyncio as asyncio -import time -from machine import Pin, UART, SPI -import monitor - -# Define interface to use -monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz -# monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz -trig = monitor.trigger(5) - - -class Foo: - def __init__(self): - pass - - @monitor.asyn(1, 2) # ident 1/2 high - async def pause(self): - while True: - trig() - self.wait1() # ident 3 10ms pulse - await asyncio.sleep_ms(100) - with monitor.mon_call(4): # ident 4 10ms pulse - self.wait2() - await asyncio.sleep_ms(100) - # ident 1/2 low - - async def bar(self): - @monitor.asyn(3, looping = True) - async def wait1(): - await asyncio.sleep_ms(100) - @monitor.sync(4, True) - def wait2(): - time.sleep_ms(10) - trig() - await wait1() - trig() - wait2() - - -async def main(): - monitor.init() - asyncio.create_task(monitor.hog_detect()) # Make 10ms waitx gaps visible - foo = Foo() - while True: - await foo.bar() - await asyncio.sleep_ms(100) - await foo.pause() - -try: - asyncio.run(main()) -finally: - asyncio.new_event_loop() diff --git a/v3/as_demos/monitor/tests/quick_test.py b/v3/as_demos/monitor/tests/quick_test.py deleted file mode 100644 index 5b74d67..0000000 --- a/v3/as_demos/monitor/tests/quick_test.py +++ /dev/null @@ -1,44 +0,0 @@ -# quick_test.py -# Tests the monitoring of deliberate CPU hgging. - -# Copyright (c) 2021 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -import uasyncio as asyncio -import time -from machine import Pin, UART, SPI -import monitor - -# Define interface to use -monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz -# monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz - -trig = monitor.trigger(4) - -@monitor.asyn(1) -async def foo(t): - await asyncio.sleep_ms(t) - -@monitor.asyn(2) -async def hog(): - await asyncio.sleep(5) - trig() # Hog start - time.sleep_ms(500) - -@monitor.asyn(3) -async def bar(t): - await asyncio.sleep_ms(t) - -async def main(): - monitor.init() - asyncio.create_task(monitor.hog_detect()) - asyncio.create_task(hog()) # Will hog for 500ms after 5 secs - while True: - asyncio.create_task(foo(100)) - await bar(150) - await asyncio.sleep_ms(50) - -try: - asyncio.run(main()) -finally: - asyncio.new_event_loop() diff --git a/v3/as_demos/monitor/tests/syn_test.jpg b/v3/as_demos/monitor/tests/syn_test.jpg deleted file mode 100644 index 783940d277f36500164dbafb0b5088ffea5b8a81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78274 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%0DaYGMg`{Tu90P zONK%DyUr=sf5q!=3fFtPB=jc}6q5hp|Po)smQ6Vv*3^ zPfd9F;BS3pxgWFjP2m-2=xy0&-7XDaeKchI=uCs|jprF@PQ|_Je%CS?DbJJ4tYB`E zrF#z6eCXG`oSusseEInqqpIo^oSxSu`-||^e8qdXKl`)^7d;;aPu%|Uy`3~8()X@V z{jI2lnR{_8Z_MNPgl^qu!<^N6b4M@KeBZm3^GA6~kn^Y7RIzhm4=#_FZfL>3nWk+y zzSk?r5;ZiSBHU3^FWrSvV!!mgz!+a(xq)|9 zPBD64b_MRqe7LK+nsqI71apNX=U`2BHLu6;r~Tp&dOZHpO9zR{wLQ1|t3Pllj+mHf z679#pS7}l=JC>=dcjwZ6PK(uq%6YiKt0vfMd(_JAtY0zu)AJ0Fr(Gn0ALQ()<$nAz z+BYm!<{4agk=CN)kjv;0uUo9x4v{*-Mq_}df(xVn1{heZ(OGn}4}Ura?5m{_I#!A{b&mzHsFMJ^rTf5kw~N zN5#CQl^?p>WEPUR&ik3PU7-qT0<7?A$*8x3LZlUexp}e~ZsMXQ$HM$286U^UGPq+u z(GQErPyQIX({tNzd^-&4yixC#{2K1O}TGio5?%xfZDBmYIrDJ4wg zir63Xgi5Y2A9~m61?FZrY>{znhmIa>s_w6+jm!N3WnZiS#Kf;{+UddNcFo~6kBS-tx6V*2%0)JgrU!S$^6~#LGt79TMX*GD1pNSB z!MiI3#D(xE(KOIMw6H2vRl+s@!U)S;y>L1PWdmFke7QqE*kKcJ2OlnHEUXf|ZWViL ze*~4&9{#)mdcqNa45_yv%2r;`Z_H==IQz|JLo|vg;7%WGBURt4#N0~g57`II_9KCh zaDm+ce!@v?|3@&(p&{yCPwnq6SHA2(?+e2!Ei|N4<8$eOfxcNmNy>zwXg48z;G<3t zy^J867r2cNJj$gbsA@r;mvy>4D7ewzm3j7Oda!5#c=aZZ?W-She3Q4 zztNGz*_4)O`vs1m;c)nV#&029)Z}`{Xm#U%iy2uwl*Gt!Emz}Ht$FYjW=XD)dmr3` zz9?{CyPRHr_G^0ZWfW~>V7r{2h;}q^G=ch7K}yQ#rn;)Ef|3kawFfKi&uvU?ozXcV z$kxuqNnK8o4lE$kVNOFhV9_2MEac-En>ss4+`OsuYZ?2W&)eUSi~LYGJA&3f_y5}# z{JUn(reOJ-4q%C!IyixC1C%=e=D+XafPmuxOlo@1_%4871DMST2ngVQM7zl`yo7)) zkT5C$AyOwzbt#}5DS+uLPQfOpVAFd}wg870;4s{^wFC4~Zy&>V5wI5mwzY8uvLOp% z86VHg?v@5PGl0)|NDfkfl%bmt9b^KzLY9yX*z#ZpXFJg145@>D$^T;fIpp}8V3Y|M zWeJ&r5z>%7WD6N1$3uu~0B}J0V_Ro)9&Th43Z5hcof$bg+F*nr%xDNYj5s>le{pnl zm=1Pah9T&a-JkLH&ml-~3$#c4X=6x(AcDsb^uF#-o5?c>Dh-4n$^i#sCu1ZZ#O4gD zIoSDG&w(I(9S9=%3PCt}$94mKh<+%e4}vs-t(4jzC?OGon7|egf&PD?H)8wfx843N z&WZhyyF6&9sE7j;ba0%(LhP5E!NJ7Dz{119!^6eF#l=5MOo)G$=qxTSAvqxt2`L#F z86E)zB{?Z2F)0}-a+e1Uj6pwxedY`{DLyVf>HoMK)qq_dloWKZ`vEpMe(my{2NNF^ zu^)mk#c@IGhG3k*!~%OI)Sw-?S@Nq7Oxxo{ZnD+79bF8Y14DkDBa>CRDe*ip-1A7?_YSBk@yNJ zFTb5LPTsg-$1ZL1dg|MU-ekA1fTEj!;hj{7)eH0dz4B9iz74h5`IydireRyn8&>(V z56=ch9 zP!!ApGJ#f@8C=2VEU7)VwvwwQ>h60zKOQ>2x;Xc9$39W?gE*oAZO|iy*CS0VE&I6p zJW|ywIT0~30;#Zo8~b86VP);F%`bI-2LU8_Lir9;cPzn8D9oSp<~vb zw;h5gE~DtQ7AxBufPr+6aD8#wNU~ljd(8De6^A6AlsiE57JQByv!%A}5UT%;I0P%` zW*%~i;57^kwA!U6w5N{y_KA9s%$;NKh0YMjntm~Qn?^D>!tr@QJsI=k9>EP&qlB&x z*{w>{MKs3hpU6dFDWyuj5EeHnM4`fJVFkPdDsC)6Jub%8h8sd*08Yi+jU^!V z!^S&g4Z*h%IW>3^LNz5FpFxH1^xp#QFsP~>Y56v*si)1^!9$7t!pFYqFNS<=Md*sU zbNvjcvjnm40X2z;=TK8JPfGe21QFi745%k)7qMSn=)Vi0f~$>_V^U_R_v$()L@bVA z1>uq*zR`rDdV=z;J@ci6OtIhIixW^lSbFer543&@2Mat3aFXa|K^<5LVGKO9)X|_q1KBdPG4+Z6{eZF zOm~Ic{+V56;&7=qK0f8{?wSB#7k1%lPs7^}AH(ce<(mMm{`mU)y8qOAMMZyG4+Ud7 zuWNfZe)$?ns*6Iv?YIxcDw+zZMMVZoK$zYFf$F7^HSJn{n6@Id?Mo4D@QKj$L zMzMi?cc@$(BB3VhV_3WQVJR1BCX_iL;Xd2gV@AMyNS#m^(7yGZovr>7Kh~U+S0G#x zvZ=Feqdq(m`_m+znO(m7{PgpDFrAnzXFPo1%X55Z-^WcB=!1IzY6$1K4o->}>2UjW z_2>WytPqzpTb-6YkE~$q2TFiBbE=@&tQ*D!Wq#AZ%Irb4C&7vtVFGgGZklFvCPvR* z8l731-7Cu{5!Wnq66bYk$=&$KVRY6P#7=h8?Ly%+AhT})!?=S&X`#_L=aF6IH{X%7khCJax`2z2<{gK&Fe8_{#342xx6R5C)>o~E568Vlg>q3Vm`+`+yi|!RYg>*yDAl752lAFSGFd`0p_Pow;?QFUQ%H?8=9AhQX= zZ1dcy7TQadTYymyFRsl&j(QtzJFd5A4CZ%!e%~D}^Qg)h6!fM|ab5Gm{I+&~-UJ~R z@G}=z7LCN;dDXcHZaG&tAw1|Bf9>)JW~Vnr(z{#)iU&oJI0hXn_mP%oJMH0S0tD?A7*WiMX*3NydnYQx(!Spo!=g4w>CW z`@Xw^(0*XYn-Zcjft?2gyG%Ql|0m-EeI!_v-s!0a}> z#on)ax`W_mJ?dwY`Vc`8V%TZCpR&3l*Ls*U<_+#MQR}|?-Hx7hF^!#(S)*`qwBXUC z(%P4~8yu>QsVnV=wQXCXowaiZiv=NLqNO3#H;9pBAeOU%x;>9n{Ag0|+acTxjcEU2 z^`|}BOeeQ*cYymrxx8kdHrWB2^_iaWOtT)_d_hM9CQVNDMheq)c-GqC7rF=(axo!K z9}AM+8K3%9pN#W85sb8?D;qx4^Sdzn4H4C{U3jX{BXKk%RWVaBaRgr!VlR#AwjGPC zY52hIN_G=5ddAF0FQddmd+IHVkQKMvE|St2Pe;>=YqN=_M$$qLSCaSBz??1e>)!1! zBA6XDs{5XWa8sa=21?|6b$4BU;Nj-3yQT~LBgzep@GpChfuF9drx&L(RUE=|tz>{O z6tXmK7pD_LXm-3UFtLt;I}o<4o<+b1;AMJJX}N~z5ah7V%#dAD#&nD1CLrS;(Id-cv-!n#X=Hc;z0EdG zDsCYF00rgg&&@Ze(_wyTeH-sLHrYFVFpF1fz;9`N;9j|nEoV}YOkkR!iN$TPCO_z;znNQH|T)+pS#_k}m zGh^_TF!=wv&ubs99W=F(s*;d<`p=fhHhJPK( zTe_Ye2vtqVgWr)rkSunGPs*P5Sj#zI=-m}S9fUK_+_~vlDU^j9iX?2D>(PwjK zR9CDIY^&Bb-TgQQJSQ*|enKfX45zl|G5HV$p-VsaqrV)1LY6ok$}TuBr&UV_5kJ9( zP!v2}I}|^bGxn7;gl@xZ2d)C5pvb#i`}X{T7(vb#PZ|}) z5eOp2ieKLq=+w1?tgPGBNFo{_Zka_Z!w(SD*OeM~fiwbfJ*XDn%V9*n{N@YT0f_~Q zCN^e2*gz=G&2y9e?Vlj@1o|mobb6bP)K3T|4=i|ePHsL#*vS{a7DjL*WqE_oZuIq> z(h}j*hDsHdr;n2_F!RTjJjY4uAEtLgWSK1Ooet0?DNj$YLmGRzYfN*meNl;0KMb^F zi0EWLhR|!!eW6>nl^(^ei!DOF=rpO+yAQ^z{hI+#2uo(hYq{txfKaJ#r3^124U4$> zrSh#%dK^53`(V!s_}j=c+1ca=`0)H&l6~CT2{Eo~^O*-RUld1inMhuy0MGUUIR$N1 zB^%P;$v~>Bm)&h3pw;F`2QhMj0AFMi+oAiNYLXrzVt;AWINu1vjY9Aye4737Rn&U~ zZ@lI&h!#+gjuY+G#^G71t_>%x8f=ghPOiWAfLN@|UUxD~U52hEYalXR*?`S$q0-*> zcZYK!vP1*xHr^CHzB&kzdlK<@&gR=a1!yUa+i%#YjM8=yR>|A^sqSvL?7?>H=8i;m!4ry6NS}Xro z{3rlFdrky#thn2EO6-yO5D&$+AuZz0Au0F6AO`e8!iXbARHeN`1FOG5jX4RWw zsViIzC~+JC&N05w=ld1S?@Nl9rp2Mx-SpoQEw8<|euBL3>CLy02Bm`@+iJlH40Yy^ zqy>0NZZ-caZZ4G~2RSch&9Xj$oK!!~*-4yhFiB@QNr)moQy%S-lFcUwW!7cdUD+J) z3VI_wkea+@@kSi!-X2dsEwSxPCdsE^e^afT<2u1MfwtK;&rE?9eS~sy9?`?8cN)Md# zY*1_hjN8HD60+b(z4%rFEeorpT6EX8Efgpx@2qbbcPMs4xXI*h!wksqLBv%aSH)~D zn!{J9%fx`iTUt<3pWWctzDK&rRf4eem2I=E3wn6IhS|$*j3@ZY8KWD^ zt$QwWg1lcxqKPpsTpFAx<(ccw-mstesm<>Mq~ON~^AAx@Tt-e%=O=H1t6sLRB{0Sv zP@Dt541o-RuIVNUS7}_8fFVr|->Ly8bZ^DTK3r*6?)sd@*M?ri)8eavQ%{Q;h5ExAds5*l9 zb>)Fn?X=e|(0#Emq$p8M?b*G2Dn(SR7e|jlAW3VTuY{!*F+_JmTX!Gsy^9nk)n_w4f^(i`l&m~WHbo!_ zM)IM&D3T5p!uVW#BF(XbDOueUGI7^x-=O?F?1 zLOH{jCiK02dQ0}6CWIyHxqP2I+0}3hLSY1RL!#mtlm9*kQb`a=hMk-CcDD^hN5sq+ zP~UCu0+R%ha-29+h)_Ql zqP)7r*&^)hPQ6#*`UwS?+QPG&Y_pa>0{Y{0h{chbVACPud3w2X`?IQ&%?_d%WGe(6 zUvAaL+1&~k54|lHHKVi4y~4Yp;wa9c?L+9kh}jPr2@AAm{T1F!0+ZMQj{G^B;g8MaAn>6|gJNrZMm+gRl}lI)5vbb#FT6 z0j7jJpv_O8s`cG4PPoi`_v!OvUcbV!*!7s@Ee=x9cpNPLg-UT0iIyMS{_hwSHd=Uf zfxAkI5^8$S(r)qzNI4Bbjc%SYvmz{u9s1(f$(tSRLpeQ-&$RaFAR_)YC0CB_M6lju zax6WYCKzI2mEiHf)l+ce&c}^9b6=nVC?{%#U+F z)+nR|@+&Ar3G6?RD7HshO*LpiXKY1%KGJ|+@}y;Dk3kr4FJ1EG+YqW5A;1I_zR&?D zSdj|$b&X!kO?Oz;FS8{0B6@yh7b}*{9h2X!wFsdI6qf0YH13}*{16nNX_A0~7n|}x zg8>8^6e15bI+R=dFIy-*HijPDL}8pLXVoXKRPu!$uS_=1S`IzXL|!z~%mG{`uucGU zgksFISgcKxY`-P-<|6r>C@9hnw`cB{`xfF!a*k!-N~q3V#3GPwEgi@|{2{ubX9==x zz7X;@_?O|RFXcEm@#W>b>TaIh*qj0Z<2cEuYI}an>DcYl%)$i1XEqmGr{f}fl$@L2 zT|MRbA=?|RnM;EJA8{k#v~d@h>#Fs>J)V4ID)kOsrCM%xkioY%JGE4mIjy5Rs`uAr zwBQFKBFgc7{op&|XIDu>);(AZAk2ws^+zDX9GaD(8MFCUU_tQ61oD;G2i%B9mgAWI z(v1}?KAtR}VdV2}eee*5`y|Jm%CV1moMdAoD^s%vBSVJI!IG+=SAOBK!RV>AJc?72 z#KE3>=d*2P#hMyql6WS@?U%rWe{@G@Ld-`U+yB3`-y1p^iAtDqqIP{4~Q#AIhMz~%`B9Tz4iHy`0e9$tQNdWlO6+TvbF;SUI8n>TwZBAfH zi%X743{$t$wVh3~40>WQMIXw=#%HReO`w_7z&K{>F!8_Sc}i;kc>09WsN9|<=cf)) z?;OO?X95EJnGnZG@n=edUym0~>JWIAnUvy;5$ziEl*Z7?cnL%KTl{ARvlpfWaj}jE z{92YxoF0_uHB;k0^?}_&z6$N))neeMdzUXdQCYHh@Q~Ne%w#h#u4yszu)SWY%dATk zT#Q<6tZgX`xe=v2+0i0|Avj6GSy3az7m<>tm{Y?s8UviL>TW@iT^Zg`Oo~s}dHm z4cG3#ZJwDXctjjGo;911`X5yoc-+iW{gTx|WtF7Q9A1dyciFtPFkO(taE z#r{7^095%OOa1ql*aXu56W;!bXHCrg;uD_F(A;C3p;Jp%HhU4m35z;!eOLl_>{18C zQpw$Kb%N5*wl;f^`kw8J_W^^{^0svO7paXV2lLz_1|<~*@8+bmsc+a6e*rxgivzlT zo$g6@k#I+`T%Afz{16I8f)2qimVu^h<58pQ<(BUUtu-w1Gx;M1rND{KI6gyxg@%t& zCqUKqq!lYsnG^d71CyhEH~W*O;*J(1`jCXKq?VxygOipzxw#po@pADAiN8+8^p?B< z+GodKL`A9T1W;~&MRGZff^N$aU@(wl6c-;IM1MQl6~EL(K8`6p?0skt-EDgLtllD9 z{9^vELsGv^IL4Rk&KU;LovX>7xp*HZ>6w377w;MxR#eMw0I7`P%}#v6UD``hQc@Cx z&uW>>IU7bo6(?AqyrC}(MxF#GvXRj3bhVWG<6*y=PkPJz!|3^fKbo8jAQueW3M#*# z?ooD{L~blBB7{g^#*wT}n@C@>oIL8~%erf^hzhce@D*{+I`QRWe!mExss(54Vw0YX zK7AsryFomzp|#}1=BZ$2S9YNmBOE8$8mEzg(E&FbS{j_Z%{F8mG8Q5@WmEsOF1STu zisjm7#nuzlyqp9lX2xXgdAdi&y$=&|Odc{_uz#G|+n8Z4$Yf>2B&K9ZdX^_#Dx?44 znq;ILZHQQm#89)!vfKKl%}s%==$4Sau8hstkus?SG_w)Svmxkgx??A+$^ z2yhq>_BgnvW#`Crp>9oXO=9UY+ne#S`j7MaGI?$*t#yl3aV&Cm?tj18Q}1<#Vg-3bH3Ua>>Zxjc-ybm{D$bE znc3H1Y|UVDvsivcf4f`wjAtHc%A1i6#dcE1bzzuI2+E$t_w&0m)-TBa8^AKv6rdNN zCue-JX0iU{y2oV(m!y|=c^|SDj!zSjc^Ph}o$jvNG>ig`swNxx6If-fk1Klu?pdj`i4n#{)vhUh8D5tP>kKw98&6D2 zj!rZaPFoPvF~5A#Y%XF=ZDyp6^NQumb1(iJL|$ENTFUBcmkk+l>t49Wa1i@X+^kfj z2$Ed+)AU%d@@4($zJGy!s_C(N@k=S@5XE|d|7;9LaEo-?3D;9(ehKowZE~?}(jfGj z91-jb6PgTlpZG3Vs>0s=)Me68uSyOdH)L*$O6Vy+vJGr-e2&U$Blr-Y*IklbDdC|fvWFE-&bvp5UkyPB1-%XEa z1DgYzGuf!@SwF{zokGJNk7D6rYkmC<-(TSAMDjbSf22bda>gDlrIyNaSlNqP`V{ed z=1K(^2E2DtS_qr@tElJ^4FAaRn1&{UaQ&Yo`h)OW$YVO>{{E^ANT0axEntLA13A!MQM%t6wbFI;tZC zhd7BJSU)LzIL^bRY*rUtZ_eq385D^A*(@h%%gd2g7QE!zsomBn@_4nQRO!N+y=yaV zjLf&dCsq3o_lLdryp)zqtLU6KRtqWHVZN~CzpvTitAMD-UELA!ms5zgJGMuHXJ*Qg?W}fa^A21*vH_bM&Xkw zEc0`iU}SRTYhX~y)BDSlD?KX(9a44ewY1jV6U=WD1lH`HTCB_Ln9?-4Tl9B;-+3zT ziK&Qwk=%5UAn9yG0q^}>RAnXFG)X%o^BQ*USH;`s_!pKUT;nMWJFN8HOWxX@1A3S) zdwNN4lQ6;AJD$G|XH7u30 z_+$P)HQv$uq>5*p_q+XJNguiD!FxH?!z|rX1Bgn5WtF*>2u3DsU4K)ko3oUFcl#fB{Kg0Tg;fG#7>5igeu<(=L~XRQ8#Q+8%aEciOL2Brd|gGPIisy|7FS#^ zCr>S<6&NG&P~&O~^Xj4CzO?3MPiq4e7b7FiLP8hwrN;4BstFsS7OlH!JTqP$38bt< z^<`1n!S_y%$2adKvZRCKY0hpGJINc$Vlvkqj%Lr~T?xjGw8d9OOd88=q@BvhLs9bt zKvYF>$*W|HI2h?WMe!sfBeH|Q{B!ukygH99H1Kn+yjU4;apk7qqxy~8lL1%xib8Dy z>?kLKov?0|_4Wp^`(WLeB)DN5T66EX$fh-dOiGHtpMtoemBRATGN(Z{Zt-Wg_^aJ# z2JciTA)91^TN$+f>O(7gHIE+0w%7iB^faPDHhR7JKMgm?#>VcSrMmYOS-PiR92d&r zn&z>ee~|?&Km3_P8S#s8P?~m!X_GmcW9xMqe#27}sZ!2mj?UVZS`tTU|_Y^1PlIko% z;ODv5=b}}Ajv>qia!(RuBWNe~dTZC_dITBRo!w`+d~DS#=S~;m=7aRmi$rspc{mZ^ z$j6EF>aVz5m=11)f{#-DnJ;h%Gc&sRTC7Lx}w8ly(LuNg7#Y(zRP-Z~h zBr|l&5it)DoUGS-PD&^&EE|*yKO8(9J@+)7JDGp3y5{(lRoppDwv5`cj13 zg3&djo@0wz&NL2q@_=_uXU3PO_4LNe+}bY`1=@@c2_k?1oAD(2XLhMnNJ!AUnJLlS zjo^}ohB_bIGZB>Fj8_K?&klD;k0p`WT{vbVUCl;w*_ilNCX&o=dV%&Tzeu$o1LU{f znKO;ojxRp>r3~^Mx8;-R{VbzSYuuCzWT>~k`mwc@vzU7oiRz@Wb;MaTX3@ungU6{$ zmMkX^MwZ2LQRpm^F~`Sw&Bq~U5+1Q$pH5k4ySNqHzH8xCRHZY@wi0*0k@Hy`w(!Lr z3qa_83j*V~SCYy8~qTm?Q@M&kBg3y0=G2x$$?-n`OAvw7BEgON09Dw{{sPN=ikQ?w6 zR#tWKiIUxjZRgqT=5=I|CgAZ@h9t2!bK0!iaBELqc!m`JfXL$As zw++LI4IT+MqAMuNYx@5=805tl_0P#+>oYpU*2unn$@AoWw}67lJlj0b@vJp-bG=sCq;nLF?<`nJGqxNICplHQL8C*lvbN$0%NLY^(DJ6i*r% z{(zu`z8?EZ^_U$)SDE5C>*F`XWr{DT;>Tuw91}${{F~3|CfYhZ*LZsAUn5Tl9G_%w zQEZtmeWYP8fVU1C)th|%E?F!{!Hw7LrlUK z$W;x=%W5P9k&m44%h3p<9;skyYp!P<8s*WN&8^w5%V*xz)sfe8CXB6Jb)RA2Y-QP{ zTzyKej>-S?1K}flX=P#XG)*b@k=}|PHrbsm3SLF38W%IhbpPmmMQAR1f9G0vHArfZ z5Az0g(X>zFSE`w%Qr@P$<+6WkUmDA?a3~Mva0o1lcaVV-s9J7-58hF>I#;4}&+9|(Wvdpqcy$JvCok@&}Yt%<2@Vc?s9h1>JqxM&f zdiNs16e6!?wC|qEaohI*s|=jsp1$B!>lCh^jfX$6_pz>@4lI4e$Oq>|m-vIbjo!Q0 z&M!Ycd>G>G=Utfx(?;IKx(BhI=lesu}XGXebSf>aP>EFt?F>K=yVHFd!LZM z7B4Y5^NNuD_Cp%7CmMe`C{@U=jj%&?uQr@c;edYO-SC6LCH(l&n4*qIWA{h(f5rgB z;t8}e&Arde{;E=f@v{HO!#Dn)*@EFJbc*B7U*E>7{3HIuzi6Kb>RTqgE5^3;MxU$l zifgOY9~~&L>qsLa5@Kr=cCiy)^Km9i{0l8lD58{8<=sWjgP>?G6>~wp97I0)J2kCK za%8+{Yr;?3uVJk0k37l88=O?B%aXNYI~Vh^Ujro@z00jlm>6*+LR^o1Jz_=bwhapnyJrFulWFoZl~(v`L@-zNx=*Iv ztnDtetKWg1*+3n_6*%rJ6mYBGkt)|B+mp~%EIbk!L=ahw=`z=5X7}ODhgh-RAYOgd zk!vxrd-!e(@M;OK*E}P1s)@{d8Dg&xH$3j>oh8ItZj^Thf+etSS_^ng&M05+ z5u_!$lU8|JEsIKlQF!ig6q5Ye|G?uiZ*@cZN#TR@f%kIo3wG7%e>J72^TAy~BNR9McgbS2+{?@a}IQSRwy+6Lpl?d4+%I|Ba}_6zl${PbTtiN&qFcSn9=3?;O~Y zahXw@5kR;E8e7BTMLcDd)jy5MUUsG~p1)9kkNIzb;!_NMBS$H5v0m$MB^2&JZHiju z!9ga0!Y>=cYWw?|O|gAU3H)|Re`6KpfN$%$%WgtCLz5j@tSkGBV0oL!o7szwSPYJJ(RLA)mzM1u5Y4IB< z$q~wz1?`yWv}xqaPAzQ?;9X;`KVDJo3^?33sLcJB5sWf$P--a8YyLPQ@RU~N$fhWK ze}fEE0xN^d(=w+XRenVjhdR+Nn(sGeZ|qegTtmvoE#b@`qxSS-s7@C@^sS&U}6}_SxZ18K*uaeH~@jFPO6aobAx*&a1z%Kb85N z8>T!}pP{Sgc3xup{)f_^&J)td4d}%iSK9ttEVTFvyT68=67gRWMo+YzBSGj!3wFNc ziNMY`Dmo?}Hh7cpc#fR zGk8=FzKnCxG?S!GT*;Z8F)?hbAM4$j;G>Rcyf-{D(gSw}|1)G*IcoagpszDFc<@cC zjX~bjm5k@CB1=9xg|lfp!*TjMmEa|}9kTQJy0609ml7PO=a@>SIffXPwI?g|#Nw{4&fbum%opvm6aWF`HW~ zJ-V!Sir7@qFK=3?Uri z&>m?RKW6W?APkT=yn)~T5aY^So9_48q~{dX(lcdLn@Ja}^Q;AEvyICmOl9>$7?x5F zkW}==5V9ytAw*d|#F3*$Ti8D@HuggYtEs~_U4SsTV_U|gCOFy(yt%F{oqvr@T64Z- zw_p5_kGObVjfflD#>PZ}#bUv5HB?I*)c@Oqeo>`p((W zubGY!q6LMuaD1v;Z+man&>z%S>X$sFkI3WY%xw%?|tMDO_l1Mn8!BPGSYOc34;4xny?#Z z%dn5PJ5y5yEz^?4YQ0e(D9Pr&er{S_ShlXNQy}mzMUSS|^z6;DhQ%32K{kX{NHG@- zP30%)4vnm<&Tccv_G+B*Pf;SLPN|g2^%x>({q=2(zDU154-N%yrx78&bXup$9|h+c@<*;}ao{}K1s z0a0xK|2VE*3`*%%WXYvFMWorKWGO*HI;2}96p3A$WoZyW8UaB%RXUYakdPJ-5CLiY z&MxZJtKNHGulMWo`~AV;%=0{+IdjfD&zadXG+nrlZs?nwSfU`m_O!a?EzQqDtx(^B*&~Sy^8uG&UN7B%iaLif-vW1a_9Vw-W}ClSJ%FB^tQII zsUm0K>Uz`69QN=4*qHM4I+cA*WrDc1#c_cZwaSc5W5-8_?#2%1^PK`#?bL-!hi>0` zk>O#U9F+Cq>t9VXr+<`UT`W$vA# zH=cdh>M)wrA4TqF+8)iMbbkgYr47gZ%5Oi~xfk=>zniBbozHC%Gyp$lA3D5ICGX_6J|7bz!|P)jZ1xui+3H=B{-^2_g(PhSPDDPU0SCC*#^8ww`)^Axf~2 zoo?ZOQJuWl2>8;ZdDBP$zA&wKK)H!O{%7cTy;l2GU9Q24x-p?(BPBDiyjJtDjEh~r zcO)*B^WCyyC%KDT=4?o{80;fGO;-jcYXMQQ%2-b5E1hqT)z7&m_OJ~1w2Rw=$n$8P zv+tIro{gRVMozo258G6oda76Y;j{_4`{Lc0N;uA|Xe z(ZfUwG=(V*UXFNhQ{r~E=tjB<$hx-JOC&wdnD};!2~T``&c5Zu3%d_J)UJr7 zJ*ZR%TUmUUIUi*x&={?8VRZ!E@pSX$(7i+8Dw^YKE2g4gv3b$k&?E?yhGvNGGCOZU zTK&SF+nm9DzBDoSi1&_hGhVUv5$0zH9F2#U2JBRBq|eyTS9n<9S2t}Q zWYUMZm5pn-p)+m?Oh;aQ2NQ0i)qXv>pp{-SD7bAOZsUS*6ZW7|iQ!U@D&3O;x*%?0QJo((WJox}^i}9CGeTfPX zMxNX~{hejvw}&VVFV#2kXy+tIVlrKJyzJSsJEuJ&RHIuyR(sV8#q`gv)fT35h0UVd z=<3+Am%lqkV{hqSHl*%Y__#f;ts3_olV|>zz_^AJdjc@jNqs(+jXs?~j7T{!XK(kt zrb#~CZ&<(S`#bqRfINzt8Gu-2e(;~;MW+BV6&I&HX-hRA#-r#1QmPOTd-4piKvKb2Igqd?3-O|gX(TOxOwV%*w=9irdAol=DTUMu|8kba zy`}EB&$yWsc{83 zZ`L+vt>Mue4leBajwo+_u?$ATt84*>c`DzAn2SGCY^bSoCZP z#9ZukQvO%6f93=f(9v3*Ca?P>tnm+q{mIM<$rW!cyU7Xq&Ji(gcut6Tv#bW>Z+z1> zXUL69QCoO`b0wa--q+xcMP2dM0ka>j9i8tvt^`AScim2jg8=KzD7J z;fcIVSl;@J{sJY+k^M*)0Tn0oFxHid4eBTUrC%Xyfs`Fv3V7NPk&kQ z(*0(Q58Q$l2J1yE9r^Ocg{GU|rm-g>nozMpKJeWqqFcagaa59C$Xnuh`BPkC2V(8| zo3}2GaHQy^Rvb2TbnbsD5=IX`b1s;<@VU)~s{J!+ReS=>g#q-TmFXsmyG_a*EddK}^L5^yPtC4ktIzx( z>E4dX)-ic`WYfX&5a`ci%uQwV$O=z1nSL+zXqaP%>E??i0Q;fx6~i9 zeCW*TK>CbC)jrX@Z*~- z7`D*uVxuYqzT;x8+Txt(lk1mX2yjeU#+k1b(`-h^=MMXdZcx!%hR6CckbB9K1~(fQ zUwfp*hORt*1zFbY@e}@X_>EqC%^4noKqsM?4x)JkE@V350$^Wm>sqp3WOaNIdL~_* zCJ*r*g9l*RUn>A(fDU}>aehzT>G0rX9|hgwzJ>jD)%Nw6ju*N0;4aj!?jSyg31?_b zm3^&oKLB6Zm`blPM3ULqM2RQvbj(@|Opwf}{PGskkR^4RlGt$MUdg`1%jBrt74qal zEpO#D71K$~BtsRGo7e-n$!qigwzq3~(J@NkAV-X=@c_G>wy6E&JX=hvx|e#w(cOmr z$7Q;M16~5*w#37M^MF-SPV!41fj&;hz~CQ=B$85q06o_~GP0J5S*PKnBsPZfwNVCx zP|&zAssyZav7yDDnErw9NCKULA!cz6H}uOf1B1es8a>_VAi%Xi7@!Cv7MNXq75{~V za4z~{RR(*Cc3{KQRQs;%aAk>4l$#xUYKDGN2=fBD`7eo+MiS(kziFEyIW0<0Xw0e6 zovLPf0D3>FBcV_uM5fxu+1(ty5%R?G-w+$qE_)gOBWsg_YW}(1km&prPaGv7hSP_e zZB>PU>4t0nqy!uL-@1zSA6S*WtS)XoFXTBOm>T8^K>K7$SH+jz7?;&I>$OISq2B)xPz zS0vS7>QQOv$1O(2j>wDrQdgbQi=TZs33`5tUixm|TRrG}7RFe<4f_c44I!p!n$LHg9ZZ^0iiq@8$%srS!k z-P?!1e=V{4q!C^mc>K1Qyd?6m|Md%0J!evY)oMXk| zk=@~J=fQ`2r{%HuvtMBgg=muQ#O=s0WDDY19C&>CBrWIV*!}s~(%bp~(u)}+evvR7 zNd+!}bCN3$Mr1QxokVZyNS&eyIoH6FD+^^+)5}vsy}@F=U_AH1Q-Pk8k#sm%fr-=P zG*2%K$%)0|>A{P4>YQU)@p%r0E{c{;h{P$n?mMHo+Nb0~LiG^hJHp=B!c0!Av*}$X zjAZY=)M!D;3?+m86ozZB&XmDoA`wuS=D>ZWK742Y4Z3Jf z_AtI+^EQ4&$QYsZjdnscSV-~NL~EAetm@z#glwgMM;w`4LNlK!l}z)Bi^GDT$UqyA zl|-=1!oKDuw?OhyyqV2WSw2d`r=oanCn|&9fz_EG>wA?k{|Bn z;xswuvLLYT3N4_^A(T(1t4mt`c+mT< zi~69#*;cZoH(E#P)fk6<-{6nM_cS7Bh~?Ag(2harN@1>#TB1D2YPyEG_-yTA>%7yV5 z^ByuxNkVLnFce3N{ zn0+ji0&gxmaY{gM_N()74RNLS6zo4*c;LS@Jd?OFpqFJdDApU1(GwiH>F(U;%}wE@x!M%nmH zw7yh8&ZI#|*Dlb*YR2_0S0-JJ1qEd!LVQ=Dcl;{_z$jeljx>RN5BrWWOuX_TXz5l7{+E zqw%ISUWJvV?QVbsE9COW>ZyLlH-JnG+&>wTWj4ALkPIng(C?{)blng55%3?%`#s#! zkgbsIb@9`h*hyw0PjbLgnOL;0UdA-Ec@T6v2a)bi4Q=U0;q!W|(iffIFh#k~eQ*5l zmHpD!pW%X<$dFU5PP2ynugwd$>)W&iK3ro3wxaiySdgXf2Bnj9nDIuANALTzqvKqO zD8#P0M34akqX2q>QU+*JO+)ssQkMD`!cm1z<{Fw0XjiGzKm4QE^y~s(>-$Vl!uLSZ zVeO4i*bnQuN=RWgegZshMYE%i=daocZ68F(>*+N*65DHA_&GyTK?cUoi!VN?y)HRz zVi7ScyCngm%XknLjE2bG&DGPO6dXzc}Y(uIBw>_k@(?yM?`r z?{A%8Ki$rjTxgS?wOUPjZltKw@#;a|eg!UQpLWbJb}DU8bH|RAw(Q4h=EC8x$iD_- zs1yUpXgSc-T~+3$oWdydw>PH*@5ffgcM`FqjvO8W^V_)K<~2dXmgdzrIrv2x0}cL^N|<3Dlwq2@JfUL7*{ z7r2C&8J)>M3zTK%?Lq1Y7J~|}IcP4wge5Olcmf~oO6nBVGX^7)3PBsl-CZ-P0%_O>(027@Lma9%cNPw?V3heUV zjulZ_0xrn()Hz@Zy9!O}ove<|7WMWZ68flZolRec)7x^v+2Ai>cA| z<@Rg7r#~u-4*P~|^Sf91K0E-v%L$w~u?y;=n-<$F**gs{>w}G%F61(P$bOl!Swc(x z!J)Fs|3&+ME|OL-8bJZeid^GRw@tT~>K7bg+Th^(eRU86_%#+obrp$pxz}5yq?oMs z5$Ob*7ZvuRS@aK%G{^Xo!gTc z56H4DReq;y{^qyUJT!n|(LQJoQFk&$dC;?I{GNrn_rG!EA3gmJaO5;EF70^IOPfXe ze=zDFt^Ns+1vu+}Zy0Fj`)mhso#pk@CNu5)74@s1H`sccv9| zxGdi4q@W0aC@MZ2MeLPsQR!y=K59Sd|4W4LnPp^>;W=^bbu?tntt51fG0jG-7^a_X z?V%&V3>)f_MBoPqB2eLRjSS4LNFn^jd_qhAY5}7`uUidmpqSCOmh@}e%6-!VkS($B*-(N zj^siU%JFJS&Co}AJfwWQp=0L_BrHwLy}9 z>OS8&BX0>CDfvA6Any9<*u|8|_T|~sj7_#FPS_WKoyiD6)MC`tgDx<&|D<^I_iX6a=-@t01zb2p-n!^Fb^=DU*3E50mTNHPEV2^Z18CT{B|YfE>ZHtT}m0cUkuDiUV{jruci@zimZ}y7r&S^Vkw! z)&g8(nboRhiDEO%spX=YbrWoK)4ZGjEPW`WAR zqz zL+o9a7vjqbNNNvsLuYbJO?+Z^Uhh7wD>3+QWEHBOs}AchJt#WuP4Xg&3BC3aEhLc~ z^X^IGO8V`F&7P`DmluVSpBr(R3&0e=lJY0h zcj^Fi!7p`U(5}Q`vp0ca+DwPs*H-wQO0OC9SwBFF`9CYr9g{&jg(-gP*`GFH;*j)@ zali!QOYtZ3cMf3;Qv-rn0Tk>d?4YK9wgQN6$I$Pk{pD=@9tdcJ){$0HA?nw7%p`!N zU2OtjcfI2$PZ6Cyh5oD#dJ8%9hiwRncm(>0=_I3&4^I+Y1YzE!S95f7c0rXq>nAaa zE`IWSie3tK*Y6&waoUGd42-?fS549<)lGp<4r<(J{IbK`DTx!1f{LLS@44$j441(; z+w_s01HS7X(fTig&kvO35?J%8A%Q7Z{cR^A-i=L#==y*t!=yl{>q6ZUZiv(cZxl@s zch*!2L#=i$iW+LUVj7#Nx3r$@WDHf^58%xvcTrxz&P=XoD>7*CmX)V~UY*%Xi|LsQ zNJvnU$`pH5zOZRXM>bwnBs)g--an-zx^+MByv-X|<&xe@OAAP2V&7NbWd}k(*J*a~KI zGNRWyw-pIbLCTquV9IJN^9E3LRS+>8_y{^f?wh5}KA&~I{Vr8kBrJ@aZwlrS`nbgjoKo|RpNlX}?w z%6?B>k2ZK__Mw{<3<42Vcs8ob0HwHo7JSCG0m1k&H zWF6&A@zKSvV%nX>BOw&EX3o^%ugVs*Zb+dp-Fwc`@jhW>K%smF6u&WCF&n!FOPJg| zmHi@>KeM`B)KKqD-$w%iOBsIszIUlm*EW9+oce?o84EBIWKP}u?GRiz=q#9XlqNZd z6CyI_Pi4V`U?ysaFH-cU&af{vFZ1@ZJ5?gH*^Rv3pHbE41mPrD?#dSDo@@8iRKPLm z5%-+4w(PqZpOV{FXS2@t>PBScbNH}FJPQg^XuiwL>i!OC)hmWoDVf{on62h%r03JG zJ4_ij@5bEzdE|^gOd4)f11jzkGDQmuZsi_}x;95$!ba2RCTkLJ*M^2*Em$Ht?1)X} z@kWDo+7CKNIyPj&AXDG*`Sa^C(&eMAh*V{h=`evYQw%F*liwGMrZe23jkNDe3XD$THi zNj>Z&s};zxQ-Gf=mo+h_Tg3cPCjIPLSt5-Xu|k+eZXa#_U=s@o&dC#ZuROTQaz-he z7h>M&8O2)G+G2e7iq$M(qB?{*k}l*PLX=rKV+PdV3lD)}uxO?*#w51nbm!~BGknVX z6vgn|a0L`YSbChT=QbdX1P6Bg zh&mIKM%-S=%ojPXxtu*i*()j289SzgSx`s<+k1gmGbx9Ch)0Es2=F%#zf@>Pawe)e zK}8C=r1JB87g+d{UUj{4Q=`q>+B-?8kWOFg1$d89i-`>9r*ni(fkyUKN<;m^O%aU+ z(XkL3$?Uz1;s8n^Fw@R%;DDfMM~1`P!|UajK{!%x5=)Z<*fh01 z$8D&*2=CWWW5NgSV@!W`NhL8(5wDxY?IPudo5pi3tNRZdMD#2che7nnDeQRfVyewI zrc7%-C5JO4!Lpzz1z_nFf8yDcF}gn+Hhn|c!`md{PQBB>& zkTR)fMem8aU-k-ce`FY8i5*CQD(*hlE(#SPvhrg~!q(6q^g~`x?2sXe3wLA+w)b%; z5Y087)N=wO6Wx<*6*v{w8*J`Hh)#3%gsAt4;DcB44;5#*`#rioyyOR~of;dC&G0G| zLYc>ON9&p*rNVU>ctr+ihW{YG=5LnJlk00Drq6&7X&Iw7K^nz<>}7yWhDM*Y6LAuG zq5z*wYQICIfr`?bOZL~DjswfXtjpxo7%mO}16%A^@9 z9b0D7(_Z1Qh9!J?4&~A0q(}53&B8;n_YM=w-FE7kbM2F_gQ{F@oPA6P4nW@fr^M2@ z;Ne8qd3Wj=xEA_z>q;o#HlI2`yeG-By89Sdva-Xk#MDEi2-#cuG<=NDm%L$8mstHs zaG)tZdvc71z8WGb!%(>$vJ(@!{hTa-ibfKNOwR~qY-*v>2+J5?K#*L88H_}t(h)*! zGl~!p#|x~~jimN7px_abfs}6Ej4Y+dB1ghglNt+o=byx|v|8!Nw?3)pUK3aJni?kU z%r{9|rwCjiUVY0mhV3QefSeglp4*O%s#MN-!>Y;n)?Ns2ahN`G6pGhp7%wty)A<>q4vIicGih`W}{zRs! z3?(8iT#GXJnbHL|xkJf1`sNAUP20 zagk>vmn4l$4RHvcd0c4NCpBeTAYqrCDa1vRWg&xF%Dv1^eThp;-dm5f-X5`deV(kX zQ2#p3M0}HkLaOia@OO$+^!~A3R%%dnxjAWPI)cW zobNi|eEYV4KBiFB1-4L04T=1Wo-oY~m;(qKl+X8l|@HE-|0|2%@JJNb?CRm0iGF?MTo1G^EbXcm*!( zt7v3*NhQm%S0)?Gs&snG?9fUe?KKKK_tKH`(%6(*Ih6iEW)|?RW;uA>y9yJyqoU0e zm2^vr^~@W^t?tu1Mva107v9#?gn%ie1OhWD2@@z2#`$AUfVtm0lZ#uknkV}iGpAX| zOrtXUf~qP5kVbv-0Q#Gc z)^scV;hj$Sx~%;1IKsK$@IN@|yGdzI>B%Ru3gcBq@c%8`4|sg~Uo-!Qx{lyy&>HIv z!wxT_cx4|v2J`6{GByCuS`_|;wf~jEqga4KOR2E=uA|yw4{HpP-ehlG{NDud|6tPp zTZhk1r$t8Qv#pP)XZdciH=k)@k)tb6y zUk#P=HzuJcNTUeAprv?l2+_nVcBJimMG354C6f<`dS-Gf+M+M(IH+fyd&yi|5PmfU zPw#r6Z0+c$q1ZO0?iA1)CYdE3JX)u{`k(&!ux*F#`Qf)$;FumryEqTW?ZFd@&6`_n zITYDTb`PYUbb0^kPjXIqFroa3OogcFej~S)xAGX{t0Wf%IWM0yow>_@_xE6wbxLD( zxF=b;-T%NCAKTF>cZPJsb4S)9emCG3fByXMp7<<_24)~3;y%UIBJVp7TJGidg&8)T ziEtiK6nf=r!*;24;CFvy5mQ8C!Sfs~A?pp~AA?`;Y~uVM{?1tSnjeg~;zJmao(Co_ z2J(keY}Jjb-M!c$E5(+tPJxUi;4do6vzbP%A~W}YbDFM91q-gSB8ctOy}b3n;e`g9 zr{O3$Q&<|r1tnh;T}_9*BTh4>+~?S)ZdOms1TVnK4Mwiq6H$tE2Mcd@yn3l_AE|qR zCARcZA|qXM4m`HX-~U9BlxeV^>pPg){@`s*QYb|=1HP1*(WcOhsn%hw z)?sd9Ep4q^fVbGX;%we~p|HU%7U46|ogcaow@Tn!R9O&EfA{h_73~o9Dp`@Vuk&ml z`WMxNEA}@mQr>!<#!xKc4VKl#jl>77t{m3ymWdwk*m5b|kk1zx6Xzg0Xh>41Hr@W< z22p zZS^da8a(bDcsJW{Y+!H2zdU?A;Zf#Ey_d=vw=EGL;e7j#ORImp6-5)GX`XLcCoEyF zzyZVl$XJgZ%knVl}K)3dN(8MnMYBHPd0danx{m|zxVn`!Wo_^c6=j;Rz;s-qRp zVE?#Qds@o1gC3g^`oUK#eZ5?7cFG)6cn_w|ZElf1u6W5jZX&(g=sh*~&VI$K+VJh6 zv3U1;y!G01cQ6E{b=ID+?S-8#= zkldf>un3Z9ej!}qx$E(7Blzjdthk+7!BAsdNj!#Ub~#?xPU%c0cZm(i4Zva8RN;O z+9A49cdztbcr@>_QL6gs(C7;KGIEiYD$h=*5h3zNWf|pk=FytIa&qY1()NjHN@`IL z-pJJ^qzvpaBo8A zcRtVL8mV8#dDGy8`Gp0C33N73Kbuyg7O&7KaBXM4%-C=M(jEm{P19#>1gcc~mpm6@ zB7iYU3=C#3FUfD|%E-WSRtd=nhQZlHb|g75%!ORq6mRg0PeAPfIsq?VWalwn zDUFAqWGB*-tvQH0(CdQ6f-P3BY$M$xHpsUalK$|TARc&57)7bC?$H6uuyd%3%BOb@ zcps)ioy=uw-8MRTn_f$99(S0kL`c}6Z7{0p`6szYVLjv}0;WHKC7%2{M!ruDuYkQ{ z76n>-J1FR71m7jn=}yo6M(BTJ!LPya4dFnhUEYXm&k6l2L+(5-e`7jSF-orS;sZpn08tCX*q{hu7CV4P@TKWFY;-V@5>gM566fWmk0+undAk5HE_9q|P?c zjkwYF8E(XI*tSbq@S3_9j<7eV1zb+S=D&c28ooT4(5sXqp}(uJ8i-9r6c{^dpOF** zjTVPSA5L+-G|1%)c{!zS3S$(iOp2>~^oS8mn^{ny;Z8tG3KAk{MG9&B3llU7a$F)z|Crx?At0Y`Fh|;Sr7ir*u z+eKYI{3HFmrq9Q>+$lyYOSg|Z+4MMjDG1?{Efk-%bc8yDL!MmCR6M|UX!zxV|3Ry-bbBSM}(*IZr2DOz;LP$@@jmL<`yjB7+)Tug?}!lR$# zEcxbHz40I&x;_7?RJ!DujMeU^)`vk1X%Vs^fruIfD+YaBU2FcHdSo4|>d@sO*7IdI zyTLlba_Q1mj`F2Xp^c2E_+{s&5^u!|4RnMtu|aumot26EbS3Mh z`zIn$y@dyV1+H#Mll!W#vIw@yO$(?WFGF%|mgH)&6calV3Z3Q4ew8tCMSs>o3%!is zzYw+h!}MH~Zcuxb^AGHy3o8|m$(g$5g_`_QjMOzOVdtVC{HYs!+MNiVW&y;evtVFzgbBEGf1oMS-}37IVRw>zOmm zY>5M?Qbpfa{F&3p?Ib#u$BK8ZLO>5fPBkG~*_*mKmHCE{@a^Zp)=3sRi6ORHZ@+ zDz69As-@go3h?`g%LOd$B2$XVwWN=0^4Puq1bRoH}ZIWaT_FMxC!nm$6$ zwHNq>+u-%!0}Gdcxcom;RS>{w4TKNh*G!wPLTb4?nPD!x)_y+A!?j`Yi3@4|suvO6DDK zVdSx;Og$Jqb<-Kg+Jdrj@?vp97{-Nvsrv7n@*67SHe*P&AP7jgB;&DDeNx0h#^94taELWn0HVTNW6k)aUIG;i35GofeZp#FH)^uak2z6;# z#3?yW-D?)&*juFh`PemjYl1lTiS69Ic1E%b^cOor0vSrz55wa){mm|`vtKAp0iKKU zdy3+p3eR22kdPYd^fmjsr^cS3)ogP^p{7a=2`$xIlICShnlU@n z5Xapjm{&>Qn3ei@E16O@c*mXL%(?Kn!DfaWw*hr?qW`-;WFIHno)^OB1Tp+Ck z)ipI1m_bSD4oE}Bcl=o5y%t_D=ZeuIdeUVcqquOU8AW3it0&VL3PcfZ`XJ# z6{>^F?>$AvM$D*ORLOO!R2pSg=E!JAnvM^U`c6MEFibyBt1bb046Y`Auh(L~eRM1O+NU^tJnykF z*qLwf=Yl4omy8X;d87KRlKk0);5 zY1)vTQ0c0-kH9guMyLbJOmw}4S2F?_z;qBC1(+0)Bocja5fa({Q75?)ugG_v;tjzY zGM;jF6Ps%-uZ#W5-(&HsWvy{Ei?F9=nzDMZSL{Die`eM&hxPebhNM~D9L{}Q4xsO7 ze9*Dq@yqt>^z9XZQKIXZ9^z9#u+8emI2-xa`awagW&(Q^~m&}m;iDUI;Dwo#sc97 z$hZw#ULG1knyR<1l;*xRR4=TKf6i4RrKsyUqrRhhn0o&yCfn)iNe~~%azVC(!`6w1 z`)!CHiB2ajWT^;3*2J%WSPJ$U=o=VLkdFN$dJW#HgK!zd>lG8QiBfr$$=@Tn#GwrXW%);xrvgeEbkmBv*WM46P^ zEi*0dsm{o_9f$#49JLx0g$EX9iV!Q6Eh?c>Cx^+q*EmVf4XTe{4FQGKzi}!_O5=Rz z+?5O3XVA{Azt~GiCk>t<&a&XoQfcr#IhnSlDyL}v#(A#AHq(}GAXnR?UCup|Q#y&R zNt`e}tMgI8b0ra0br*Oh_^gspXY7@anq=|WOoR`5sggPp4=E+0Y9_k0w+~yLs>*L$ zy_#ckJ=Af-JRiBc&^z9DFoACX-;5J$;kVCYo-X@^|4&^j!>2`c>4)@IFXYQrr~8QM z>S7O)u8n=?2?y(`fxv-E%v|Q_#=BK3pVh!38PCp?X)2*X;mY;~L@9 z8=|A~jwu|3OPuZznh*8WB+eIbU@Zo3ue=YL+m6EtTFhN#A;-exqZd-nFLRB37sebYy@~cF^FfN5;rX#BuL+{btLW!A zC*A_P(W|Qe(f_b6bFZGN4y`@ZazaD3^(F+J@TT&>eb5Q-`Im7`^2hHiIi7EEDV2Xz zH7>3d<|?!AR$5A&$2MJ4g3gIpY4fscA6%y-PirDfIq4)eeM{yxBmOMmdz27LzU1n- z+J$!MzmhJuOE!E|Bk^6Jr40KSd4{nNeEg)1kMK`W>JIv0mOn`dSM=F4!%tITpTv>L$P^y~px(&4pN}Db27kw&YWm>Cj&MkO z!zcL`qkF=_ejrDo?uXOV9n9ZL|Bm6C4Tzs?_zjBC-@!gAHyyM`-%zA5u6Ft7fczpx z|Aur&v6~~)Q%|z3w0(=RulFQ~aFbz*hL_i7bt&}E$b4s2s%hFosOR~i=y$nbt$SSC z%T|`G2|=y{yR5+)7hfETJ-j8e_fIG9C&P5dgIqJ(8S*8g)St&|w5QJY$@=b~xJ@Wd z9lvwqOL5F$m@8|HyrtA!!O->N=VJ$IQGRQuhDPqITiO|U%5L_VJ0(}l3Q!(;roX;^l)VAd-pUL9hoC0l z2H;b)xosu4v9xOQ&Gu>+_tFN#KqQcZfbSQg)0h8$FwSj%c~Gf$PRf@Mlbvbr?(;wN zblr-$5&kX&{m8QUWdFljUBjZrZxOvtC#=heo=eAP)b zyOaKpoZrKKr$Vv(_~&Ce&u40Wq4`h#cDJYgQs)($V$0iNm8&b;`~}aIs@DE&>z^D= z%Xd&9x{6ti1`E+oyc{e+{pkb?is4{Ws3H=T{Yy{&Y9i?Ku@{ZW1#g+TZual{6$b!D z{3b--tNp8#A7c6cGoW;R0YMP#x+5DeR1YJ8TdL1c95~O4R1!DyW`d}*SV6D zwA!8emmm&Vj3PeGmp5&q2y+u0Z)9rZ0i9pNr{p!}QFRI1)+}cEamci-AWau@JN_Fw$w+Cp%Iu&o5sNl{SoIYry2(puMoF^Jl)__`EG-81V?8z|7 z7itgpNOCZEjwnT7rL|=cGxgupB^BwDL=9)S$1qQ;J`x>l&ZLGWHdIJLe57L4>o~Ek zP?e1#J`_pPmI9Bu2W~2ahD7WHOEq#Vr=TC&t9Ki^{IG(7nQ+mZmf9`ew-7ur90w#^ zBrmQQY@Vb-=`NZ|l5at#@oW3CMEeky+JrI3?``<)ksyh*86b3ogeqWxSMZ`#0#BNB z3;vJUg`N;O3s$Aoekd_qFtf4bWM`pDGW*sdvaa!+Xm$1cYwF=Gg;P)|4JT<8%+J?UsB_E8K zszNU_0j@e^iJsUxdL0AetUV?kxdXi3a{2q$TmDG%zIMo#ble7*Va4H?JmB)IWJ517 z$MJag|9BSB?;JRs$6ah)OMZ=O?u=gK3BOafI807+m3u+(epMo)lPpwkO-pK4%tkY^ zO63iyDW`gQvkoq5x%gMa$Eh1_x)Q{{Mm(c(5hMu@ds69pEuYRQrJS`*FWWY_gdF_F z_)$QI+w1=sF%b9-s*zg*Vw~^L;Tc(ABeQ{ayUN=7@6Gem*PtFLeOnc4@^&UT$^Nl=7`n$->&8Dfz;crN<1pjI5_md_rrI9P5kqNFaaH;yU}O z8f-VwGdA+Z)P%1Co80J&&%=QOF@o2Djk0OD#$-(#@aqKSbT(421B(B*WBL;g9kZ&j@a^vgjXY*L9 zuQJL!ZgM%Pg;+Yj0V&v}AY$y$p_9@~hYS2){rGn=NFsUdutDl!gu};{q}=d2%LL0? z*vcj^hJWQI>)`V;5?B;LL5ruC@`ZuiAdkEDO%Ch=j2^G9r9QYYxKr4S6@Jd*(e1}= z^h^c9SJ#r^A7oLQtc-Y%xHxQ-ta~Ca=Z6Tse=o(@S4EI>BS=$@tSCie3hTpT7Gu_G z=q=?sKkURDaVi1MjK1y@Fjf$6*x7Y!-Fs&evDjOM1cmHk9Ax*`MAI~{%VpvndvE9R z_`#ez2{SJ&)B18AU@Jk9N0iDrYYKexG2a zD9~5%_ZtoW0QtK6@Q*~Cmf+C<7CVyKrh%yBImLV)ys!rhmFaBV#_dcHUTl<&A7dN? z6ZQoym3D#bD!9!bH>e3-%RLo5{uuv`rL|asy324z0bLboWgKOPvdr8K>SOIT)9Qgz z9IwG)&sC_@RVeidhaVn$ed>q>&HvW}Wmc=Jxn%|ouT$It9e!^9$|yrJR@xWh&S-&*aQ=iVXhlJa}?bUSigJrdJ|* zvf3ly**pq4LVqI)If6T=4&74m=|g=!D$0jZHC6+qYMf6?*v5j(whbuCThJhBw^MZ) zq@vOJ?F7%9aKdk14Zep~Y+_8y?H1UVCDWtawaQes=v@T#>xGifk|5+`)j`ay6;X=yc zDvIN*T2R=nvsEPSU)nGoG4tQ4jCGLQ%n zwg)Q$5sIg>7VNT$9ZGPneLlw4g56Khmc||>PIwZ>FAu7AY($HD;!OG<)XWcy&DU{l z;PF{igGLr0>^5Z1WZ6J$@G)pP9cRuZ7hLZ0G2Iu_8;Xe#9VugjhYqtZYezHH9Bdkv zy&{-R#wt&ECSqlpRRyF~#TY8~g{2#irOOcGkgQtPA?zrId$KsJvb8#hbBPzF2G2p^ zO5ou?ZjIsC6t>y*d)hxIk0=dj*}k(*{c)wW%~99u-Oh5rYQ%=bE6}*gsltYPyHy)t z|EM|716#2iPjvkHdk(#m|n zmHCfZE7<9V+vGwHR3?r85-M8cXlk%T1H651(g+YCq%|6BBb>34}SwMcu?@!G-^ix3>VRqIv(u58cv|l7}u)I;7)J(jbUPNp~n964FQ=I;BHe z8U&OMLApayKtw@W!2g~D7|-|l{(tX#UBByvy>@2q`P`rTp558m+1WX>QqK%t(so#W z&;C?HvU47EvZCMfHo4&(cXREvB9_8-5dR6uhp2jI#o+oP?3nZR^f*$BqlN{h8q$7!-KEeJ*_VM;x(}e?kt@sZU^L`11CxAH%lNcxn zc|PDi4U(yw(lJ53I0%WUrg&xvL=p5D@lXtkvkXGg*g6J*__>r(9eogo#gIWjcQiOR z*4YOH;&`@*V|jNpr;%Afl^}iHyi^ij8*h`+T%{1dV9@!Nf@=RGqxG(Sycfm&GgV)`0WeaN4Yh(svgM)Y2NIs>6mewx=Y+BHf6hsCyVwbr(vYm^2Z z99qxd0w4jC!ve?T*<6J=#XW*r-rJI|OpX|SX90-N=sb`-1K*gCRP3dQ6qM7}d8n1CRoxhy$&h@T_1=&AA4L?D5zlx3g_^@~W{fxkS>ip##Oq>IV_ef5> zz0O}Qc;_E3Y@8kbowy^Gb{@lTo2e<-67EQ@dFl?lSay z##ySChCeu8E0t7_&1F#@sti<*EtDK&zzD%(MUD8v)!KAa1;|lAJklX_UfE^`Ba;d1 z+dPiBEDGu|0ptN{o`vsTRtP+l6lBk^a>izMZnQ54FGjyo>@Zd zh8DhQ!n6Q03fcwR!>4SMz7P-PSS6RD8MAF-DqU{a%z)buMy5SRxTBm|u!f3&a zOa@FjVr8BNqR01DbPB;xV1_HO$)BUPXw<4hz|k8bZouywl7U&cV5q2Mk#J4nph3;PSgcT%Fs7cPp#9o0&Ut=g;LvjIt{vA33&@~Pl<<=` zV&mvJwK;h>uJ7A~Jgg(d8?(C<&zKvm89X7pIfu2>#z&-9_)>kI;L4$ZH(kJTW3(-! zhYr&sZs*D!rvWrr_2yYm%RcIOynMH{td{vr;MU$dZw!;hbSe7|62*3T3U-vbL@0Y` zl*O)W9wSC0Nj8>yg#K}}ly~9wV$G8;=LrT~-n8A=YRhpppzAj8v^%6 z>e8G0+)QOy?n>5^Ibe)z%kZaVP6nOV&B$ccVt?%f%QgsNJH=%W;X+BbTLG)LOp|Rp zFCmI`9xtkypLN3V9D_aR)BZvG63-Sd2{1GsNBn~@){IoQRtijSVHF0v+U>pdBAdDQ zx{<+CZ&OughRoMMQa?-6hAke$gJAns5SA3fty_pfwFH4BC(?$&4Ll@iGCp4wce93_ z^|eUUn~>@H1z*>@NOgFGqdlK|Nx%NzHYwbmL1oh~ki-yy7mP$Xi>(AP_`&VQ+@v8} z+=-*d*6Xl%D4Wii-_lvoGak!mWp1hvx|xV;n{FzFCa08EE0&Y{tLh3$vh*-LW+l=W z7cqwNf)36IDIMyW*t+K}IVRHQh@%Nh1_Ti7=8Hks8U^t<%~FZ%Ec0$xhTbEZn4?6Z~# z3p#V$j3^s|Ey6-wAc>4h+F#O3WHC~g)c5OYq#H$IX9fQ768oxTpyLExUztdyU(%Zl z{_&AVFKI3wY#wC{cCqSd^6C@l?*WUxP=%#=!4`YLm29P;6Ujy34ETY(eH;F~)bBX( ziyY9^u+Eh&GFJRTJ@kL_PiXk&!Od5M=2_`a@%+FlAq0NczvP>RFu-#3gW+-oh-|_G7#dt1{O+=gLq=;P?A(GN_ zMNu75D z=A6a)UhILwoQ*+AtkQ$Eo_h~tvWUU4vg>p?b}y~8Y@|2h3PAFf%wug&{7rZ37*}6a zd)fB*-!EW~%?O+mZJRaND=b!**(7QiHdf=mp}wIJ#(~#FrMs%f;Y1A_EzA7ae{`mt zI6WXuJDZ=oD|c3g5LMn*G09su81(RP#M2dJtbDXeaCVzV5ntrg8qLQW?))O`q!~ei z$ue|pcx@eZ_`XptMynS3*qtt2e6gx9!inp_`&E9BAr7KI(dSNROd0AqqdN341wOsG z+sQ#UWNF{h_3%A7P3%kz5Qfwpo9*rfy~58mX}VvNfA*}5-KhlgVl5(6KajXDLgpf0 zvb##xdw2(#b`&a@bZqs7Ou_wc_}Gym#;#{bci6zB8*0)R}*~)JN+ccHy&npmP^B{1D#jo z-bJk-q(z}HtI8-&X*>lrKRpI`u3z*q%+RFcxTSkLB#azu-(IeM0}H~%Lc?-r2@YmQ zYdG%aWa&_nmGE(Zpr68vv`h4wtVhf2s((a0RTjL~k9Xf{3?{6q z?bfS?N0a9*2cdD(9oHLMNAEv@_k{Qfd6G~=7#P1$~*;ZzYQjKTA~(nAlGeY}pF+93s>Qd1oBLAM?;ySy@dv#MdLI%`{k=(O)%JjdVAcRgzT`@2iYP0xwR&XFXS* z(SpJ^<)H!?JUp3?mGw$wt9qrzGqqopL8)^;wJtS#4w;7~;0DiD#tpjNu?`(s3F^0D zUv6C3@2-qyFi)O3qPQcLs^fm|s8+GgVQMc@8cz=8KlDsO+g7?@dOyVOhY8-I=B@n3 z_taaUbhmQHF2P~3viHr=eVqXh_8q6i9g5u!G?O)f;yX%D$6E))Dgi?=wBPl`K;~WF zL%*_#UEg?ojY6d8!^1@jW3R-$v^&8<*Pdsri{Lv-NoB0V<4hi^QWf7RwW;~;SR0vB zPo1pX>yE1Iv0=J_CNb#p=T3ZwNgr=Gb>>a+sebM(4(81+Hf%33$1|i8v_0H)>Orv# zZ}D^AGo*7LZ3x(`8s)aUwN$zv+SFTGj6zIXf>a1U5sAIBm&;-kxr^12hs(xrmB5g4 z`rT`3LM|I4xmarCCKL+#j%wb+dq|C+m(V5gGO-vM!qjeCex!R`XddO=B1Z5`vFG zE?JABK>ut2_xhL%mrJr5Cx_+rA)mJonGji^n6`o|3wKpYviMjz1Wgyz>3@O{fmTC! zJjAjz?sY+`f-^>D=-VG;Y+1zG7B?BO6?>V(Gm9KBW?u|`@VG~4)o`7vyl7_px$>lf zb!?&*yZC*T70;NWDnZrkz&%>CD1ilZ?42JN{z#R+2}cyx%_gBEpX7RMxSo1vp0}XB z1#VJKA1RUtxaQN7w|;^?s<|`jXdLI!PaDH029(F&d6{%wRfW*U7fXv&srg2T6m|aeO`&+)Oz)}!~QiS@LOrVdS50q)L2bKec zh=q3yZ+m@%)=G!;N+z*$5s7%Nf49%>Z;s?U5t+G&2CW%p1~Yd6eNp$?p|u6M6fY{< zJyw*moB(~lMJAew^1fU3Xw|Z-4*qf74$cE|Dgu=D6_WphyW91>@H>s(-y<(Q^1)kjf<$P z$ac_gZ!X+*uN|B_-NH^Bc3;{OljA+wOCazeS@{++2S0B6RH)gp125!cFEPPAKsu^&YpGgjEHm` z+7=RRZJX7~-QdS(lFea(wE1(y9;4JuS$Z36F6x`+K!2bZem=j}vfouR#pV8(df$UX ztn^ghHogy2dvlrLefvHPpb-^|m z>!NLB#$yL-D5%?|Q?=)o#7P?4f0ViPptU%oAGuDiD0V?LQ{)q~v+4bSdcMO4c7@1M`9Ay$l14=dO9(p^?DDV%M{7Yo*)O^^|(3ch?gG z8FjF+_d`d6LEAePawxJJeR*b^`?q>*_{|~SDGeC4Yg$L@uwh?xBW=02cIY^D;e2-7 z+JOhU9?8YM=!yR7gQuK-ne+hd=wk^O1aghLi_W?^nF;|TSnI3qqUnCu1Ln11?I+ds zW1Wz#t3KLO@?aop@O7Pz^J)P}OGHmU*bo`(GanJ{cj&+pk*NMXExO+XqF}2y;!7M0`=bA4=ITIWpowY{l)8+`naitrIhTU6Da;jr{}#0Sz#E-M|_fS)4_|KJoopGbE&tf%qUft-?lgcjGYdXlc0rCMiRZ zD=&swTd`>hoqB4FjMnWer#KXzaA}jOHHc(_G+u!WCom-a&F9N|K4EHZ*f8r#4N0@u z;7^d@^M_WSdk?FBg5sEg>w$c10~wD#aL6R;NZf6W;>LfDU$1&j{{;2M8gMR#TdnG? zeDz0r&v$#JuH=rx(}UJ?80GyiFPeK@MP+RB6V#hOL2XMLkFl+FSQevAqR)1uy-a{9 z>F511k4e^}T#;_?`tnEq1o;D6k-M44=-xj;kJ*l`DI$*>#egph{RH{CGzRh(#aRcD zNj}_tkW3LP^;w{dexc>r*^k5RWv_5UTp&J0tF%WzUEORB7k>pLt928w;4D)(FW@1U z5pQ9DjCHr?Axt*GWOxCaIi`#M$YM00A^&>iB5Pw;2d?$wU~g1q=qbykctQA51IG9x z5+c}0lyk%Z>)BHSO9WyAs|_N8kbog#JObEF^B)_{tqR%7)qeHQx9~kZe19PU_980gr(b|qeiaf#DatiH;vfVN!Zi@$7jY1} z83Ett8YCL;tK17S>r&>g%3$6$kFu--zgP2mP& z2$aKy?KMOt*j;Djb@YPrR=Kw!8oIm9^|4Jh|p4}xst8xQ?4=Vsl6NL1T*oPUUaVI z0V^Q~gDR~MK6;=r=9NE?5HM6seA z6xahBeKfXM<3Y4Iz(u+MJApqzVn0DA&9Dft4+wllFZfKobyW*3Oa^EahGAb@~ zeEJTD>;{Cb#|Oayg!8!q9AkhC)dxe(k)gIWFqhEKVbJft@Hzb)HBl*E$8daiif!$h za?OZs%@U+$^#X4I(AVz$!mB4kYGOe+7x;*qAY4=!D+o;-ByfROf+cx=07sj40O(fkOlcy({!Rc&p%z8E}r6b|G}Yi+#ICLpjH= z^ZU!66tlP*pfQH{$Dywzeck)J@gu4sqge<6@W1^jtod?LfdZJpA(8*UBiG!c{RjRL z;3B-j1qgtX{ww~sKQMgsYlvyh2kzar$N`8E_Y^~X4@0FVxV(oUv(~F>p&zPf8~e-u zZ22ZZiJQRzee5D$ck3EZhbAnXhf>_nJ71QCe?jTq}MJP{z(9ENZ~<;$Upi+&KA z`3)}K4RD+#I2Hnq56OvF07D-nE&ak<*M6xP^Z{YO>JJE14npG*$1Z&fs}G1k8u)t) z0#P7W{leSKV$>vouJAWO$j?C6K;kew;1%reEpRb8?)}0~lEdoHANU9muI@kaSikWn zKzaYd2ZC^){114bhxZe7&~nNJZ#^b{g1!M)|4s_f*tB{71pRFN(Q^0 z)d73Gd^6?;lQ)3tR(8f@XvT(O#@3g@wj9Vwv3;!%)bW+HU*-8oVO#Uo6e^vQ0@O>O z-BhWAiaSFg^sga6C1V1bPXHNJN%;%^;Y<{$uTXja=qVtJ2daWK3~N)krXh?_2qFQ5 zP90GBqrP?&8(q{VpB?~D^mlzlCb(>mcz*HMK$ZUk5AgHv!aVsG{A-|8Kx2&VPxTHA z6~76^qCk`fLFLIn09YO*4WWVc0mOgv!|E z2M7_6fB6H$F9Ib3!*A6*P0r}CK;sFT2Q52@71x>gv;Gv$z0dy;-AKNiY7u;T1vdrAb{Dy1`tCRUZt zxrJF3GqTK~H3N=>{*%hP{t*;;Vh`5%DZ*kBOkbp3|-!%aOC z*Q%SoQTc=8y~d2=*;bGdCN1yb?WVr^xTw@V@lUCNHA5vJ)_1It5I5b>x0s@a9ah z2T$oMwrSYp4cjuV9Y!8ZD@$>nVk)}Ue5O^cjSF@ti35ZA3gRXwXM3yJw4#m8vd|fS z+#P+H&|r_RPm58{Pt2(S(dCP?8zZ!BPmU?HZ5+(ac?D^E?y%IKr>)!*Q!UwOIUVOe zigAOTgTG$DzMg>~Yi_%JgKw+wt638;#x;iWNXo6JgN@=fa#h8YqjDR;JKGw;5I4S+ z*{Txxk=S*px2RQrC3tp<&u)R)Ic$;7spmdjVV>-82)gu_P{P>?oEgj&!31k7Wto}l zCDnR0%QtNjw%SDp=GvnCrPv4@^-U^-)o?!Qe)UhP(TZ-Mf4X4Vxpt-mOcPfay!vPu zn-<^OlH-6)Q^yJ%yX+++t3Mvuw?e+TDS4tdpsEuP*AJ9_~Mu$vxki|QF*qUe3jfXXZS9W zh8=SfxtwsA8PtK+^6mp(7d}ie@5oxIvuZy}rDz9zBkE%9E zTJ-^YuFUX>B-0xfHeKMWOsrV0-4MynBtmcLvMI$)?Hw)k5ycpf3FP7bA6o8_V7}N3Il3&f_UKt*Ldla6Ib-XAqM&A=!u9$qEX}V$BFE;@c@!iV&`51O_i%h^O`ZOm(9{cB9p&1cR{Ot}GWs z%lOr*>zmcg$Gkh^e=eQAC=jf)Mb3{Cw{lC0YUyfTYoC8n;OEXRx3LeLxpRSE+o-1EelJMxGE-3Td#sug*`&O2ErBL{)ELwVAwzA?5dSACPunDvxcU<&dxNB#8q z9UaBpk28U#C^~dS(OTWFRxu7&mP+flN)0}?H7qcY!3pSAMa|`Mqwsu?DoFh! zptdEv+auf2K50vk9vto4Xi`#xl`_+y|jttNj>)3mdLt0gkc3ETMR!zSwBITZxm0#HBS6ff={( zxPFMYpX_Ct|0|H`cn8QlN;uL1;)WsK0nh#;i4)2c;{9CZxi`vKIGg5_&U=Yf)<<^^ zmC;RirI?^(B2hgnfp@{T+-YNau+!zsaDPQjcfs!cdW~3)^c0ILGXBQ=CcGDz-->o_ zTTk4t>ngJd{LONSh~a5YYkd?OYC<$l9~tzQt?n?$`|Va%#inn&9MmzcV}*e2-@F$N z2@jMOhU3cro16cH!hDz3Q!#hLSLbe~k}hEy`m<1`yPVQr!rnxOf7@m4=w~DS!&xoc z3AqipZNpH)$3`cz{B7M~=_lkj(^z<{bf!}NX9^ycckPCosKFewjxfehRSrbk`>Z{E-$dNec{BO3E+Z<9oz%8jjbC4 z3nbop%=^seLd|7z(;4T2`4VXnKS5Lhq-9|iD>~Zu$6U=taP_;n(;`mf*A_I&*1HNF zXZxlN23iG)sGvtzE^w(wE|+cYdA-72PIe)i{JOKQmXI|qNIaIn`=|lQ_-Zmk2m$^% zwjDXa6JaRudYbm;&S7(I*+Mo2>}55bbQlwt6b2BDitd&$naiZdw_R##w>=&jUMdN8)TM=HTs>|OB|-T zkQ_w?Gl?myAkvClUWieY(!~%h^4EA^z!ouPoUNcUi73}D<**CP|1C9VZ;J@!*{kuC z_+kbtSCJ!q`0(%5(5}xHZ5+!qNi&u)?mQV5{cnRxnsF(&Oq_Rm0_Zd>Qa7iemvh8u zEHrkLlRZaK!}jG>GbIBsiI~-x1M%x!dSOx3=_YYPsYc16XUUq(Kz0yHvOg>gz$Dzp zr!21F9No8)GpTwU1)H@2-^90Zk}ztvFl3j*<|e~EEH1#G?!(zG`8nBU(wCyY)2UUp zVB@-8FVtJ*HMV*ThGHl}Xyx^VMaL0G(LYKdlH$D$M7~Mui;ezJ+Ug>Sf?%nuPrKFF zF8Mn%Hu&LH<}FT3gOj!Ur@GPu3ep#v>U4h*T_zj|IErF0?@g0!mt&Xs0gduc5dPx_ zWh6X5_TSeXRWwz?`l8^^glU{g{K8VAfC{cAD{9WNKv;L%@rRk%0c!ai~?y5-~ESPZvs9!j7cX!dl0>#q~o;2Q$F!iD^ym*z=s6XBX zxaFI1aIVy$j2C8Nl#aG!zi2%(Q+pMzX_c)-)~ZzUF27&?CBc)J_H^(LQ;3Kh@>c`t zkw>$%)z}d&o0|Khg`W#i6v^{TbMG1`q&LKgy0Ut}#KsFFTx6173G4H+0AIJBrnBs877`Bh=9Byo;lIzCNNolg|a;*EXlNkD#|&_-g}%S=~q zA^A*8FW+&H!;d~yu{JBQocE8nt~YqUUEd6H(OctRHbAVi_CBY0qp#F9m+2J)OxGn& zs1I9x-yO?giYMW+x}aKhY;JIy`1`1F$9Le`Kp9CQsqOd<%*l*Qgw4-<<7^)f1m=uz zKBvza4ObTte)TN$^TP(GuLXKfD@&6?k2krEM-9ADJfF&(c`c_{Rthp{3TI8+PB1`O zSe}J0UqA^UOyEreSiLoF_IRONO=9@&!2-Uw;ghv#n*qq!wRQNd8_0%2$ofkCzV2*8 z9Lw)X0WIm$9pstBxvXoD&`q%Y2qM}D zwX|=K#kl36q&O@15zzhDjZvPZ_1R$!o8r+1YvEk@qt>pOl^@h5TjJq7U*bkz+zo;b z+CPc%imW)e(^~HFXe%G;G9%2gqWAb2>EV;Gh96x5ql`H)g$sykhlS{pEK^$#FIzKA z80<@6I}4<{3wXYg?ASFozV5oO&5~4mI?EM2b+1P#{M{@Er+aFdi~UIbx2~dggNb>; z&bNujk+oeSC+m$7at%kTo)zBLd%YeTL#N4lgi?asUWyFpH1KcC9+`|Saen?vx%?3L zwI{WGg!}%GIsM#Ir>4o4Fz5Zb1inBgmAu8d{13O|w!=>Vhxd=*4o?!kdD=CzobKgv zulvxDvdoQZY4+@~S$}3~Okv$)EX8zlwmB=#Q|@h`F*=NJFK2pk=}u+zcC+AvEyIrb zlkL9byv1v}K@8@ImA?1h7Jt-sU=tut#T-Dn5@t<6eVAMaAez4<{5hBRKZ6+b@uIM zatyx5W>;=r!7u%GTk+Jq1u-pH)vs&kZMG1&SM~0YGplBpUgeJVO93Wj!KFzL7XBSZ z&rbi1oT(ofO^aLH1>p+DP~B5CWLpKHa+g^g*`XexEom`N!~iVmS|{Q!p|OSI6%U#O z#PW@dEGCUV53y*nYMg*8iY4`{+#2K4mQ!$m^^>{Lt%h{BJ==%M%1l>P3%eFAzdU`* z6=$=pqmZIro8*vOw`G(YQ@K0PNN$XlYir(xBh(+vi*N zF1~XLPn$cq_CsG%xti|Zl7>$yrIQYc0rT&=N@YY}&KHGA80DjVH+HaLPallr)-&!7 z5W^}uxPndUH{B^166nbMIl&aTdBM<1N!+y02G7AM2lIU7)>ewDTuthVzMg#;FYUQ+ zpULr3jykLL!-JvFu@1^TSW1*g|A&%%XJv%o`dW5!gq2{yWzZtTp`6=srLt$D-$C{P zYDcO3#m?5Q{q;|fJ8thUG<$J}OzBv);M-mrM-Ay>#|oJs(JM{BrqLJGnJ?QDqUD(; z=oj|kZtU6o$WH&o@yqa~{ZIdq=fTQqF;>b~P&MaEoK#RTSrf!^TP-x``f|@p+J&li zj&Dt`5|50vH^oXwa$DKO^dZiqHPpaZ8o?4$i`}=5+yi0cKD9GQ%$Z-m=i#xOFGuEC zs~0&Xur^&aFuXE3%zbYs!AjTF&&bZ%e^J4I?Z{1+#cj@%eA-A9_R!Y?f9RvYtS4PK z_f#wWT9qQ1k)acJs~vAIcLav2`ZibuL@g^fu?KXWww& z2b7T~82yq5gUz&XPFSA>c#@<*d?QFYf9}nTHC=sew?emAn~gYhfe3v`n+?Cc#s|-8 zSRgL6!x*0NR89L9EJ7b#N0f>Rq1q+GyvU)GBe>A?_cg402oS);;VX_8F-qBC@tu8{ zTnjjNhcWtPc%c%!#4}gztlISI^@@624Y%lmimap{`U-ti#6|Fe{TgXfw;U~{zY~75 zQE78@DUaE&%u7s!B+H#UNLLyW>b%3V!e;dbGL2Mdv?74srFz^9jN~!_1iXWD91R0! zPBZ+=*xh&G7T}pSjV8aiU`{ZJU@5*nW$2CmQ#yI_H|Up7FKDBkD|)DJ9gO@MtV6;9 zeqauK9uEG8fX_{9w1Y=;Q$o!VHd;5d_)Xkb^Rq8QYHXucXh$PMiLFgs7GK7Jbp_w3wz`Uzrl54O zHbLxSBjR{o&Jyqq_L_WXfLO(>wt9tAE^1n(2=f=iQpKQxw#JvA)H!kKvW($@n^ zU)?<}S74Ep*I_QNVbL8e&KPQ(pk~)?Ye}u8Ahd0y8=tRXx~6q{evSbSxN~eXDycuS z_q934A2TY^8qI>-YmcaPf4qfi_c`TE&iIAl zY87cEFUh|mW8h&x{^E?ukjt?ZM0hK6dR{!AyuLAc z)zDCZ_vk&_k_PNm0yZJJ?xxNZ%<*vXyyATJ``h7T|8Mn!VAvwiILLe>5PlEHu0FY3zB}3`dT|B ze%KR7rW7wHmRZ5Fpjz5iFSI2fyzTtWsH}8Kp3keC&n}wsqXNALbcxLUz&bdV-*jjA zmFeq7#W0lz9n=$k*_unL2OYVzy&n*vA`|XOiC1?NT%UKMOg}VbC=98|-58vGAFuWNCx{pw+S+QvKoEvw#-p5%2j&7$SADr}y$>q^t=4Ri3?whc9WqAS_%9=)C#q$YK0 zNl=XUpgM@9+ zv@2(Z_pngO6WaJXgvtBI2r$qq7c&*g^UM%z8YmXJa^{XUB&ciAE4h|#w#c``4c(ivIJRc%cKsxzT_ft=yTDvM>ME;`;_?P8~6Z-cKv__fHep zUP3nj2}QMFf{IyNlEd14!GvpGo@M22(AX&vB|^2U@OBAxnU2_L3K-GK1}TAYItk zS+BcUx_>i87VdF}V7bkf64bGA?LEEw2Psq7;0(I7^`62E*4K~Y4{WVnyV_6S=HbK_ z$kffkk`0?TColgp`WFk}?;i@b{&c)26jO%YXxAqU|G?eJA6$2=w9hh;IsEprI&6FP zcd2DM^bUT4FyN)Ju8H%)=xqO8QgHFsj7MF_)bWkm?%UnUIpfF5w${x$xL@L-b{G); zkeapKvds5S@UI3rB>J-gA2*u9p58bcZg6SR!3R?cTV4y23`*lkkO>>~;*a{n9_P?F zq5#z8cFV0XQcc4h8BbP4s-b)Rgbtma2{Hku+}u63onY~%m9C<1N`6@NwVayXwj$4_MML07eGOK?>D2z#{A@+9D4AVOgW~o!Vq&J7ALhJSavttA1>{u!T z6V9rIExbU{gs%NN+XTRt-1=#-umyNM7b2ad#teT4(rqVnU4vYpkpRLR7d6$x84$TV z2E<``5Zl@V$6JMzSi8}&@)46 z7-vrWfh&Wl7_sYxnZVk{2KH9!WtZ-0h>02*-Hh3RRH5bNp9RqTGxh+jPBfjAa;_23 z0$~ol?hnD~OWjANf7rcce~0mWRPefo!^-U7D{jVh_;3&*e7rD(fG|*Sa@>QTM}ZH_ z&*{X&$8#>H6k(^P`Z>B7M|;J$pj07>XWSO!vMqtiSti^f*)io>I*n=x+m$@$>cKi{ z0sniFM}_0R!MX!CmkwybpO?A~JN{>)e>L+OgRN+q_R22^IDC?q&Ra9!)DTLt^Gx2b zfSn2bb7Bz6#~DZ$YxZnsQv_{X{QEB89IGE zj)$1OhTRE-NHJZNZfua>w~BiD{wNzrvJC{B`aqAf8po1cG`thMwU=Lwe&&22mCQQ70S}S&rltP zD-HGa)g*2^XUqkjD5!a0w^#4v_0xgY+(?bXL3qoK~D(Ox&pW>7;A4OTe4UxS~3%$1HG8QVFsMy z0tf~z9WpwG?kq^;Bqb>IwMvpVdI%+KlCyGTTRw-(URlGqs1pG%K08-gpb4VU$9qOE`6yJ@#99?K@S)0L-Xt;7k9?rRTIA-v56J94BmYI7H6DH^;XXE zsd&V&u>2SLD#<4_`_a>KmN#OAuFC$cDq=8T7Xy=#E|fOa^ZgMk&2JVt8*8CIFL ziYw{;%%0;qAfrDQwGZosYMSRGUyOIGWpiPmcBr>11U-<9a5a*wU^TW$hvf-YFk8uC z=uDQflrcybW>DrETGi!c8m&*Px(u{uy$jhQOOvXJPSjOY3Qc6PW+TL}tGWE;kf2onO6{d!E_?B~jjPxj)r zY+IV%x%RGq7dW3lXFFWNvfGu2XYY+4*{&N}(!CE-YCWAzDhMmWD6De>Y}833!o;=& z%5_o)OV<95iR1nsQj+%OKICnC@jGuczEqw)vHR5a_zq$Hm2j~!91FPJ*-n0CzOKjc zNKfx8__+Q8H}d!nMg4ACpOPUnn1E`0(O!`{CU|stZT=6gk;gR0^`U%jWl?2#1h1OG z&+U8Muc$BWtt{~0zA_C^rd*ho*mWk#s?`2UFjJj-M$>l8b(L{NUU^*i%Z*DNFMcPX zrxPI$;xv@KvBNyAOow=lzb{^vYUD9N;r9nT$7}?5uy}Fr$h66reWob-{@{uaDEM#d zaN&y&DFpmIk-I{VO8WQ|(k#QYs_HClDL2U&BscIUgSVvJ=!Efa1lQr*SK#!&h-)TG z>DENw2##o%Gp4V=pA7Sv4!Qm+%%9gz50<>p!Ao`n6Ngjkln-m-Dd<6eaiL+U|2U$v zQT+OGktN{pTSP02{elV4BWfB-#_&op=YltQOJ)huXtb^uqt zdxErfoB}5UYpME{+jtj;Ac1@g%P}kL(NCyK#GXWaI4IeF`DmLb8|p&k#OB$eIGCO6 zawFKzJ7O8$&>G+!td9dj;=sdbk=)G&G>NtB+ImQj5UwC`(?agLTECE~u;&ZlqU29= zkul5zfb*^vYb4_9BBO4HEP_lDpXh2E_nI&T^Ob_^9m95k9(8GOk!(!&<*!{0_*-W~u-zeRlCi@ZW-{ie{H?>7Sq*-KKIt z6YH_J9$E!qR~Zt%gCB~@OqyZrdl7&p@Y}Ifxy3t>@K8tYzime%x=8&F$W!jW?YI;p zc!DPC)|ZVZryg=i!jxbzkn6K{{TnIFq+JyZ1`@y7ai? zJmT#LKYXq%T^UsJ)X*BgPJ|gb8AjhIOM|mG@$$LJX++X%Otm;ETYmOAf(hhMm#7l1 zLmpnD1r#nQ(VpSq%QK?>uWnKD;E1lU!ht6HUrgC%9UFu!Kf6+tENWN|qN7VLCS^KE5E!Udfh zh*niqBa7T7U6GU`4m)jKxR(JHFXrZzj>waW1$xWdL!BZIeD0X$(khlv`VVqrj{QJ{^q{o zB=ldiCJ{jJNt4m=kyyvfp>N_9?|lDQ>J#tQ=_@rY=ZS)pU{Z8UCDxSUc-yQbv%2rE zXWgglM7Cb_U#Jaf~i zmkyX;lyBO~>QC#Jb1jofmt>qQ5#&IsDOD)2r{|#5s*2KLiOR6xH1}8?Xt%bp;WgaM z48|xOp>@hzqCj|?MXYBz`ox0%T@QgOL#*USkQF*+XNghfs0pLxb1a?vjJvY>RA_G1 z6Jwf~Xvp`x5X*@0ZC<-B-f!Z%4lDb3-~Q_}n-+bp|7Vd}eUz>=AM~%1#AJD!RhSKE zp5I@erQWCfXPmH)@}tLbKytWyGos_H|v!+HLzk^|JNdjTXE>;s=R%JTi6N6CR zNqMc3L=7WqBlaj%9}q>wr>X~Mv38-+bfYNx%@q})au&aB)i^fINb8;K@mNWdvsb^V z6WAg2{(rYP{};!nl(LkBxuASJ8l7PM+LE%HW*y#cRpxVf(Ik&?H4eT|dQFxBQHzKA zEV|S-dM34@Se6Rb&n4Kdbzq~^AVho0SgfLHVQXw+EtT~Km7tu8@@LgokcZZwJoe2S zQx$J1#`s{N_-ZH=QCX3^L+g4j2QL2ru6FgZr8H~B_i;zb{gE#{d4dIvVT?i zH=6%ZCNP6e0xZZZIV1bx_F(%>o+vGH&>T8ta%QP?!#Cc#V)D9UWqg$K zON96*j$o>8mR0u|05insqPhFEi(^zl4QU0Wj8ig&xJNNLfGz2bm~54BM7cO(4<7~{ zr_v(bATBFmwj$i4L92u8F*914Oex1t$kL#peIp6ri`yqkP#kau>^?i(n-C z-K*G#?isn&t7bd=p+!=?Gd)1iiSz>ku>jA3{^)F9;<{4@vM(z%f4JiYD24g#6AQp+TR%lN+{Rc(`*#= zDM7+yE)K^Lr%-48EY2fizY0gIgcDMY*5u1GuOm5r!v9f#g4p@p6?fNi$iP476z-*v zs<$TfQpc=}M;wzh&w6Xg+uTef*+ay2wj3yynM)X*D%PMy1E}U^*_8*Ej$8!!$~dkL z5zWe4!^A`D)A8G#*p4}A{>Xu-u}?tnpH-nCJRH_2u*W?f0gmORM_0uV*+kj6@x(PS zZKH(BCUYgt(F>!{;GNs`_aQbBL^wDS-_erDA7T!kW<(7uSKP**p}A!xUV)E4Dvu$q z*daNhhQKqlh^y%x$Ep3MeVkE~RjOFEWf%9GN`Mg2x<)2E7-VzDKM~}&j682|1>BCPjq;<$W8hb8JkMSVekUUlqnv$O*7sqSE_#_@Qtu3f=yV5+sP(Rjd zMO699Hrry;LFt0>GKYm+4EUL4G|60dlE^Qj3}WVwHXP3zawSme;rjgmkzDX9^*1Ux zRP^t=!jv$E+%J0fDLn0)f2n~X^mi{O9_$y0*>nYc3m z#RyH8D6`1%4-p^Jd@3#7!)eo*$dm>K_eljy3hkVkHr=Y;V6xa-}EG5ZJa) z{Vr3thQ%)7297ndZExpoc=t)IvFfp2Tc(*V>gX=kAn`{I@wSrAOxf7)`NM)pc|#Lx zYvI~)YxhLHAULcb*FM6!lY@DKTp=^l3URQiii3IdlK^8{`zSdB-3w44Vuta%ca>v7 zxUMlG-4uRIu9)|Ehm%=o3N}>LKZ|Dw2`lpS$d1_}k^5*SB85s92!{s<#bai?D2gBlF-?$AT%%N5t3$!#5HuDRM9vU9Y*Ei}$?qOXLB0v7uIw4>!?Z zkk50(TklzQRN2Z+&Mbj1J^SBu$}MP^5!X}IWKpJAf939G1*~bz*(wwIA_YyZwA7rm zYn!aBw~EtWcN$w6!Qu@+s(v@(9eIThT#&V~Rb)(YHJ5C%c3{Ov5D1eLjY#(!l>Hs> zYr(8xMcyco41R_NDD8v$wMZn>L(VUUr?<@mn(nlP&Ch*{fArFX3&|5!0MEN$KMLO+ z-!aHlAF!z!98xHM67NjcXvQUE!9bDHY^p#1{}uKXU{SPP|4WA=ODiF;bW1CUgmf+f z($bwuEZyB8t#o%S4U&R@ASInDBHbn7x4R(E^Zvj0yZ&&lx#OJkJ7;ERX3u@@nS1%D z020|#7@q@@(I+}s1FG-`Q%a_!asiZdm}enpT{HgFI-<~nOtdO$b!^*V@e&EgR(4c* z%avr1@AHxXs~pcxjs+Io9NCTC88BwlcjUQQz4v8C>^hBaRtA!A?(CZZTw9m+9{rYD zBNt7ReHOx^n|XJg=uj&4X?n{&bev}LD4il185t?af(y&PcI11sJET)H)ia_6XS;{& z;n|jOo6nZ8_D5&m(r$8`O@59?NLi9)3-OC9FK{f0RCU8U=*E4}@c46h@2Og1DGtT2 zfpL5@h$Oj^;d*+pE2av)<5@J zH+ExFA8K+fA5446b**@dAO~8btiRE^QLw-qahlE*>a^4O1Bid(=%E*n-m77*xAOT6 zRcY?-)nfeXTu!3wulE|C^kDA1bgLgOjZbcxe--#n$K{maOF?3<@<9?dwfx!7soA8;RjmJ-p zukcd$658?g>5*>;^s{^PSD{<^C2glXYP&q@h;&CgwIBh~qrb%Y7K>O7xI-K*myG2D zCrk;j0^Uo_4RmJ7SN%7ASUyRP%#mCS$NvD+$@)DNy=1=aAZDzkoP!y-rA~R{zjfMC z{VbiojT?y`c~qULkXpM7v$z(BWZ@rxf}Q$+wj6WKnB`#oaQ?+|8_lI_cykLuU{l`g zfz5NV4pj>zX)SdzW3~IV@xm0-*xX@UDK^h$w+bB61P(<}KTeY#BsO*MXANdj*TLQ} zb^V0=#h++|7)vn>y+p|gRrQyZWrFnT0ag1?AwtwjTg3V#2`CyWubH)1g!?7lX<0)Z zsXAiy0>Oq~tt=ECX!PJq#Yv2b6g!Q7$L%;}5LNU`LleMuB>crRtddzEDk*}EWhyty z9{LVVV?r)Z%SOrHAN}tANBSLjx+ZH`6%bg1PBNsDcdo5V^oDa&_+=6HD~6QQZrD~N zAZw!k6sf9eG_)wZ!c2x~--_N@&i!f`i&C_Wdtv|f8u^xou=DSh#$0=>6}0vY%EVjA z?`YLmWOwu)R4^`3voXgzJn)>~m@7~!bAnkMz_`SAVk)~xZFttG9Z+}=IE?Kk!e(jE%_f`Nvh+bivbUkDloa@;NNys5Wlb(u zR$8l!{L@bL<+%eJbOsam1O_6kqcHm^u%uoI89K7b1$Ttj6Zz;xHQSZ!s3 zJ9*pvuLejkyMH+B3z_ZC*sG$3hp68Mk!EJlE4*&@!~w)emA++OR*`S?ad-`HnzMKW zQewkiTap6bZOH@iGBLfaf|aLX2jOZ3DFlXk8~X}c?5WN9nqW^E6H5uRz#O%WHgTJI z0$oj=LyowXlSF#l2mm%6eyHMDe5K`4yze{7S8H3;q(u0E(DXoN6)?_&`54=vA4L*9 zNy<^C_g`WGvx&CtCJCwxM!Pb5l1D~e;9 zBD)(!&M64zR2Voh=`WPa9_(zs0Yu>agAZ#`1UpbN#!B2Uv=?9lMkPL18ky{s(}BJ4 z*EAaz=20yNrUJ--(ss%Dg4{sDLxRZV|) zXb;*srJ-a$dG6Kkr8pFI+1!k~?L5Ti{-ANX?L;_q(;cT;gHhQ9S2l|RMHW;r!^{&o zovoZjX%MtnU;|0ZKUw6i&R-3&n*vX7nyz|c91q(T^Ij1)F8kbNYd&h$J z&mh%g$13~I_K>alDQ}0m*ELgvLxZrnoC_r*oT6+n;E_xsd+}7%F94GtGz&HzNa*}% z_HbAC+Q13Iz^(@1Y6hv|KmE@~_`fGwVJu8b!UIS5u-xQ6Fspi(_U;eB=rp~@&aIoQ zTR^CR@A4@zc4YWvu$$kbUxJJRt}3DAZQ^Sxxr(u{0TsbN1@0-{rHHPy04@j!N`GPP zjWx<3od%-Zr6O7pA6_RP-ya`A7apNJyHSBX_ZWNETO0-Nz_4$kun+l7&L05CF%+hh zC>%f~>hYtA;0@Sf<|nhTxc84+Ti9uaGq@K;2xx?G+O^JgA$y~mNo*_2GHNgkcfc`3 z^8y4wXALQ?%~i+T1bQ#Q!KyCk1J7d6K&A1o_7*z?P;IE5jp_TFi-ft#cp+g?;T8Fx z11G=i2}KZ=KEG;`(#Jh&XAy5j3)8RlOR8$c&vfaR44I)H6x#&ns129Yo#qtNl+YC4 z{uj~|f`)VAUm0P)@aIHF&*TwTCE=zw{+#lb%T(NB?D@M$elLdrQ~ACWZV-Pa=9(M* zCWm)6r?hhC`DO`(D50La3OrArcA(50zddPpN}e6$?+RPR;>8dNzInnXxeNVnr$XRo zm^K1a(*_5QbdAr@6oJONd@nWV(;vXl+0&Fu``!9wci3acG_b3Hqe{%22FE)uKmrN& z5mODWqVm(C8)T#&E`$#BhB4(fPkj{2iw&BME3oziT8i4mDYfN_P);f-4 z7v=n+>G=Ykvo#C74{7n5IQ?CVObfheO2#boT7VA^qfH(SQc!QT{{%;J()U3HT63@u zqtr`vUrqu%I00D<(I-+t-11(&ROe_d|2Cr zDZ$rPoT74#(cEO-Xbl(!9436jPmtyQ12H3;FKG4a&eF9WjW!o5=xB&7cTD}|y=5s& z?n%C zW5K-9wF92OFI8*LsxC4#u9Ql>8773^Zcz3eMgRESyYFdq_Wx(l)TU8~muv66$3y0eW3TIqW|T~wK_yY_|9G9iL=78g zoVuUB=)^=)qK1cN=OV!s##&cYXsKP3Oq8P=#=B5kO8y&XQGlgpK?K_h06A8Av#mIv z+6`QmzfMxYGFAOUv7}O$^oW$!<2$IZw^r}>YdsIXPGfc2tW$UWO?A~h&5s6P!-fyO zZEI6=L4DU5t*1;Y&ouutx%GGwirxLv;DHGeM=7PjkfB59*^|$>R@#oB=DzVkTqem^Wa=q5Mb&qx zurXM$_pHlPrY+$Q=zjnOK8wy7W#5{eW#}-8^s<4Jz}1sE09I* z>EzlV!~{f&WdcS;7_)Z>y)kR<8b8PKjT8%kN|Q9uYCRQuqD)|8i7ZBpvHRNU2l5Oi zF;f#Q6nPN4(B`Eo^Sta)c!3CMDwRegPOxvj>&zVm^u{niJ?TlGoJ31%uUGs+|Kt=+ zjZ#H_~BlluW|DXCTVS6W*vOi^9rMG*F|N<+XMC< zh!Ro*V)&AXs*1m9q}gd3IVeVzkP)vIe7^*MBj-TyV!;6!!4xVu1` zQK2gVQ7DG}olTCoNbW@kPq#k%XDEhiGu>V)R`ydS&Nzi$+U}v!7+e9a z#lu_v5asoZSAE#l)SCyexE^tZg`o5+e{+A(x-rKvsE2E_%wDOw|8xmt7`vQZWI*t; z3~BWQoFf^jJC&#U?LA}Az5dysN(W7_b^KISF;zV9irMZsDXgd=#*!bg)Kj?dbBdto z^)HO+#o$DjpL<7M;$;t4o#}N4y%@K*@;z!Uf*ayTf@&K{LqnA%yU530(zKY5z>Hzn zu})6zVk6cB%opRtx;siUtF0-4W>vaiGLXcsqf3=KGjkp!u;SQrS+$htEuY%_mF1qf@bexbyf+G-LGxQvZp8$`pzJ~63JQgnNd?N zY<5o3@3orjn%cCsrTDwXO0&M?P}l17L^B6d2%njQ;e)ICAdlhC$?lJU&rY)^C0PG3n~!3|29{w~6PKd(KKP6zn| z*h|S9vFvry4x5m{Qw-QL+Avv%h@~hd_B7!f6e149zQz!RfG0Mh`D$H67~G?+iq_ z&-+!#VnazwfS-k&9-F>zr*(4s)cpXeWtf272_%h7$r1SjFe^jIxQ*t`l}Ast^@YAf3>a=VG#Ak zGEO7ZQrD+V$=v=wQ>&Fn#G_~PN|sLH&?P2pNfbN3pu|iQ#T~cVJ(SAGL%dj z3P4&7#w1LrzH6IQ^YyJh=&OdBn=&ZM44Vm)DxBI?jYT^fN9{z*3Hg3dujyhbIkbG~ z>y*fhb^7_d*G_Poph5_PXs$rk?{v@QDRbj4_0sa)wu5sz$k2z6H1jM?JewtrFt^j* z^+e;%?)e^byH7SH?|;)N%`MH%&D@$jN*|n`-=wnPQ}WzygLEyX<@XxpSin+8(f-M7 zjT?(IhnMN-JBHEI3(kL$jyF$dOL=Q=ZKJ<+(ECWnBV% zj@G}XM{07+B!?o?$OQOwgZ?T@;2Ga{3od4>b^&3WSZX(ur#S;8i9AmwL^cmm|TRUnufJsf5=&9PiC_W9Q`-= zYxl0L27eh7@kV?;jXwBp-PUsWzW5byEavG)Tb79V=WT?H-d6?MkLmi@Ra37z?{Cw6 zUy5ie*%u678`2y5j zgkq$r<+Z%!cu5H)9J4){A~YHH^I@dbkMN3WsuIdl!6c=B0H)>)>P7}E3U)(B%Yx5O zKOoLGn>dlK#ekUvQB}$Fn0D52?5{6onU_pigSAy~9{FK2ZvQ&mrbBi|p5!HKAQZqo zCv^&{6p0iQLCpm40c_?@h9`0#Avd2hG(yF>gVX6 z=v!UO+E--fOlEOYgj(}IuJoWF~W)VIL=r&haM{#)Y~5T-vB(g0f=38|D+lp zAOD@HM~Z@hrt%-GS$+KasYF)aErk*oRVz2fN+vAkw8Ks1$^yuVIl#RZ-SD)_K3+89bfME}bt< zXl#{@p<)!QfkO}f-46d#wyZ%%<(N%bQU zX>IaBa%3~uBn{jee}R;1MN91Cf(Scc>V0u~;HmeS?7{M!n%7^UK=R8P2T{|HJjx}G zU?z(nuvKxkFXa9Jn)f)acDmcn+wlKuhl0w97hRU4@;Fhj`HP+IfuRfe zmVXyg(L)!{*L2Xf3dqKXLta{PEx-QD}>6(6ZPu)9hkWYXGu>1dF-AKUX zEi=}o_VpIhD?-NF1ox21?q*R}X*K%hY{Y_lxd^-)!Byzh6$;P%MPa7`k%Cju9ENk+=4iy^cXhpQ0 zO2>;k2y4(b`vJ}O5+gVUQ#DnmgklCJjX~(TeA#~etme7CPQ)N$oLOctdMS}(sU8lK ztYwypIFNDM`gaenl{6u2{E3J!mS@K{AD`5^*-IyiU86pXPeYbVG4b2VUC+@hLL#Dbi&O7YF^f97Kh^)_;6mKDQr1 zb3r4=q8j5`rY}tBUzpI5yuc6*nk+f&FoMd`--z_f3E|DWj`<9pfQulAB+>ek`>(nG zMCXcezo_{aD=xeUEog+l736gPp?N%k8_4GRHbSgIGV3DkOQU5wU|TmFK1XeUQsQ)H zF&;i1iuf$COjVb-c&>7>-4jX5^(YKSoK!)GtWy~_RwCswA4v(@@M)3Tp@FryoCw9} z5@f`AfcEwc)}Nx<@XfUjNq{@_aKXW(&{D#Ih4<(iazz$jy)wbgZwiO8dGa=!Pw@~? z2BK{u3rHoTcZ_7A1&f4;!B`m&$F3uy1~&y0yUzz5&RYK4(d8U|w?t(sX63p%*MYFPM6~Te~bafXNSh zloGARM4Bal+~rdl7SG~5X@AQczFnh=4zM-qmy&^Z$Xyu;1AV?<`3e81zk0jHFshWB zfnZ!q2A?K$1_hLiS*DG?SRVG7JEZ zYDs8U{F4-;x@;{F@3&E8DwG*NKWXyRPm+~5q)Sdd3>o&CM+s|LJwOa^>k}q2=gqLJ z{tNc$BXajHJfW0sIL+tt5qyxrtLU*s=WvzEpz^~lVZD#e!F#%!rY9BlNz)OP zewnGdOf_0QL~;b}*(2+bfMM8ph+Lj)|97ycV7Hi7oYUz0uQHx$qDhMHblnJPW{UDi zGXv39-HLhCkkX|b7M|x^M@25tWoswo?z128KaXviEZI3fnmU19-#g6;t0o*a3y8#! z7dIka;2n%+rt6peERHpQl8%jtso>hjFYu5vWhew?N=E5>!bBnLb0=xCh- z*j}91W5i!_oe$(LIFC-w>(U&roQCImham#0*4Fv}|e`YMiuFSZt`(H7AK>t(4{zQmuq zhJ$TERk+T`*5m1+zYt^#oMh1w{zsav(~d7JpAgOF8d3;ALI3~& diff --git a/v3/as_demos/monitor/tests/syn_test.py b/v3/as_demos/monitor/tests/syn_test.py deleted file mode 100644 index 19e4d72..0000000 --- a/v3/as_demos/monitor/tests/syn_test.py +++ /dev/null @@ -1,54 +0,0 @@ -# syn_test.py -# Tests the monitoring synchronous code and of an async method. - -# Copyright (c) 2021 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -import uasyncio as asyncio -import time -from machine import Pin, UART, SPI -import monitor - -# Define interface to use -monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz -# monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz -trig = monitor.trigger(5) - - -class Foo: - def __init__(self): - pass - - @monitor.asyn(1, 2) # ident 1/2 high - async def pause(self): - self.wait1() # ident 3 10ms pulse - await asyncio.sleep_ms(100) - with monitor.mon_call(4): # ident 4 10ms pulse - self.wait2() - await asyncio.sleep_ms(100) - # ident 1/2 low - - @monitor.sync(3) - def wait1(self): - time.sleep_ms(10) - - def wait2(self): - time.sleep_ms(10) - -async def main(): - monitor.init() - asyncio.create_task(monitor.hog_detect()) # Make 10ms waitx gaps visible - foo1 = Foo() - foo2 = Foo() - while True: - trig() # Mark start with pulse on ident 5 - # Create two instances of .pause separated by 50ms - asyncio.create_task(foo1.pause()) - await asyncio.sleep_ms(50) - await foo2.pause() - await asyncio.sleep_ms(50) - -try: - asyncio.run(main()) -finally: - asyncio.new_event_loop() diff --git a/v3/as_demos/monitor/tests/syn_time.jpg b/v3/as_demos/monitor/tests/syn_time.jpg deleted file mode 100644 index 88c1805deb868b804e4877b0fd38b0fe9466cdd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74885 zcmeFZ2Rv3^{5XE?i0lX(?%aep`xg)2;t!1KqtWgZS>RK zS8%nqgdk;QUT7Btr#;{gK_?II*3A|%*FL_|bPyo-dChKzLgZqj{J z)Z{e#8CaP2GcYl+a`17nvK?k;V&WFzIeb(=NJxl zfIJG=Qh2!dcmxFa_yF1yv_trm1XKqEWC^LYOo-U*X#{VCKH9}Dm-CKR`~4KhF;j<| z#3Xy^==U)kenMVBQR$?ziq2_WJ^eEVXU)tlEH7GF+c;ixa&~cb zbH8=_&Rw5-zJ6ih_a8(=Mn%UYC8wmOrDtSj<-T~CmtXLzu&AuOqOz*GrnauNt-a&J z$4{MI{R4wvhlWSKjgC#v%+Ad(EH0szVZC5IHxov&Kj=jX^uoo*$HOOr^}@k*0e^Uu z_yh+82&rVXh)nFM*#vLwqLB-Il=F_5{h0O?t*OI%lD!;4{RgLE)zF&#GsSNHC(X7L z+t#ZKBE`c2orgyWA)qnF>M#3Fp1P|nv(L0zt#Eh2$2B8;Ti-8aJ5p6&U zAN_mIoq6`?TI^7u-o@SdJru7j=~o7CRFQw<72EsiVogLpYVeLrJ!+Trs<%{mOC;=hA*Dm>l8lodTEmfYR2{fxD`Xe0_l+E=Q#ppMn(rc4TXT5MY zpv31J&|99;B|lUdPpWQDSW#H<15;ju;K==t%^X!JT@F`=4~s7^bEgo!zc>)-efL;a zkfPEuP4B7#*Xj`jjv1n>9B(}?Z37xOH1b$qIS;tWWdT4s_EbU_2e;U~b)ARWHOqaGOf3}t@z^rAZJyfOb zb6M`{%2!>E*VH%dKMLW@X}}zjEoU(B+)Jr_Y<9w$8f$JaG5y9`p)^`!;f2t!*FnU~ zr*mrFzYWWjXb5^;Kp{g?;E;Nj%OqjeSv46Limf921s$|c=xfCZp!eP!75HJgb zQJbdjU)2^mW^?LUp`Oka`_LbY)*BG$WP%My)?`znrgZk>IbP+v$QpcX$g-r(-qO;? z$iF-#Ve-|k{5p;|^>gXrJI`4Zezq(|G2^+tEK39;8xW3JP9@r*mzU!h}HRAAn%WXmXE=Dmu@l){^J<=pk#GXhq{R6 z2tBP?hyjk|nsTCq@XNN7jT|@6w9uKl@_t?1fXsoP1WqH~USfD{^EhudcuuUadKEWh zRc(rKP&`!Z9m}_4vl~#5eiNb2jTwFoy$y(A{N)cNb&cJz)>l*wi+Hq&-}a4skIUxx zdSxJduJFOxu$yX%%lG-sbux_d?HJs4HNR%KN1>?1_Ego$aev3#ps|6Y<`xnMEo$)l z?A#q}h(d=PW@=_6oGzMHtSLWMe3I_<=*K-STb+1uZ*?*DX9C~95%&$tdAyFYxZs@P zH?rSum!w`ql3drn$ug!md++MD4`I|ph9?f--wr)<<0CX2ax!{l za5lL?V=}nO8wdo!v{+`pC9FD!$x`@x}bQHRl7fEw4jfHcx3s1^>5N%AJ| z{a33PGc0*e1}QiYc`&tA>{U6+H;I^{L@fPKK+CHXmC(!b5K`u?$l`TPCYdaD1tfrD4r(Fkh4zS!T zbwZTaH5^m(+z2;|6}16Wkg;v2<{+17bM&bYnGpKEFzemoIfC+;0l1>&$GsK{RQ%C4 z3>H8XBg7$Vy)M_shaSGiY%Vi4(?0EjTEYB?CEX z4+zRhG&2%qE z5Roj4^QBecKO&K{D8~&5d2ZEO@>Sv5cmRvbSI9#7L7dGq3^0=Wal~k2au6O$Psk`cl+tA&}we zx=`um*I=O62d^ym4yyeGs5Ml%0ZIKV-83v6+6vJJfAq;I3^gIYKSl=fNA5*@sNaCR zd^R9jwJ=dO0?nc=G%ZqW=7W63zk}eTe3`}>@l8Ivh9G9uW~j!Ux-X;H|V_; ztV#)(-b;XcPx--w1ufW|q3kE(M#DDPVjcYUW{g#LIAL;b9dW}#n`jo>Lb#nC#y{qwVwU~FfxIE}m zHh1GNi*PY*&?uC$sT?@B0a<;yC0Xy=i*tdjX`u$JbE~D+2o%`PX4vu$yDVu9r97Dtq<(@dH6R2clgsD4CKY)1N8PhA;R6>lBwR zc0TgHdftuZs^E94G9ve`01@#m=aFdoKMgE|kMq?>&@k_*WSoRa) z&q)434LraNLlM{x&Doo*J}4>r5IIlbejcE50?T)_6vtI#Z|q}(VdZcu2XTERcM53j4HQ-! z8VLp3Z1M(LH&3G3L5z{@T7{kpELI8Y*MTp4#%vLQt{OVgTs54hDYZ%c`yIg=G(dwXzq2QVOJx zj_+|pu6#uXG7>!o9>Bh8?Xm|r1#?nF;sM!kU;nN5%8QHN7Xt4&w7Q-Mh8banIe<&8q!s?S08btM#yC0B& z!8?{vymV~_Q5DU`AJKWS3Q>fpN~x*VI(0u7xC8ge-E{X#Z3~A=KO&}rCt2KmE>!CS z4^>)fUks4`>ipN7Y|-OasAjk^cdGa5aopL%(}Sj z{?*>#JQ$XS5_eqtdU6fJWn9-MZ$gpH%B7R7!0nulkjqM< zun{4-dDHEULLFCXF%Z{cf$PPGBYRUpu>Cef1~p4-U`7kG2iC`}RTRRvz%PUOmlI4v zs$gP#U&lZ^(wu-9mBG@Ov0*KzkT8#>H+ozF1e_h^a6F?SX`Y zov_rH!rCLJmD(30(#UIwVq|xQvqJEeM%>?Eoj1cM^JZ7Hj_;5yWru7U_l3x@ma&>0 z!3N?;bK}`o+m*S}RU<~Wk;QH6}OIW)R9drt3b z-V=^6Wd)A-RXu?qCh{+8ZR>8JlW5N!@7rzV%@KGDKfpQd!7cSWi*Jf<&WzMOY!f`n z%DfA|k8&v{e8u+k7vmqZdnK9rMy9gxBTO5VEycLn-}~7b$vn-fYn5wl7pDgx&2iuAFZ{W53n}yf} z)C#nd_&Vq|>- z5?Ok`lfM!`7uS=|&wypO zRfi``ykqh8>(I`wiM|(9%Cl!2QLQ^vW57{@6}e^$@BjWn=P#vSSROLAfj!h;>pTbV zwZ0?(;PXG3V`p~+LaA@;7kx&Q|E4>zk{Nme6((N-!#00zUR$@FPTpzs780}r{C#)$ zI=w}K9cg#L1{8b-*<(C(x@3nHv#V6)@Az_LaD<0hKgQYUgf<3npDUj@VXUdGp{RUH z0X%;P&(0Gsn_Y3lKMFxtu3mG{R+3`{kAhi=h9DA%8vF@B>?UT8cCwn9r#2r*|9W5e zfPP#LwH<+J{o4MQHDu-%j%MINHY1ya$}Wa@h&UhEDJUc~Xn3x;o&_0sfdEB}f@kgES#l$P{vdtf9-`<;4+jzY1y` zA#Knu_b2!aXnak8WeTvYAv1s>57|OjAQLn`1or`e0cqQ|j+Vy+(N#F4au7uDWn*LZ zAOsPGL(p2t#>UE{jg7S=@b07sg5F-;#8BbP6Jz=YJ{NZ7zpCL z2xuGriQe#wpe?)oB@EVn==U{vxVZ2M5@h-t`nlhOR^v{44$*Wf(I z2k$Y!tBTF{HB9g;FHC}s0ViA>0(?C1z6QMYVBBoO!8Cy@_N~VT_SFFe9tEgdf9CGJ z1+krB@M3u|v;1n=zU{kV(|(CJ9`7aGzE?k4ah;58+ZQ>Pmi9X&*JhI@DM`l~6cbW; z;m!U%;mur;`n|EUJUkYH=aNiU$LH#2g9Do%Wo`2TIXo4ix7K&@Wbon9_G_{db#0n& zPG+2P9{Skd8{MQ~=(?;v;l6Go)yAw=YTS$rAFZE!=xkXkU45>^z2~>u{7MzVNo47J zs(#y~KAXUbkons{0%<4r&A0e8R)jPc2pMEAKqiUwCNlI|vxo7>j<%3;JSR_a4kEC^ zr!kSCA`_xlR`iyYkH9$?W_2{=41R=iBFnLF%IFrSCqpVyXCvFP>y4x&m#%j%D<(Kj zSV7?lObuP)d)-o(BCN+wWeb(E1xT?q3%xQ(S8v9DuI$sY(m4`A%01tC@$Ok^%l)d7 z4y$BGSD5{!wO4NtJ!_D3crV=E@98m#*BSiGipFz%Np*&hxn5{VmG#ym?KqSb%ZZN{ z!wjLhv~wf(@E`6bk~YpDDr)8^F>R0{NMIlEB$k@@?Tb)3nv3)ONB3fhM)|yCDdRNq z+%ma;Wcv3^qPl$AAHj)7hb>XZyf~sf)vXut6R4lKsd9&t+oJ}1WE1CbeW7`#6CadQ zmmo3$6xR~$@%6p*Su{Ll5;(>6s)iJE*GtW zH9)7ki1b(0_7@NN!)l-*%jsiYDfFJrl;4EP8=Qg+77mB)2`v-HzeWCN=I~pS?_x?X zLu4n!(i7qQ#grD4F-fHsJn}v1qp0)8yGW&ogyy};Z?s7&-XeD^ld8HId|*PF08fMG zy<}CC!K@TfT=FSNu4}^)T3j^?%+wQFTp{0?af37QVf={{x3`B;%(og0QsSwL9sRh&huxcr7Y>KlI0X|c zwcOhCEd7!Wolb4+XgdhdOmSZbZ8O3&3TwHOlqJ%I(B@bjUEsj`WNnE@u2mtOa%)c~ zS_Xp(ZpcZ4XAWW}(`cRkI9wz{q&X7*9A`>ugcWk$BRc^l*IZY>Kp5ssE(#oY+@VpZ zeAp{s_&(e{Ob&h=w~Aa*|h-OUoLTW?VbScF)PWoSY#Zo12GFYRX^ zgM8)AuR5>8m>*_v-W&)u^h%oE|xN?V+LTw}v zjq+W*Q)sLU+#%L!EOMmcXCE4!5?cE16=(L@F#7sl2UUov=Zm6kdk7@-7UB-Uk@v<) zgD_VT4v6e1Ogi|`Y#`JI!Do^T0fiOF5biy>RYQoO`&2`=Uvk-9kls2Ga&&Yv$Ug}& zT!3g!0vrq`xDrr7gi(;BRJ!T|l%lt_Hv~>CR@{3+AcMD?11_R#@L*FMQU8+uWGG?3 zlo*JxfTCD_SP@oA<=u&RryvkB@N{TkdD9@1fgj?A*>!4~KnAVrH_}OJuuF$PQq+k5ifCSOOzJ@Q6g$e?=^T@8ca z+zHzTY78r(s^KE4n$-;4CR zRWi6g(I=)e39z06^YH;gHZ9#uQ)}96?7bbH8ZjDMo6SQtwpMS@)+8&lvbDv5wIZkUB8)>&Y`0PaB(mPi_$ z8dA7lYB00Jm7L3P?fJ~eiEr+2K~zHJqVyEK$)0!<(&<1X8G*1oqXY@-3qd19UtWqSrNtc$0*nE9U4C@?jTb&FRhS>y4jq_Fj2v`Z*mOhS6_B z!OkNw#yJhSZ;6hHg#N}eti3VjW!}>{I`gqk2v{03ENNun@kwvMx>Z9DCYDhfr@W)g z=U=qd1qii3Vui&|x_@!Dog}9}*0cbt!G;6wNazhRCg$)JQ)4p6TArHuq5Z8}QM1ya zLFurx~4s!-P<{3<#b03EITsMb7g_Y59ho#Kqos2T8d%iOp>WimkDX z4%#~dMP?Q1yUb3b0K1rD`wd{hU6+~J4Z6;~xl}^yE}qKYhYBarUMVY#@i*-cM`Fxu zCGQOnJTP6mN0Eydd>gv9wQ9jK{arNsMMyyFw?c0Emsn|hWE%*)$2niZvlZ-;zE7dD z0V|w4iki4dhh&bvioql2Qf?p2KAz?cx{Jg&5uPcN$RNgjGNgQ*-oOoMDbM2Esc6A9 z8&f#t^t~X{dwRY(J>r`Tgi~8zTWu_L3L;2(0Eb-IYfXViWnv~#9P5K8i4xQ!4-NIe zq3vZ@3dZ0B?Uevpd(#k^P~|cQ-+ndqY{Ndu?$XP>q|NOMV5f{Kx!fB9hjA=r^wbPk zphG#4Pw}XIDK|XYe(qDua0Vz_Pa_yav_?V?Ok<2-p$h zo03M3Z9`tW6EV$OYJwu&e#5ECpNhejmVMBv2`bXpDKe!z-n z#tp&=1O_995bC@qpMGWA1<*s7DUX1n$Q#aO#K2P$Kq8QD!DAoQGWgds>lGJWI7zFEv z?6Q2QEBpd!7RM(%-T-gH_E=1NF1yU}luytV<>!RD)C@Snb8JV)#z0_T;WAe1pNt(z zZvYY9pjfgu=a2Ch<8MHVX{U$M`h7nQVc;>p9l{7^@WLSOqK*oL-39$w1jZTo4rG4Y zg7I2#1qKAC0rZpOj@iJH?lCG^6FdA*0t0%Pv)NssTj8kx%c{gy2WM)id$KL;EfCTX zDbZ|f{RGPvD2s7$kim?v$Tab|?69375xZ*J9Jc8AAc|==zRjb*U9ul?-MXLc!vSD3lpUv-29N%Z zQGiE%1mJhtMBvxy;77tB;eZk~4J!d96|Dfp0YN!Jwxh>{gk{-}AJp7?LV*K365ItI z$m0;;x+|LU_|qKL%#o|y{lDyHX=8o6G|t`gV_H({#rPn7!**e}%)LVqSC5=fRQ_o4 z@O9)v2S?pI87%GErebf@^5Uc8Z)q9)FB1o==@F@w$26>M#s;0y^bzOOAE-vn^SH9# z6*Xguxe{6QB&lllx$4WFHg%bBvO^iOHrWj#yzSJ7Uo%Md$Q5@TYL-3R*exu^IsF>X zUPQFK@dOhe_+6D+VXaz|(m@e)5<`BuDv??%85VOn?tV{uO3GKeZ!>lcY6bja{K9Ff zjZ%c6+@E^OiySW@j__ov%1-4+b+=eK94;4_YKj)>v9B*AS(RXCdh+a6TCMu2oI_&F z2~)!l)2h>yRYwCl%(H5B!yX^hQ+e{hfHzn7|Gh;8J-?@WKPm{k^6BJF^W-9c9xKYkQ>`da-`=$j{G=`s9b-Amw0+Q(is%Z?_2GtM3rPO^Y zQpqM&NvV{If&mtyA<4|6xS4R4zQRV>FTjJRB9*>Q;C?cXQo$}iEgCcKD(!oUhWAd= zs%YH@1)Y1z47++%0M&geg7Q?`oKz> z(*uJlm=YBe+Zo+g{b$j{5AP;ck!mH-2K?)^;@E~1nz*sl53u^N-#)2*FL@uJ=0~-c zDIlFnK?MlPU3o`S&rFulS4H5y8IL>%rTk$^HZ+{zeI;TmAapW)xbFZD?QLbFinb)J zbN0Sj6!$4^h%ya`RXE-op=>(99z;Jp*jJ|}qkl)Y@hW$Zvlm_1FmCZd^Mk^)VJ=yf za$-?t4r92(R0Gr*(-C7dH?mf-1#BbIwu?Pz_LKwp)tkd6|WW6b)4z zY#?w|oF^fj*VST{dwH-KHO3n}*0upj2!Zq)|e+`Ujc@XmCc77^N;zU+Z z@{5*|`HF)TYz=apFPNy-F1m)9QoTHAejuvvYt!==x7~e?UrppYz<1!_JnaMBj>RjZ zOO{N9-r2jdS(~z%UcPoO);!{>nz8$y>OCcvyQx4|ju)e4stQ+kv4`Au;!63bcKy6V zLgjPuP!5gD=Z_n)hKF#6+TKm3BX;|I_K4ioeGk5TE!4{BkB+cyB)?kzutZVF)v`nN z0b4YmLX6HSKjO~2ugk}}D>Fh1&!zI6E(E8xY>j2w+3`2|Q~obwWMZxzvq{lG2-ijO z8fKR$4gipPR7ya!P$L{^|AEN8?tsi{u&k0&SCY0K@=thM>^8n;R1c=)V;rWE)AQNe+nih(OZ=}X^E^k}P2 zca2|Hx-PX$xW=}m&@H<RQ4lA%5FkuT{OGOWwN|U$gxp@_kxcXX?U= ztJv65Lj3^Ew9cv3svMUeV@Q`uHLk^{%f%-15?zkdZPnmcLIt{O%>hRDH=v>Pn=VZA zmwF$Pn@CVDT$+38OxG%IW9&n2!L)KS#)R6MB0!Gq@+IVt+Bsk{Ip%>d zEh3|IWqe)aBO+tBI{K2lsLWE<-#xnSs?c?Au5rP?V=&oky42V&Ea4VW_q%5Oigja( zbibI3LwWwCwmK^%yw)slY&!t ze!43}v!TuhoV0oUPHQKHfM2up`|Rg&PEh!yuW#dbTFXrL;8vef9f?SKK$DRQRvZvP zX~C`dY(L2oR#4{U(Na4!6I0o%m{s}}Tc&mS&^_;3KIZarau85{K$G?Q1YL91`bZVu zd+?7Qm5$m;_r@l*6pR|T`+HNkXTzyWb_-g~uXxfJW$}dfG2IL=%I#Rnln%;Vnw>q` zJf@j6nH$S<SIvPNn7X&X1f;a9i@TMr!f6_CX4nj8L{2fZlnM z_5;iJ@QX$<;^Xhp1Q1zjYR5=`KsInP z?WUo)bnJ=&=53Q>)Qmj2_x$tg4)dm@j90}!ggFBXpoOe0j1H6&QlO==-wVqa z<#$Xlu)wut&~NrdeSS$OrBn`no#OLSQUjZWU@4J3$x`IV{egicMuE=_j~~#8H8ZoN zsW#E62Y326XV@6w#N5Lyyi0Vf8)SFIyRJAN2Z>g|r~NWWSBCNrb(fo&P&b=hrY;Do zDod}=$=~DIOokx#(GB)ZV=)L<|B%$$Jw{$7@mRI^L^qkzcoK0h%hf{x(ob{_{_tCV ze(KFDNsaPyy>}9WKPv5gI)Z2EUtf4QeJV0vV6B-<(z@vJCEO(H(o(>+UUP+;%fs{4 zv!ly7p#9L;JHds%>5LNqS#V+CRry#oD!}Nu`cj%|H~sp^-FOiiNy@obcaAIf_O9($ z8c%#UjfPsj)J;2oA^N(VYx{xz$AH&P-1N)(Z8t*!XGFOzF^s3?Y{GKDQ{scr`?<6eJ;T}7p<1uFYP;EA|#7SgnUZg1v)!r)(*BW8! z&=m*Td(RZ@0Oj0*f^nyvL>9JaG^H&S#jb1-8nD?Uj-|Z1%`&Ul=IpH}Hd336mST3W zZ~~%iK+HQB`NJY&seBcW9ecBhhn9F{s|_vt+LpM}!Xk@1rHhIE33SZ%2UcMKq&3~B zHs=WVDAn?04Bs`mE0Pd(mxil2@7lc}n_aY=ecEHV6S#Db{K<5t=f$e#d6smV&ERAk z313ar0s{l@>5P1WlRWJH$7v<=;Tsz~V$4h^&@J6V@XfI#m5A&dTnokooVs)1LEzI5amyKtt6Xr1d=LNLkY6PWRoo%SZjzvHIGk9SSW_a%idQ0mk8qy`jmx z*=iNvCoT`2{6nMfl%)I8BAj)joH@9V_H#}e)pyG1S5a|I7mr0Z4-Gqt#Wk60p-ZGl zkz>&f!;VzwtC&AaBs#+uJPbos1fXFrA|hcJHlvR^D5vv)M#IU!BqoM}Ib0OvUM&PS z0rpum6;q`lKtwFAG8ipEWq<=>W>zdl8|B*UQ8e;C-pSySW=DT?!d?3xlg#{^2TRxV z0QTs=xLL`3Any8u!OGtpWle~@t?IHSsW;d{nY!fp?L(?+i{zIR+7F%gt8A9Jn7?fC z8<<`f38upyyG5s6u?Oj3_m>^Ce&V-TC{y2JT{&2$F1^kC@ZidJK}mF8wc5YhmoE7_ z*?B$$Rf?BZqb%;{x3U7HZeZfbY|6_V+u=0&Xs?@C_W7~h>6O>FgE)eC6;2{r#!a+h|5E%4-D=K}$blorWh*YoES_yIFPNx+-|EYRWQfO7!c)>O~!}C6NP#CLib3)qQ#PVku7I1zi zF%ZpvnI?F`)q2UMbi7wd$0+K~03Ega9l1xE8IC9$S`v_>v4Be_SQ@q9lIF=ebn9U+ zU3Ae85d&IJ9|%O3ut>5s1x9MFWAmUcF%;F#3+SAPvClXSGPkuJz>uU!VVLS^F%(>R zM~-_n3*2PcQH5|bKtfl`nxSQoHDn52EpD=mdENim%|ksw(X+$)=?UGNmR|d67K;P6 zj^Mzr7TCkIN%1`PI3dPzsHPr!RK#tA2N={WY~g|eY7>KFiwsQ8!#zogU7ij8MHVGC9KdcaSJ zB*qd7;B`mU;XlTyk-L$1(~pKF>}!&4?>T!uzvvu=jlD3Vcu6$P!o$ZoT&xq%yHkjB zUBPG|mDn*9HsfdJipQVE-~Mc5K3G~YsdIj^K?dEH+FF%>B^T%U9;V4Guiih4y{?x^ zfB0f{7ntzsDf}bgJUnLhxU_>`?*GA+ALbaVlZY)62o($WE>yt#0J(k=)WlVgY-i9n z`LyBT;jf2g;i{`_v2gM$IS1cD`5OyCB7yU)BSN?FJ@YQ29OWq)Ul!yqB=-&HYKT5SmY*INAmC(K~ru{(F z>?^+AH~U`kNj^j02%^x%15qeU8C?l32Em75sNFYtf@9$O4Nh#!;1FEf=V#t-iTFAJ zS5lcZp&|QTp{}BVexVtB2uad05)$c!>- zKE1+_^8IXlGWquMRKOjxwq5VWuSZX>EW?Mo@hAP9E3cce^h~u)#X0OP;f!`58fcv_ zi98}9UF2_I!}I!g;Sl^2RsPlz*KYw6qs&&~8N3+MSBNXRrA}-uZYx2=%YFGx}CF97$`tPjZXpr!3VeDVrPdo6%hlOlkcJps?W zVjuv))9#rd_OvKjzGq~w76`!&3L}ks zVEiF4?Sgq(i0vLc9oLwm3Ll{f6~nXjv1pp}hTs+zib6vPu+6|wO{SG-EYqr6p~&#) zhPFr#uFp!sUzFHGYrZ;&F$Kx5`0mzO4tUwVdW_80?!8?n_V~zn*Z*!k`nVHi_3AR_ zxNGuEpNx(y5`Fj{eZPg!@`CKj7VdkRa>HkgeCUJ9B&k2CKNo!@^*hN81r-c~TciNpO?Dv-!gX7ua{Hfda^PdwYqC|KGTA{06gC`j6z-wc~iuX^e^JyrL@pRMi=I8#0b`${Ex6E)(g+Kk?T{Td1T z(jsryl_gM~s_L#!yQFCy7Otb#BpZ>rG*zC1_4bmO?SQIU^7Kz*5Es(b{&=&_>a_zH zAJcE!=CAB#16GNcz1_MQ9az2cbkolDfd@Z2s%0*V2}=JF?ewaZ;Q`0yXn0o)Lgey4l~PD7xJk-GHRY(ey_{}7(0 z9eF*9>tH}XxbpOY;9)?McF#owAIxJ|fh7{I=kRg@bN3^#GKDAc6H(7FNDW|-*^XXh zHWY<^mkdGsD(timo#`1Kwx_0D)V9bS+CJVqJI9AT_HH%gaGv|J17;gK$MRg)Rs$&f z9%_r`-Yd!4aaWJ1U{L_sOrXPt`iAVqqK z0)hVu!N(;c!Ux~$g5L(?Qc}IA)_O#9Ksz*S3QypIDZU8-1*?#(W)9m?!DIFgH|*Z^ zPZO50=eDBX41>>SA#hgv`MY|DUeC#8w#JWwe`?KMebl@FzSvMY#eY0r#c=T(w~ftt zt@&v$d z8i*^PP)BMz>TpI3cwORODq0>+=YL##YG!ujO2+A4q|XzExY8_-xVi%|{?q45RS$SI zT&ghkSv_;BOjAL-&Ug*En&iONOD450a^P-6*Q%`+DG5Ez?y1SO+dX_%pI3O#>nF^! z>7o{k3r)A_8HY6&QE2McA49K%Yl|f~PlE3d|87R`fMTgQ}(@0tmHda_tPe)MkBO>ct z_-8+v*`es)lQp;SH?KinUf%u-M*J=0%`GfcLxYGJF4W5G9FhNiW*`!~lj`i^$TxBp z)a2?i4r&>%dj0waB<&55z=tA0NNAoEiESL7bt@hFVheH`klw6Y@t0FO3%1JzFbJ}v z7}Tzv+lF4xDBdn?qGF(DEMYQ#irM%TMl9}~2Luy*!D&~TFcBUeeNOSv4|dvxP`{Ov zm7Xc4C9ML+*7U(I1N6IoZjeSdWN$W5{s99tDE$EgkYcf>-@?G~sMzB1hlW384lqQ! zHlR(>Flg+*%KRI8e=qYNwfwEj+oJs!=J%2wk9)EWO_#8va_PfY*MLV16kjG!kvzF> z?@>JL8qX)B!h>;{;FURKPbJ!ys@Ikg6JD^>EP+9*v}QcfQ=5A^mh*pbp`<*n7H;18 zKfGAZ04dg+t8JHR1`J9+%TiAb#FUQlGjpFVaL{9^R0i0|KsKnV6-cU;d#M``NS zku~E<-hZE#lV<-t(iX-Fg0i&o%Egro!??OG1$*;CwpY1^p@s&0C!ZaTzznI`pDGXs*)H)pUKUhmv zFtQ-2F#r|<%`IeYjAqs-{^s6?0ud(!JIQXpHJK#sa9S45I0S^?! zt0iRD8)$V6X9{T#gS*4)mA>`Ce())-wH{xZ`&%iD=MRZPdn=tx*-AD_v44Q$K?Cjp z*@E55geqgw$&a6wEHvIN*%e1lB&lU`vH2hd+|N9kesfrJ=}QFn;+w--lkR7dzs%*F z82sz{O*o}Dqo%>vTPw)}M@@aI=5jX6%!{dhQj@AQ(juvTER3y{o8{|(jfX9!9&U%r zShl|t111NPg~frz+9bcpZf89VM}uzASu6NQT+vGZMw+eq9n$SI%0E#aR(ey)O{rlc zY}Ug@`x~SD6ER?;sTc>WSKnAf>|W+?gJUr>P4C;KYp7ae4!(d~Dvb`>LEo+<+*zX= zMMrJC2`{=iai%ms^xHugW|{|kkmy6jgf_V%Dh zw-kiM(|qO8vtD6b`E6G*_^u(+lz(szAB0Z-r8hXhKiSs#Wt2d;FZFMsL@nb?`j$2i zinMu0%mXPpUaY1*?@Edr6mFspQ_d&rAMm2eR(ctsF+lD z4eTfv)-O6y&l8 z>-#?@KJIy*fLES1cYAGQY|r{6_}RfVWOeD#h={CuaQr&p`R82pBo zyvkAz6Ms3ROG%dizI$ogJU%@kOS}Ke!+T3>rS!{F32kc^hTA)Ag-(z8B^2sABPqDb%;GA5x8V21fYZU)?|^ z1siGAZa|~z{8ZmNz7###fFAeV+{JqPz%Dt88`_c`V{t;6Yc&F^(!8pBJ*FwtnIim@ z6RwqBUP%AkY;#?wGroN#`_jNvxOZmdga%%xRQiod$H=?6{Fb0C?E0~;_=fP#1Q(8# zAcoWFj9roM;_*BmeS6a+YO5~cW#;wiM|Mn|0`I-t>ms{-%KGz)g_gKR+l?h`=MXJ} zS;-PX60WC^Go_Cs-!AYMQ*!;dxobQ-CbMk)?lRZt{9x}$obVmdFLfn{CuK_-MklPx z`ajts@AY0Qu_+->vcYh9VF3Gy)u${B{e^y>zwsZH(M#yD@R`Dw|4Km^`>rD{XSn1 zzfG%q4x3M!YUvkdt}60ks^6r!lj&^xHXFCGO*wW7u#@T^1^sKW6;q{x=;p4c&Kln+ zGw2ugQl16&709#elTZI~Q{vEYW}jVztD13`$13O;#J5FNxaRee+MGs0JeQfz8Z+FL2Fv2@60XfAYkw-gmZ1G$FDC z2RSHY3E)3;<~Jp{N?92nz7TLYONG&W5><)PnOoGb5j&u2eXO@bg|X2+^Gfl@tcdRa zr!ptpvUQIX#LR1V|HBNot?*cC{_OdzbN?n|=X&KlwVr-?{O8Jljr*?{{a4`T{~djB zUu2|1^~&&@)A?U6btvXB_J3Mu3SSkuu@<;SaXQ$cc0EQg7+=81M0M*Roy^7VwOkIg1cd7 zy~?p)ssLy~^l$1xmF*_E&E{f28l68f4A`98BK^fe^wj#f>mT2d{7Qe5&X>OzWt%jX z4G?~dit|i9Wu9w1F zJ2*ktc*bshs3d{E`!yWGO7GaFqbJ>!h-I#iE-vU$N7AbS=KOLWv~x_r-jTab3+VpC zsrSa4YGOnf?1lfXm+f?W_!#V5AJ)Z>)^cmV7|@CKDrJkS(}`Aby5 z{ek1p!v2h!Di-;D=AR3@Z2>F}R`tM>OAAt&W?e>ON&JPz!eI6)vP}X*ShVSqRAwFc zXj+4NJtOlFdggWkGdEb9Eo4_pba;XVoGTMY5Va>Y@QEMBelA%n=&`QM#F<_zIW585 zv6Swf5kKTr3%)}7_h%?g|6#kB1dhLl+@_|uAeCO)#WWeSl5*OnF2%RPhU+@DQT>y* zF%w3X_#Yjdchr>KGW+0cgzYA$>)a4!`)Mg1PA=l4KY|2-u%yS~3+_p3}# z|GnVnfOh>-Py2ou2=J=3iOt|7c^N1>f0=ubwHnf;G_N*Fhy&YzwSGWvJWE zUIe?U@-k~nldjFpRxOA&Uy^#H+w*bwP-uNmD43BbKN%l)kP{XxSm$YL>+eUqdj&u2 z+ZQ?5bq*m19!g0wGrK+vwqBK6&Mx`|H~|eTQLBCW!9nH!epz9@pA2(LAoocxfBQco zbx?U*5XMh~M?9QPqgGNJge|n?2ZX7Yb^iN+`PGBrp~p}AC*$k);QwW${tqcR`c&CW zbspBhADPn7{+B`Gf5`gZ#-#rp?CeFK1b^ERtSTKO-myRbknAA^zO48p_7ivxq(K2e z_z*GCE+RsFd_we(Lg0^sQc|%B91FeQdO%P});{b#1vT4I;S*Xn2-r;=9Ixea$jIfq zct@ih{z%6(Y3ktV3%AOqX>Xb*_hY{vK#0JV&CmA@u27MUDGriKnD8UOy&;pR$xTD@ zUosSTyKfa2_zubE$ugbQ?lE8sGdi5J_ryo$#8=`BSG8$+^3U?9S`}7M)`~NtY$auJtsd=whf=Rq>s^|7b+4={ahz&@dc+8her7U4NXL#Qsv)JIa)3?jT)C zUeRRsl%)C$u|_?G5Y|rXs!$Hpgk37EE4^TYdQ&-mFPqM!(&={T($$2_z-u0eu zMRYZ-_`6rLx7*Ck6;rkD5A2pRQqJ5x+^+S1vG>(+QEgqrBLb32Nh9IVB_UrP@owPDCMDq}7Cq?=ScbzbUp zCE_$?e|0Wp!o~@q9lr(}kq^9lWNK{s&F{%VlaaTTzQATTOfmxJ)mfWjc?#qZJn`Pg zywgdnoX6hX5>%r&iX+7;w&EaCQoxg`t+^)+Zhs9BaxPx=b_iF!tLW>ABb?C_Ga-o7 z)=X{VWJq}^W?A0D$x8QtjAODRrQOLmAV9k;Ej{M7dCtxZQ{pxK0HU-f*tT*CmUank zM+ImO8?Td~XwfO0@=?nEnjV*w880#09=B4IGt@cy5e8n3hl3N|L6DpsRl_YT^dmhG0<~p6E?Zp~<*c%MW+tL#GLrcJMtKMj|6=O!@iO z(V_^x6S0-%DBjZo6IQh0lp=0SsdO2_^BD5arrj_aV#lDc!i3oOkK%Z z)Hve&rcBqumM93YYKdgU%!$T7(USBJ(P)+wg+;>csb6+&+aEo&TNV_=@pytOaq2Ns}^A|x#V$$RLx@Eo%3t2l2vmwCAW-Fq^zov>1R<*31)f4As`(<#2IXznm1U8 ze@G>sBMQOq6v#|Kbnc25&dU>#s|X=w@v5|(-B62M8$Me-N-dgnK;tjxw?bq3d1|*l z#q&6&x31t|dFG;0?3<9Gr;V|yu1 zoK(;bfMnp?a|U%g6!&Dn3L8~I;{2~4Fnm-a3j#1f*SRT?IhDAC*n9$LjgH&cu>XJdj8>a zvJOJdVMIWxkSdo9nSCrxt2E6J6=wuZmcyj*%e@H#yI#^9O^jUZE)jn+_>7!KV1tEs zY=M`5jg_eGHnTLOGfA>KQfwuR2HWxRqZaOHQVO}7(n+Q?ahna73NcnNHXB0Phbi)6 z-)eY2)5?zaPAovk$>=3(RLFQi!W|JPGQsIk%^pRKP_O5MJh8zDjw%QD24T=Z0@C-U zB;-Luy^VfK_Lj9%!owFSr%+G$hQpJ>Ay&sC=2L#+&hVICkPqI3Sn%n57o?CM+ruc^ zB6GDz!9szYhm7`U5BpmoQ_+GHmO{HtDyQzIJ*Y!=j{CG)6bK6|Bhm!q9D0muIalf) zvV~?)Zf6N;Du$v*j5GtHZs4e#x>Kzp?7@^}Zl_&XMvr=~AHH*{wK=9BTv>2$y7{7+ zSUH}4T55&qv6Q1c3kTPD76>p@K*lqaSOfBA=@FJINw~NDV+n5$E04$w!&}=Z98z}f z0mi%ERRP?kY3zN}2yfzRD!nqJ5}|zyMk}&dL2ySk)xKyhy%8V)Fvp^|VKb0!RgQZf z%jkD6mt-O;S)7%7DcaY&PnlCrO(HX-F*Jo4)bD3Hi#@59By4}{Bn8BBfHi(8IcIT& zch*@}>L1at_VvdmHt~KmVtEp=9F@GAPK~e(l8KWgLX*s*4T-;61*%^nvV*Z1=tcid z#GB_qI4uEBm_sx-s-z9+;cI+T>zI}W8@O5$Q^{v}-xXrC8vXDkf-|=T*D?f37)KdKLwHT8vfo{T|PFn zLX$#U{xKcunRv=fk9RpQnO}2qx|}@&`<`{G3G=yd4biTO3=>6q%>;W@jatzjNm#8W zeMZ~C&FHgX!bE}ug*e{v1@wOFt7{QOznDo+-YTvX5NY$C<3(y7ck1V^m= zv6ml=;$1(U%{sd?rnG~#E9a2Nb7aP7OMjX-A$R97`zPIIE&7eDoycZK$@x=TmQ$+D zHyW+J!1!ciVVgdC$prBq80{tXupSV#P^pJ-=H5P3s|MaaNi{vZlfET6S-Kg~Gxf=% zPV$vO1O5jC=vG(u|Ja0r^1UgoR*44|d$kt@a5k@hfpOM^Y|QVO3YVW@T+&#hI#GY!&-R5; zpTH^cqswg{&LXF+rZ0~@3O?^pUt^q@)(MtaNg%54x}))4EUjmD#A zS)Bf0ts`$CYjK7y`t|P__zE6aCEk@gY@2$fao2>vNffTh7s8qIwOvKOwd*X@uHenW zpkY(5&@K0`sj7F%?>OuPJ3_5W@%vW2?aAv9j7(-hG%4;qzZyUNjF+O(Uf~Q^!K1h8 ziCgVp^w!{akzpnlJm*wVXe1oo9J5CC6hLu!lNFUxW5v852bIv9a6Qt+JwcFpv@QrM z9mnn}EZft7`WXg@+x=EV$-2EKDpuB%rs`Df7>;XkNI2!OikCxUes7Uk40u zMmq{Qm>FTx&QLNbxtDs0<<3oL4kp|2N#`CJ+8vkcvM%LUoqrLCBWz)ecmDJ%nv(XN z9yCrUG=&joRuXyi<`6~5MXbwxp%ZN{1c>N6!BrNjxhk*3vaD(a>F%RWCSd|bs&j-vn_j%5;5+XE58 z3J9=h;nJ_Y%~qe9B?$H_qxY-(6E(0G#^e`souUdO%%Q2=0r8UrSUp4fNNxMr)2;Md zeoc^W2QUh2Kl@o>+cTt(n*)>42swFD6MkbrtOurwMI~x+x}+wGxne#Lt#5_2|?ToEJMeSWui(vi+XX7g$!!0V~Ny z`w+zKhD6Pkb7llQ=}rXZ?4yTeNO5iOA~_X1PD(4JNRHm@$`|9B{5_QhirKO`lgz^MvUzvrBH>Ka0UCs1scVr z*&A{8HfZ!*LOP1kb85|KmPE)Nw^(}IMrsl{85G$m=PozRIDMGx3`yfLq^e&pJ9BK# zPF$Kvig1chLWoDEcvFFex_C>1qq@Di51XPWc@8HjDWXkIe*MZva!{l_kx*TgdlN?? z3m>73^lWx|^*EnO#FXCPA$rsIY|3od;@*o_^b>=^8T3<~;u&HYvgB-Xpe8Nhl=>`O zXXgyBTvy|4P0)%ijqGCjXsLZy(?qw(^K`~V?U;6SL=NY)mRPXjlw}Fbrxj3yzu?&7&?uS}uSXU3bDfY3C&K|b6 zj=N}%5nceQK=F4H0KU{aeWMMnZAg23>XZLQy}?#UHY+^ToZe8FtZm!(jhFQ0B{Q8u zq@Ddh__w4u|AEx&&!iZB(oR6?)89(P0GYN(ta2{=s_;JPr~g1& z@kdgK-)m9=>o~8}(AjXRrTY=hmzn zxOldF)Il?g>}qS`{2Y^d<$HypD`J}^J>Ui9+Ui9c9b+2ulgEYGG5aCB7(8d-n17XkxKa zIfd889HmK`$4dWAkM+Nl2XTn7?_=}^+-j+>aG^=;-H&=d3`O*uiw0rPuNS`3)Pvw& zJyMBtw-Cp)DGuFIa<4f=+d+k?-E3j(KupJ*nu7xImA7%PieBF!dTuknL=*I8cwC64 zvhHahQ0VlnGXp=wD!`yKHp-o{GM1N}Rz{=ZL6-2#YrSQ#5 zAjmYDfPV_bxs!fNW5#eR1bti}L6*5wql$8yL}Z*dv%MuMlPBGyYBk{^Tf}5S>hyt6|+I_^W+)4IMyIMd8xcdFP4+cIi2SruHra6|Gmp;LY7udxs=w_#Fbfea>!0^ z*y7qDu@W`T7ve&}7M+~kT|u?=7X9N+*4z2c{$M`91%};G-P2zykgZs|&G=KVBHC!% zVSC_yD5B&CbunW*`zqhAV&Czx-H@NBC$~-a$bk#BUOckU^gVpsG+2rKAQ^iD3LpF3 zb{I3MIujn?Bq+HYy`d(tbA;1ca(_d0L%|6Z%!ea;z+cRE&)UER< zNUTs_Po%q?bT$}a!qEST^w|wvCp(6|H}r>`ZzvvkWB3vSICOp&`|nT)?Z9~t>7Q}` z424+2oQ=Bw;`@`Jqq=_<|AQ{yXkgC2)AY~MafrUar0KDFk}b<`suRiQJ!?Is_+uUS z`TnV%hn)Xh@;l|u|4F%@?E0-P7(=sVhb;ZU{y*V;YoP;uzcW3?$mjoPWUlPQAA}qX zYi`-egzssc;qchISgW@pXU55_)X5w=L-k9)Tm1a+`{)KuRQtgIf$_7OyMN77!N>x- zo;v;G(2TYebuCTYSM78gGVsQ0tP0w(Q_YvocK15aYu_s?3@uA*6;VfQpV2W)#wx1o zQN-~oYfP9*NR3q_-BuS`#V-7GQT0T)5S&iQNCA0RjYhROI^um~Hj|%3HEm>~V0wZc z*>P>FNP08HarwE=eilNoD?~BM@J}>~a=V7daJqG>(ADAOKlazT1g)gFU55|HdEF01 zjs7~cs(woBs00)8#2?lN&~yWI+0>xoZHM8%4d4&MH%r}9X2XA#5z)^Vv*T8dn%&td z_`aUN><%DyhHEv!?m&eX2w_o!m?#2i^^IaXA1q5i7~wVnE0BE(UV#=dt#m!f_N(Bf z*;;Zpo&99HUC5Z`B|xt|8{xM`p=q*mlz1T~)7Gg;)-|1r?v22?(GLMW>#B@@2Wg8| zT(15Z>-V55N!7iFX8+;^upkg5NaOcxcmK}z?;wZ=&hOaT_!(u(>^iJuaRn#ZTy%ic z|3Tz$C4STAKNZ&9`Y)JiWZHyGe>31eYVfbE3+YL)M%=set!;cI@D(ex&M-p(zlAiW z*|B#Kr}mI%DGa4x-d-$iDzy6rwkEflpamAgs*`b!Af(6j_0l^57mWTo1~>*dd>IwB9BVNVKAT{we>nLwwy?!bq|QWp=uLW7I(DXA-wv8%FApGA6U|L|KlRg? zC@ZsY5sFAknwX0p`*G$8_2~3srx%m+ZMJU$AFH>diEHgk5$a(STzut*Jo5!Mumw5p zUt{lcLxByT*3Zjk)la_dP)FKd+5RvarOE5(3m5ADqX4&szb3EHNG3GNulKN)yvia6KD6s?neF=?i0|L(?~e?Wat_udIIg1uEnxWZNDE`Ar@&rGOi7d2y7dC%#ozDxMN=L7av z?tNTq+fjJp&K|Khrxon*b<3w{({Lr0_~eY^!XDWf^UTKdp&18L1a?7DRSIiaAzYrd zUbC?KbP=O)@|hxPPe<#laMOsA%q}(=E&@p!<-P|qj(;0Z%r$b2CQe!{pU(LE5EQ!c z5zOc`YJb>{f>e7TP*p);*+1d5=I*BVeP#+agj&s0&r0n5GH8K`Y+g1ycm%Ze(bT2QD`?!al5RPS-$}B zfp~*PVO*3W$-e{fKNYfy@wwBHdxiF(NV(rNqM;e)M*!sM=K0?-`YsM@`zJ=f;r+h` z%D0N~=J^8iHg0}nq>^B-IlUah@m-Jq))M~@2&HQ3|7Pg_XbdI~SH^r8SrsHBh_C#;NYH@{ql+ewOg-MMAtQNbNO@$sh2K=?x zYZ@%7IvFfB`T64y>$To#N6;&X+q)(hLLMo<&Mcr_Ly^mm8!fgqpJ-$`xML8_hzu6u z36udpCM(W~P4eEQ=|~NrOe1Ax$3dr=*k**YI2tr7ZZr^Bz0406WMOA&X?}RqN;p~t zE|;0o#>JD;ck3)W@+GO5okMG8uvn{Rl-37DN~P3k<|D{QiXWLR-9nIF=x}{&K?K7w zX55mOosKH{egi^+y0S_0LW0PYK6+^mPbp|(aqXt{SZuFV!Utm`;o5vttnbY@znWt|!w~u|;3r2?8zjo3v`|pUIwnWV} zrQcA5PTF+}wlO}e{yoXxXSabt!J#lF|{a_!rH zQ0{+VTa1tV2j#rLy^nQWyaGRZ*NLnXI^!=re7X6S6Acnw_X-TUzy-lvBrvQ42M^$Z zC%u6`=oxXa1Ek<~aAOi_P5ZE<{5sEcaf8h|IYk)iQQayTGgF>Fi)N*Z6=@EN z&*;3`>lxuEVg-a}^H&czP;Qy?+PCT2O+E1+;bS%^$-dq_93@;7VYd`>I>_dUm+)cP zx~J$!oBHfmmWRaX=dU>w%17#D`_aZJPnC*3ROwZ@JKD)yP#+~ca9Kn}Cag11Nln?- zzmo9ZD*cZYqO&)&Zg27`I8*^z^{4$f$xZeAlm660B z@2ca*(aIRV6gsSMpM(6`LI#;Htan_v2USxxMqVd&8~?HyNup$_0_&$&Jvr02iAl}cY1~!M8DByECYI++y8{lL8 z_d5{(cTU*iECxmhsrd44I47AAkMO&yoP6N_CaZDTKxQ?JU5W9kp#JIlvR$#McOp9T zU3?w1Ha@;dN$;=SqP6kfK}6Dcxjc=1siGJ6l=da@z|%_+8MX@P#Iac?*;2J_-;xb< z*T=k~M44RZv{XYpft%cdQ>xUTd_r0ow~Dk>Oqh9+-fFK;ElwMbv{XpDrW7?0UxUT8 zp_HHSDlS~#WXF!%4so>AJ4;V)dnWbO{Zo@oC^7aI0*!S7Pk?}$i0K8{ifntf+Do{k z9f+)x$wkixRQY&{UO%-D6TST^uBX$$-dIGX^2iqJ6+)t_Po-jN=78>1d?pzUs{Y}* z*s06ZkRbiX{>Z_X!$nJB#^PBgkGI-0J)kG(X^a|fsh9U(2b@LE*-`}rv;idrGU zuD@4GE3x7yl05XR9wJcR^h>XgvA7+jnJ;}v4T|G68T^qZJ#cqBMW{X8r22(B`{8ZDGe5P2c&SZ`R z8+>l;?~JbQc?{CjsbA8yEni+F%Ry>iewMHl?5AR6G|&gG_C8waaRt@7-Q)gz-jR^^ zxZ%=b4flNW-^`p?abNrF z9{E|RrnZ1zjwBDi=7^3)xqf6(yr!#pf#khyN#$)?qyEp5@7&PoAIKWnO~Q_8ksK|UDe%jZ#J5cmXEuQpc_uOs{ z^bOFK3ldRpZ-1MsJTE%Q?|o(?MahAw&_J4*dzxJ8cE>eqEiz;=1M17){ zpsl>hepjuSwA}H<+P$5`!q0j6Utm;ove++}W(N5^4?R>G(xG3H&qB3FWY!xvN#5E9 zUk1AI$%_{&w%k$<7w`qiH|o@doL$Ma$c(?TAuaubn%n9Y zmdP8|H~nWL1Rqrxuz1x>#f7SPMYtv2W2|-ymdu+QUAQf_3Yao3@4e_bNjJtOSFbVt z%C{ltVWvX7epHYaHb*KQlYIXy*q6N;Ly11_9~_r`?PtECllJ@^Dq$wlu> z5cac(u)925O{N}TsJwFMZtF}XeE0)?U_S*t{}Bq6J;zOZaAhBwqd@Rl!m5%5Vb6L# zj%W6XDEzY9*k{W+JiPbl_Y6E%CS+onInw2suP}z;4c-Z?=x#+nC>Qux>srCskl2>k z7JA0vSyTXit3qxZ#mA4%rutl6fvT15b9e(yaW7*|hlZ_H?HTN;f{#I%(@UNc2}BdJ{>HVb2Sg&$3?|Gz2$spfav2H5m3QDXdHPh- z{hqZK>za$CV<`;&uW6Mw$O);dY;X33Oc~#Lw=#3b#UeqEbH=%lMqj{FLb!}idU*}>cM)$<*NJLZnX zbJD}LEWuZ zN^Xv+VaJuq=Zf)Q@GI-Uvg+`X^*}S>XQwK)3;&?P(CY!JTK8;9?BoChN5Sh-6}uC| zK8@RJ_jDv1;}%3}mWO9#=c^Lq8+lMW`h}#lR|{*7Srk3+EiODm;QA&H9h8UhtQF^$mGJcwr z-L@or_R4}tSX!P@>^ByL7Tqiq^$Nwpr?Y)loe*@>+yH0Vk~ja}w=le}=QJXd~8MmJ_h9b6-o_iXHH6wr#M+_jP0H3q(FqH*N2_*`r|vloU@r;H|TZAb^B;*GH)AcbG3*? zu_a}&Sfsxa7ACPoOf`{u;Sw(zA5cNR~&njyuHiEN})< zpD<>0dH#Y^g!ELZMi>9B?!GmJyzG;1=Qe6G^-J(^FfA@)A zl@QnNH(o$vn!G7LF&^%x8KP}zUbsX|x7F;+)6*ZO4BA4WVQ~TlP<-_ zi&|ey^1`J|O}4{Lh#53YD6xbp4scr}S(^e1XT41d__>U8h^kObb>eA`*O+7vTx&<8 z$q>wXHe~A%E=>l#E(I`>j@!95_cnE;H~A>vsS778fp=fU!nbwF3ssRHygy-eHgm#F zGu&~sEDmEOczW_)<1JjYc2l06m{8BbYa-3OLj-OSQGUd!WT>KavaZNia-Gv;UB}6S z)GaRgHXK!JW$EVAxy^I=MG4%Kl)bq+jD?lXetMWpO1IOGlts?EA^gpWqsy)=a{i=+ zWod?RrLg1qQ%*DYF5>%GJv&bF3TrhA_4u;Nbw!Xqps)Urm_~e^!hf6o!0w| zTjtMC_jGQ*yDISRGp~p`?%UTfYKZn0jj^23R=NTWbq4QE~B`SVk~o@2M? z%ueq$XcO}}GOKq1IumCVgib=fZEFH@ z{4>k|n~8sE)DJ@GgEA*Q5t3O{W?Ke9fDqTpvTj}?;g{eL-5Cc6?cbk`gcv*ts|iR0^g5ckSQy5H z$1xJEe9v=3a^mZagwh~>EMpizAzj!F41$@j8H8UbZ9@Na)`-zE-&D=~c}7gel)IYf zA@bA(lAu5bv18=5uGK5d??7LQ)rbkOb`)l@3Lc*!8OSTX~I{>oJi?{YrlQ^fa0LBuE#Q<4^v=1O0AaUou zc_5YAahn+5y5{KFGbV4JE1@WP8Y4>aMUR3EeT;FphQWJ))}J^)29VEjB}c+`|gXnXJ43J z*F`PlzS~({tUZ67&f9SK(pE5zqCa@Xw=C4@&ewLFhx*3(>&N;osfjU0zA(e3->WaQ zgO3h#auef)GQqX4^Aj~T=!UuG(Aw;3=eJ=xMAzoKG574LA}1cpZHQyQX^j_KG??y? z*z{w6fQBsNt~q!Mi;2mHnA>6mC5i_!^-ag$`dodAe6jKQH@rK}N+jOg$|-#BCy}#0 zJS%qe82jWdQ9rAOz0McdN!^LQ-Z$q5T+iPW9(UJehyfy6bt5ioV6U(z!x*(IKB}cKMIg$0n*`md%b$ zSxuD{1;2WMH^Xa+hvHIyAmQ{u%OyJ{1Du^VAZ7=No}6grhu#eJNXCPjAbG%HdwwJb zQ$8sSp--zRLS)sNPEgL#&P8Ig_~CLhsu+T zHCf=$n`XFB8T=6B>+CI|eEK0m@La>Q5CR!g9CKdv>Bp(To_22g+m_;Y;}n&zh-i_& z8kiA$+Bg$gp3xxMb&6QM8T1Gagmu;ps@l~zZ&bd=>a2AJv3qY6KKgxh%zgS{`LBR_ ze(SG*+BGzpAG5lxX+oEC5^O-ZkTC1q;$ZAY%Sl1r_1sLKi={g z{jnvN$OS`^7M^*h02M+-A687f-E*zXIjrG){n+dx=koa}rwQJX8~hg^IQy!5fMKi8 ztend=Aj;XD*YM2R1qaalPK7m_1MN3=vm&BsNy)M+r7mCnsy`^sdAhE9QzIbncm_$> zF5#pj)rBi5lD-ppmW5#CYf_P@rObV}^;rB&c2$Mme#m}RVKg~(@F0Uq8T3krh1m;J z>$(;TxhsiBHOf9T>WSQnem+Y`bRng2=lr~m6*{9V%XHrrec6k3qty?&twKgXW_bkV zY54FC4j3yz(IR^0QZtmsaDb!H^8qL{T#X*Wgotl}zZnW1N1@R3HSk;Kc=EH;jnzDQ zx&Q~XHNNA}(82HlLLuiyL*c_i0LqYc#bG#k@Xi4YoE>iyOmgfc26I2o@Vaw{(S~kT zXS(F-gq^H?EBPp~TjM?ZC-N=)U2O2!P^rw3Wt(GD{1vcB-V?#b@)6vP_tR={df8PO zPB_)|;#?IpoJ>C)BXRkISGuaJ9pC;Lp;b`eHz?7m6N?z!L#hmd_9LfW>{A|6rFUck zL=f%5+`)A9eM*(*CT;taXs>o%-cWzKT3&uO+xs1YK-wWys-?P5!Lb-=hht8Q$O90D zH|zB$cfV>ucwKOWVIP~}g!cBqZ+fVUe^&h}uVI*J?Acc)Vm1tu--N3l`=dXPeT*fkw{wx>E(^Pr*m`}E^RAO6xia46@EQqV zYH`qBZ$uW(_$6cSlofC+`c-pxfA&j0!WNMa;-j!}*Sw6P4;^80yguS(!Q(|2go^1P zf$?#t2gwQbYNpom-Yr%lPOr7a)$$(}hH08Lr;VgXpCm8PZEhJn!#jQLjGEWYYrWRC z_Z)6GFo~qDJfu#|_sL^f*AafRm31}xjbUj;W#VdA&93&c#x2{2fst))O@9pm+JzhY z6R$bY8{+IFxr5t@#CJ0hj74(($FSwHgPY#Q#8|HIJ6}~B%Z?Iv>l=2>DF=_H-Cpd| zpcvhZx~Wrc_z?A~uyW;^4{>KUc3JaSq zT+W}2W$y}#`Hf-fUk(`D$xOG_WSz;gh_=RJ-lEq|G5f8-d zFLF<*+Rh%&Q&7AV6kJWia7$3)lZo^D=8rGK`cudPS?L%aFGv*28{Jty{f#YYbm)(RE~me{3ORbUm_)>QC$?wN7>}@sp_yN_PDlP?>oB27E(snpED{$6c->YBLf_WS@%A(LVK=iK>rl^VZHjJ z`$-*;K|h$@b6>B1$L~7oQ9buA<234_`6J_@<_ux!6?9#U*HMZ7w57)R$Ab?rV*|5* zc4gq3OA@Zdn0b74--y-(7#qZsdaqa)+K()P$Kl>`-1t6|FU(o9{IKX7=gMfA#}{>D z>=aFiWHEtflD;XoIzvI*0lsT{xN*ahyobY_dF~2(4pXurMoqXY@9HBsJCrE7{I4xF zrUgNB@L$J-8Naf4p3cS7UtojJKfPP}9*WI%XZw1`&SOHc%BjT%K`#=AVi6avH3roD zX`w(YQ1V*g#zVt}1<{QEFfs&yyHAh(jt|BJ=_0+y+1PjZxu#?$rkgI2{LTf5w0y2XlY;Z<5&G?!+Xgf5>!xn-@hax_R~#xcbzs=Gr8 z08K?(JJ$fD51_)VGgqAfibJao%3$?+prfTruL=McxY0%cl(w1kP+^+5T_iVv7Rw_6 zdp_MNONcFUH>gwtg7#sc`t0BZQ*j`l#SciPTJq6y0!*G>nK6vULA z`*2Tr=~5_!awpax44@NkdV@}a z3F%<5M!solqV#Q@%mS^GA!flzWY~Db@_9`T2a)Qw#iZv{Q^Ok7ly3>xRd{>)gA+9}MqXcvD|E7e(ug3%zAybor<{l-7@PI)FEC z#uSdq8&Kc#C|3%IMpx&bSE~TH6YEu2e}&Exa>0#Z>hccExBN6=UGlScdYLe&DJE z{P|-tI zN%r{C5NT%xo=$`3G8Q8;v@V^jteqzC38+6OMg^8If?Hw-jA1y4x#g&M*cl`o8^QO<2 zy`u7n^Nom&>=g;SXtk%FydRd$15eyj_z!Ctx!M;Ho2tOE&4q=uV6Yr|2rY*o#aM^S zy;-Ymj=2TTepSW^A9*HKG2$dvdnz?q!LSz#V<*Sp2;zoBdC{*@Jq@7pG%Wh7@ z76tl2sNIZS`}`+v#O<@c=hlo7*j~C1xEt;E9s_tKSo<8qmZlp3>Y(#zNxqQzv^70f}sa*|0$-pg*0?0Nrn^5}UywqghS44=jgT8fD75}vz> z3@Xr$m63fu653u;e;O|J9_%iHi3A3pI{=4qz-Xm}L-Dc6@c?tCxC2an zTG8e?PfBsH$&}zS*w`d6T%@!x76N_%CcuS&!A#)GJ8;lW4{Ff9Du^kEdhe+GkfA7BFb zFk*lYyMrJVgTX_YVAS|bumg`g5im3!XfPNSma)$;s#K1NR2tTemr8@$5MObd4R1O51Y&ahIP>J^$>zp4&H{%)FXHI90Iq66&6ZFCXG0X5BEl2 z20u_R;qx$g5k)B}!TjNO$P|sim13x3B5Yt67{WUgsvZoc3ipIz#VOr^8N;bzFfuiu zH@pW13$nr5JwI>cp1Y#2nz-9iwwWO_CFqb zP+I#12G$+mbnOca+>5fuzh4*vF6#l0sQLl}*Mvf!?R{(jF0D}eghsFl!m#cDky5a8 zKn4!%FbsC*5YvJEEHzYxJ3y!uj0E;f!5AAXG>WAG0jLHCAfN!6g0efz)PR5l&=bK3 zV3dFhObLKf#nfty@k`}^(=+uTut5`&fi_lxhaxEAl(5m9a4f_L0Q5w_(MpitV-c!R z^gxak`WHUaK0d*BeCeD5Fk|9()+>%-?(4 z!3)3jHf59q)V*K__j$uW8}9Qb{R^K6crz9Z<86L25PmV`7ubC5_9Di_z?IxECawYI zuI-B68`}jSV0B<%0GgMC?D`o4bX!8jpb@O|J%*p6!R0L~XVHuBN(8FM=u_Dd_?F57 z8vLqSi!kpooC^QI3*~D=QNFdirB=YiW4tBXsFLCN63|;qhiyw1GK;26~ zCAMur@fN^TE5-i1*N{m3-fK`Bm~iwrFZ(0D4F>-QFW&b`l3U;T|LDa8D1cu|h+ywg zgQG3r6^O@ZTX;0$B-(-~+yY*JNJXo`A0kA71EW8%6(HtTP}HCU%zS}uE@B)Mxa*rQ zup1sA1A6o3;@BeO!DqgC@WOcY!}aN6^#Yp?a*~ z1O(<#1P@vT9@PQ|MQD6w)b6R@x);`;+$$jdzX@i_D0NW%M##OOKPmA4HCT#a+z|>B zc)-1Izq=QN|7qA-WVoX=9yK#;4|-zNFiK?XOi4T{9dx~QkhK*VoiCvSrJJLuGrT1O zETAd~6SOpZ1)RuZYNiGcN5Jbr778HjP+ePqwLk6f%5I}d4_>_e`VDW16)J8xqhuac zdGY$|_pBvWHbcY3B_B|Q7o9#2J}I@b866hgLZ!Q+fII!IxhZIseiyU|fJ;K7+uz*j z2mGIc77B!$qF;id6dXQ2NOzf1W61X}}OJs5AxQN-%;IG(S*p2pCKc9)&oJmIbC_Mk}R) ztb-*lYpKdQ+kQ;^=73Pn1JlKU3j!}2-uJg!j1z9|#IK>D5 z?9UD`f#lAN<$~{iyumt00@dpM8M}2HC;+{9#P# zm=#K77?o-Q`Pt9kun)gNFbL`>=)!YSs9#{?U}LU42U-AaNtrc`mzV_X>y}AQNT2fz z%TIdwcK^b2=tblt+uo&W;ztrLJg_fw++mJyZ`ib$uu6VhUz8m~jaUAds@?Ih_h!|R zC?b=k=6Q`H+PnNR7CDZN=w$O(4Y=KJEGYAYF6{6i&fdK1kQ0xSnq|H?ot^kP(w~o= zqT0X(T*>Cp@W6w3oYm*%L}D;vPfj?H;&Hu5jrh1tm6jEefSR`)x}S2{h(1a*d@3bH z53M?1zlWPLetW1Y%x#o?>b7>zW~wvKkWncWaGsk4NC%huja^jBt4rmM0oj|}eqlw< z7CC(5k)P7p1o!Byw(j5X8@hTKje3uMvPYJpmaJ~H$Uj!sf_w{p_|kEg58>AcI?6wc z*eeD%uJY4Ou8<@K2gPF-7fa+n8p{E6uCAXYs){7Yj3K# z(}Ksm+>gmsj@xO>vr$%_6|dlzdtZy1?8rq`sUrg^Js2f@=R@yfA$X6&IxHt zX*gO1=g!0nqP_F)-oLIAus$(klZZVgzG^Q{zD&(Sv5DFnb|jbp6R!`-$Vl)Pp)rrl z43^qXa1lR&4zgCi)U%erPv+_vV6RToB$k04-R|Av37%++Xuxj}%OH|_`*7+rJlP_M z*)lWB+Ae5{sL`RCc(w6!hAEM-CZ~AY`Mb8;$6BvX! zs4o(1F>kBPYj$qcR@u-Gh-4`^9vL#S3^vzY5Ag&A_0b z_66p$Avw6~s^o+^dav04^VK!slbM3P(jt3=q2=I<+oBVQIUx+qW5E>3OmTbQe8p!Q z%A#U+0AJ$bj{OrVX2Y-Oh#kM)eP%L$FD%C%p4#X8qkxF7(q&4Q_Vp2&z6L1)z62}Z z%HqHBFDJmLxU|7fQ1o)`@ujxZFEFIXOJ*{ov@bB+$h#MXg>^cJj~2?xTX(<2WQbfT ze+Gb3Y11chwA^Ms0fR}&CD*P!i6j1q{&xBo+~OA`s3h+1XsnNsKLZ~vUHOd~?kYdX z1FY`a)(<{u3Fb7qCnAkMjZ+20jBlqO-57Cq z#6)+B!hPDtDt+N=FOyWrmfpWiQHUp336!}{D{K|k1Qk+*j!}Z# zHKdGz1i%6>ZA=2tlYV1|0n{R&T9VziZ9N1Jpo;ME5&(?HD*JR{4G_7t+E=JLFJRK0 z8i;|~O#ngnXKn4DJY7Gu)H~2U!f?NC036;zhcowy>jp-l^hjt_2lg<6zqdlv_o~Zv(L?(OjjOVD zPzD@)uhJQBf#cV_@MmG+65(%om+QPwzrctw{4f##Qw(Q}thxapxM!dn1N=#WUsS=A z?JJ4V@2hgey!Ve#e}VZd&B@=}KzN9qIHkKwt}%Ei^dCG0a)eJa|A4`s`pwslc%p=^ zM!cp8vhdrUg!2TRW{N`Mx=~CkN3Wr5#SM_VfrXZ$?13!1KxW@ z841$vVDJB>jDpa?|9z%EsVHj{j3Zx+{SeFp9B=G`Nda|U`_Kh8%g6S5^kGJl{JJ@< z#uhj87Om#oW~x4Gx_Isj3@l z=5D_z6r1RcGIpj@88k1$LLL_jX! z@rb*ZNTYLC>;KgD7I0B@UE4SkN_Qw7Lx+NN>JZZ1AV`R`ln8<#F?4rIcZZZHDJjxQ zNl6QWSd{qf8A9*-xu55KpYQwsXZW32d+lqjYn?MQd)8iub9S{MhO6$yg6bFS|NVm^ z$MnB9I=7%fE<8###{R5ZT-@rq+8~f2I$>=02E12Q<`m$k&ow43JJzycb3_pkpKbirR7K*gIJT#t zo5Rk+LPuLlYG$>>->1Ynl^h z+!d9I<<8!ld)#mmxc;IzoCLu9rcW^rJEp&oK6fK1sVNDKZ9Oml-HpS}hxx!$0Evg$ z!WmdpaL@77F(t{-nU?1G^6=a)4KCgi0`TqCBsiQRT zZKZsFbf_>~3yh@MfU|hwJZV-nc}?|fK(7XP3iD3Xg?Z9YAt0aVqyaP=^s$~JH8nNi z;1dNfV%a0Vbk*ukUp0juiMdv^wlsJ#bUzoFvk)4ss9DO8YPOyd`qGeUiXp@cytQ^} zp%wM?tUuuEDU7YBVfR#Ipi?@v{-d|}te34v1q$2(EZsAqbLKP8Cm|tPI7KG`z>zO7 z&%$AAK97t@1CGY=5+r;&a;U2UCPSbSzEYG+&9%s@M*6(Mz<}f z`efaxIw=Lf7b3?$HltMMv+;A{aAOMfDJLT?nb>0MVmW?hzd2q-ZWGuAWoh`#*CwW2 zJ7TI>SQWcjwprF~_y`wT>|n^oKkTR(^P#LYDJEQ$@p#9(Sg;gx54GIUv1d6n`RlSI zTM5M+nl`J6!wdKMJf@1Sk%f*+cHcWx{O-?+@oW=no6!hSSu2t&*DcvWtY=AaH2oU# zOxjEBR2wQ6-->9NbY*wIZnd?njF0v^&pK@nNiSc;uu)XM2U{3mHz*cJimu-bZ}}|Y zg++N$sG$_~fso@`S<+6qU_-_^Reb3vsmwUNVWJPFS!XGlSgK~q4F=0EmFYySs!SGV=|!3&=IWG`(}G@;FtnT zz1I>AuXuT1|9GDOwivT%g=V;C{Ng(`iAG!4 zd(G6=O6@?9PIHm8aiG5Mqp%}FcXymGAxOnIFv0u6#YBj+{ncnX-j&gYg%MDGv5Y+KT?glSa4C;8 z_tJvioASz_$>wc(!@qE~4&ZvNhdpU7<nKl;6lK!k?EegHmdQqe!@t$oPp#peN0Z`193h{zRV-OGTo28im;lqQSMM z6y^J@tSkJU3W3r54J3;J+tEC#rJ@zxs4Vw`2UFkmx(#mA{G?7*b6asXM%VH8^lX-R zbyW^WIoW62VU4j}DMEKzW>$0%7S56#E#{SYbqRk4H*45`Y!ppd`6%}LURQC&{o3nK z=0$Jy=p=W{zqs`a$@FV)-c(%qyfv}% zwZW4N++A(cC`J%P$8N*rLH8=kosWSUw#bA;{fLv-RN} zxr9Pu@PCeeCHgb%MY9NOn^%X;#-fkKjX!fq{P?oZ5F<{V zc*%EH1!F-BT?Ji5#tr(2s2%^Qx%!>ZXAWke4IFTTM^SF*>0b_Xbh2BlbNmQs+WNn}@i3UE2>HMfNn$yx=>c_yAxUZ$d*#E@og0 zW0czUUj5YnGpR~!u}wt)&-a+lz0uw6%IhrdvKhg*u@9SacCQLt{g6RJ?ajRRlZyuJ z2>QiocjtPzLa;%DW4XZ3tbMZ7#Lt6~krJ^-UWs_SK?@dq2B43_CLDZ$ro9+`%`*tJ~E9!xpM@#f3Ed;03`7m>l`m4_MqueT){Aag#&T{hXU%R3_ zjZaMercdwQMR@w(spD3T_5Kz^QuM$-dPt%)?*DsiMA|KhNh8+_x5O2mlFc&wEpl$b zmw$JT*b8Kxe{lpq2K>&RHGdupum{|9pZ`UNh{5q6Lj?Q&m!kRO1#v55)FPG?W z-WT+mO~mv4Mzho3`Y86r8`vB6ZTz2HMkybT1SDbux7Ho@+VTB&K!ITa9qI>84AAh;CoGn+I*>ab``ox@*~I`-UFo!g2GX>4e${FwB11S8XO4*3n)<+sKUc3 z(Nl;mE}lP70^lN=41D`<9{8=`5to@5{xcmbCHSxd<@|wu=lsDk>GS&$0QZC(05)e2 zgpb1Zt~*-GlYAHz_d1SfvO};Fej({5w)DSF7f2Sq&*L&{zTVtB>K;3ORDCYze_>Wy z;oUekI_*QW8Asit5rT5+i7oe4<8QHl(eMa^WjX)w+cWAO&O7-BOE3Ub{N_X$-_qHI z)17!FzkfZTWcLdj&Dv69VMdegAB;(f6$ih8Cr*{GF8?(G?h5aOv-kjyslltGW)Q`n zAfm_v`2L;+9xjk928M+{qVBB!2BJ9+2{8Rb)G9i=e|kw5NE&|$KJ$n}*r^j!J>o~S zN9|af>UEh~^po9IwmtN~=qm#m27QMhz6+ zn`X*hdeR4rsyO-$bj-H|8QB{dk(z!te8;Rs^h1~X_fLY{8V${@z&3CE=9%WvIlBR; z2Wo(tCfdExn{sdO%RcW@pU+?2Jjr$y+C8=!3;K)YBwTW6qP>Y?+ zqJ_`p?w-!;P9}JNOx_SPJ$SDhYX2^Cud^KUr#N^MOb8SHoDk}r&Js`Xzo(0;IXx4Z zsosSk(kmIGHb0ekYQ0CV>4X35QM&Nm60hdQ;}n$5AsUM(hrE|2GsOq~N!b~G>EJ&? z$Np+xor3rE50^Lp15c9Mnn;N6wL9s%h6^pZiITs=A>^f5|4TfrL!N&Lwa-#w^gqD= z2mrB9jaf5)SN*pB_xp}GAXSZ@hdv>WKpbm}>)dhulCPv#6&QNWlpO!(!8bUFr&XgN zgK>b+YyD<5V%C|6d_MX7rw^w_M(+h~SN=Osg(7(F7;Qb5EAZ8=qzGz_n2Je zJZ$&@KG3D0HBIZ?(HSqxV?#C-~f)B*@(NMqGxktx3>-aXJ?I{CRcP zJ4^yT5K{H`x4z2Q3wAQBlqkKlWZZUj?$h>{kfLIq?**$R}ffY8rz zreu-rG*dkGZ**yGDa8VnEOYUldH; zWqa2pZHqb4Qx+ETZ#F&#S4gU&Pd3PPJnUxl|7LSX8@Mt*-~8$H+yTPXA6`~eJz^?N zh+>vv6%L%U{$Tpe`F9h*{wGuGzmFX3(jUZ?e~ldNk5GRHeIjXq$?pYtFj0M_KUYVL z1Okojt~0C*7-epMa0ne;maSz0Uh&ch%-I$P@6e(HgG@G#oCB zm>6{NIv9DK3_?#Ty?qT68P&flpQn|4T%`)b3p++R$5-44jpq{?do^Fgo{w!xDkWrR zgWI(jSHh)6w!~^P_{g_s@6A*&exOWH7phyU13e%cdI|e_)#owRnlbc%WT*~o;i^)- z+XxNWQ1uCfJ?rk*t62c11D$1on*iBR9&QAO@0@hqtR_VJiQ!}zuP~K}@SEk?_g9E9 zw25@@gfq*A^o*b5#3AOI-boy!RhFj|Yo~FL4824<<-b|=7;{Y^hS0%wJ9NGKhzOCy zFk!m5dd!2>MDF+}eVQA_-!~T8zW3tkcaO>Kd%-_WUP065+n|To=8u}$fo{As@N(KZ zf8h)Lh17UvXpwbp_zCU@;coHq>WN#6)U^9C^bCn@jyg2f3#YnYNQUQLXMTG5G=`Y`xqpdk#9oq6(H zssHLh0DsWN?j>HiR)i8pI%6At3#7FBs`X-gnQ-PjRqjJXgL5S&Ql-%&-F{maEI|@9!zZT?O?a{l< z-28OcqxS@&m%^}pO;U0D2YKEHp%LSCwXsyYk& z#;(Yy7td?zi2twSrUn0Dhkw&CtLk%f+py2Ih4b6G;%i?Lbal)^7vw*xHB;HK^1`{= zmbb^-axrm!ya!)3*0 z1a~zPDjiHX1}lXR$1r&(m+ntccEF%kjI0!19~)yv>qeY>tC4+rMJ@SQI+sWIW1?A) zZ^U4%V~_I?UNTKd)*rT=xtOqj(`Tcn5B3ULfk9_7<$7I;EA%P}Hf>Y2uB+-j!h1!_ zf<6NawTcSRBXSzQ|J(5Y{VszY8#=;%DoxbDm)o^7iNRD9Qg?(a={Z?fO_G~s$C5TE zMjv2VuHPoR<9pY)mXSj%>jG=v6uzF=Dol#+z7cR zJGxj(Y;UOry?Hseh=V0YQm!hRf@jZ6Jt)`DhABitJ9nI=>qm`HjI_pGHMhd@xL2xr z($^#6sqdp#8odpRsV+%^4b=tWQsNc~%dZHMGT3F3r|^e!yuV~APB&Q(iLq;r%8f9o zy&jX9?=-@jCioJ4!60t9aL_hs(9Kz^s_cka$U_i6e1t7k*4xnP`a(xQkxUBwVt8;_ z{J(vHnlL1d$<@gB*;tQKGo<7$#zQFwU8iGr^9y(}aJN#)z~8(ngR1lm%g zPWeR?8k72Q)k~rlZFVrVxoRgTCX!OuuI*^u*o0}NdTTX0`mBo!`Ke0ie)zYOhd68V zE?MdMuCSm$%?s30gO$+gBFeLc;#A{=h%gGwY=z_Y@NT@4!crd*&F@Gp$GIoNBhIGlpIDx@g!szKIg|87|Gu*AUdjivjsh@~mzdra~CAiW%>+21IgAa%82$ zQ1erXNVlD>e)4CGTKyy!@Qgtfx%hIjvF`be_Wfv;1+>)^$n6xg#4b*nga z9C2AoV|G0ANbn4%Y;KmC_Ail2J;rfnd~gMIb1Gg)o>T#A&jaEC;WJ)=n)>Tn@hK<- zVx@8CiN{*SZ7vWI*?nP{WSFEg&usC1g!6=jbTOn&xL7T*Q0@?+Cn!DI=w;OIRN4us z0p~jwgH5P=se5sZt08h|X^oV!g?YoW>%mN~)QU77u)e#g=h^dJ_PeYKC)S=;`Jxbc zcBxgt73?Xiay8_KoA4k58Yu{66K3V;G)gq(Wkxi|rmU%l>tcQT^h;hB z5(hg&VhL-CZT6VrcR`5YUPnBXoCb#Z<|RH94yCCiyTNfWR_ciuj2wJO^0x4B0SdK> zk5Olj`A*@I(m;0JD?%?2kaf%Jz?yXfJxFoqE4f4w=V)%-W=uxdkywma46!`co|tjY z+g^wbYao#u=~K&>191g-v09398Y{P|Z&gFBOfBBrSlU3JVXAUg(nz^F5&QjCwWQ9b zzmJgcB@9UUl8uRiHmu!NQi(2T*&lwanG;|~gu?sp#u%Y}`s!bxmke0O@~y8FDJh33 zvCfUbA@Z0Od^hNuJws~JbiHnh(mRYQ4~RqI$0(p;603J}Q$$8F#(&zssBxtE72z&C zQUV2?Z+MNadRFFojN1DcmXXhbc^bD!b+Q1J&&4TDpedGt z9kcxrBWK-m6+Y{BRJ_z*MwFB(^Wp)K2u$4|VU?3c^Fo|!i&Ssk5Pd=x zhDHVN1{v+4a#?&XZxOhv5T9YZ?NZc6YErWGBn0*qrNa4|k%> zq91vXQ$WSFlo%E)E$Q3#4wJQLOu{TFYP1SF6>lzgccyiI+9L@o1>Y^Ts^7 z&4^ylxOz)Wa^#qH{&?FxUneO zyhS8gy;2Dbv1)2^M|k>JwK;nP(WAU?8+0&pc>Qd+D{9a~YM(_36-(8ulIcheb13RA01w0l840T302$i`ty#D zLY|_VtF!27D8+DzSGEK|bB37=tAce#8GP{+FZv!~7dU0`eNZQ~zahVBR2lCiHKuMU z+bf~2Zclt&;r(4gL43g{jqWR1+Kf+*TdfRlXU0>$k#WS;%&J*xDB5PbS$~s_6s`Ui zN-t#V>O2#YSZk&~5+n&H)6-I(P)LdIh`jS7@|?~RgqAnVI`b+6RE_Int@@^#gYD}V zm>XYE%P*iZA>Vcn(7P+7o~mAa(^8jI9+|#f6H0iwG-dV8xTF)?iXokfq-uO8txx`i z>}Yvy8zv0i(YO)IDE z+-dP6zVXQFt+o2%Z4*^ZdIEHP?4+>TZv4@JR87()>o)057N2z;X;sAtWzh(CacEwF zOE6216nc}v<%(r`i&Q9sW|tIWqeDyyQ6iF0E83>QtdG(YL(B)HkP(Lsm^_r^h+2v~ ztxvO>B(2Z@@5-Pg8!;s6o&}TXRMH^sFJax@zW-8 z7~?nOnxv2mG)!wQFkO&V!Aw}$XhEkm^kIUo)!rH;tK3eJ2`J~7&Amcs4=D^bl;+JZ z?cK%RPm%ggygm2X^Y>yizVW8#^{t6pYA z^tl!i`n-01{dT`Cq%VkI(f~zCZBlM}N}q@b(+s)Ivs=#J_A6m#{Lb>yeEp<@-p7su z6iqT4ekjrWd`YL>WM~FqjlU`(zbzd;ti%sXIAV3VyN*9BgwS&iLXRJUsa&@SOS|tN zo9YEcm!e~Va7&D4Rx*+2kEiTK>3?wc`TB|0A4;yucw78R!!oA{GpH6L%LAkresn9_ zAW7xe+;wRSRw({BDkPpb%wS;(t0|HOJ$MJ-e~&gE)wn-|chmOS!s@(<*9EpUrjJSf zY?A19EKSz3Di@~7>pI_0Ipj|=mKX6haf7QseIpb#%Wl8>3|(rfA{^9*&d0J%Vt7Wy z_hMYDndCpfd^L;RoJM-cg(c?p3n`-OqfYy(GG?>P`=rKsQC$IH!P)utH#Lp>j$7e5 zTvYEpKi347J*T@zF@tQse@BE&SuRbRxL3n^Kz$gkTtJYfz67P}VL=;WxAtK6VhxS$ z<4@U5ie6eYa96cW`|7yq)_2rWOLn(2Mx=?zvQq^TTTM*bbPTq3TNv*}L29LCJ!10h$U;YyV?M;cC^f%ZqJ| z!(BfvVjHaP69!8j$qXmretPLP+41&$lr>?n^gX21m%6yGMl(KKAnsMS9){8fk^8E# zSckkR+i2aqy}n~FbPL$2TH$@hobhmUhUMqR$rH_3@NTorcXeN~$GAOjvd)OJBO0!w zmUrL~Y*v6fg4u1-Mlm9A^Lq^y%HX%N@Z)R?DVQJjiq0|EqtRM5DjSgrP`n@@Z zR(bp8UK``Xk5vKpVqre8rl-~%ja=Kbf9A>MGG8nuNx5rT%cXA3poT%O`u2HZ-%%%H zYC7e8bTf;#uaz}YjpEhb7TJ9GuTxdK{a3Jh9~z6=&aX>q>92E1!CVk#YKjlzW|D3zl|9wbK?$ouKdqi^%B)HDShlqu=AAi*-gUkoF*ncFeJ zS2y^(<+bn&DJaN<7w(RCWXgHOTSn=<#D|lEz$?PS(P4~{qlW#JHxR|lwcL-0aVPxb zSlnwP*R{5`rqhuV>VKnG7_k1V;qn$%I!=iILC)hFi=L|a=1QD|{xxV+eA!Z#MsIKV z2Bn9zThn}X7k!42+n2IL`&9bC1MR>AcHqHyVC|)|UbJtVU$E?zk`WJYu5G>P=gx__ zYbegj8kTt@KG<^m)tG@W1f~h9Z$A`Kc~SWMLUKq_;=AaJon$KK4D-UD_od9nZMPC| zVr?j9B;b(N{%$zwmJ({~&fh&THRBodMLyEB&h{daQhun37y^IqCO%t*pNI_^46(<))~IgB`rsVYK!jkL*dDKGQ=Y+}N1y~b>+ zo<|_T^pZift~(YnfAMSBXH+t>lfVwb4{Er}i-VPSMN_2>mr{^6_R1SprrfNK2ZVJU z?p^^Chy2}>b>iK)AC_$8lyMvJ)V?br*`s$B=E5-hv48;&BZLY^I5r?UKP4X9!BNP| zSAND5?D{l~MRY2}R!-#}kHLrm0Xta;k!2XRpo6kCWW@Zj93Ps^73=pmlo&bdr5MY) zdjUnAXxR3vq`r#HhVGTQfmf9Gs{q|7ZsrzsK#ea$%cJOt*u0|Mc%JPhJ>`rR7$dLl71Y)bUSGfI>m|RsRT+srpZRq__D++Dvm$J zV!QGu4(UIM%TdIVXe>vE)^7I_v%#UWqQL|lM0E>>x$v1aJ=rqOOWYb=P0?kXokm&edNRDpiD8yt;bzexMu}2hr%qx`F}L=Ri^;`hd;wc1bL^)4l7D85jKu2&z6Ll~Uhk5xpSY!t73M~d%SuF|uXP~i(Pdc+?W=$r~FGauq)-iq40C(DuKQ+Rhl*EpO-fE5`Z^n<4sKj192`bQt z*|bW9rM{f}_M2<-Q09A2}jz2Ep&V)5d<(F++uC8wh)UVl1?x#Oqa23g?sCB2BGZf1u zsT8K*beLHa?@oo~uSZD|(y=U54}Om1aG_+QXBTFW8s=3Zl8V{($+k>k3eMKx`-PN` zkR+5Ppe0#wZP0t1Ap~leY-4o^(W#|LmdCdbw;X10(pn}5mOwwP3rFwDJ~a1NA}cIR z(L$x?EQH#z61&2YkBev;PTa6xUp_uDp*P&_MM&W)(ihC(0a&IKd`0-B)Hw5#J9B4z|81T4`KD1hZnb`g8jAek3%K5m7> zfnY9A(KB+=^#I0bmc-cw3-HeTDV(dRA4B2d^OY1_7^b)2!|l135`-aY`UbkjAfoM{ zESDnamsdbM+#ezCa{~@$gC2FoMb%^Hy`@;lm^gZWe}eqw?KRCy$IzrX>!2&+YV7xN3;x-VBp~lqLB^pG-Pc zsQ@_eLarWq&EOJ32i3w4H9S@VKwdOy%30M|1|XrE(+?Jc?kDrrBsZW-BMfK`vTXFv z2)H0VW^f=gnBF$Xk;0kCQST}nU-X4jS&>3H52AtU;uF}dC{;Z;r6qdxi-6N!0?R3c zMw8^JOnA|2M8wh&s&a4>R37P^8brJh7P`?NI1ncv^-w2tQynx;@`gv>R7+|qKO&FO z`pd`2^|B@)FV-O6Wv|Xm>BOMOJP;_1Yw%Ac`!R7W)_}X)-!G1ixu}?1@CdYPRwj!h; zDJ&02=jC?DEwLnARJdmQ4Rb>4Cp8%5-YVePlT?=FH{7mboVo`9w|RY~iC z&Z%njJh@Z{79|Fi6ryM*N6jdk7~YG=vctC^9Z!4fhqT-4_f147%8aO8P^#tiw6$nZ=@=9;&JE^P)^o`2sBpR^>+pJ&&2nefkpYm;x^lQs_Xd;|8!}_o@N~_{Szx7ZfI;o6Tl?mHJr1!3?e@Bqg+Ite z5JMWs%~Q2+9>*5A2r0Q(;U!I*$7>cqN|%qSk-F+6x-O?{3)8t-zmwW#}y* zvGf^I;7{sYW}8&uVM_3P8+!Nyvm{#x{>&-EY>+M@7=Spw(mR_J^U2c}siOaMWH~naY>Cm?uVVtU;nny343-LT{639M4nd9+3qyPV}lzW{DxADMM6mc2(u_(%S~RcvB93KD7#`3I(}U{Oed-IcEpit zM~&-o?&Ni-==)oIrnlSuQS!NM*s(~T@O!gOtGC;+6(d9C(SlXM?dqg!D#6wYLn+Z5 z-1PBC0;2T|+{KQ>&Iw#JjH7Ctu>@{@Ck@;8k_C5_+}?-EgA0UF4L;JN(^~tYV%GHG z=zU9cvEzqi`l1t-#bdAlTRMe;HgKQFan$!tphdKRfuQ;MZW1U*e007O10GL2265^R$~lK-EF!}OxZMdvRTllt zVlBM`-#V(n*sy&J#)i2xabxiH^mv_c$sSFgQ$9P-z2+m=+LWz!+rzuiI%@lj%eTWr zImCjrJ7y}Hxw>6CT=5v5e+;bit)r@IuoIx1;h0BhA&#xRVG~1rqdf#Y%oF#kc1P8+ z|E6B`Hs1YR=f@74wI;42zmO6rBYv#nB`#x57=2;%nf5WY|HhOfv7zH>$FEy?ds4M5 zFtwnI%*?bO&y=L{2m3Zw)%$Dj{dztfENo?#1=^KUD0K^92_kL#-fmfavMp`P+43jJ zr>}sdC4t&<+sk3!3pleMMXBs6b4>}Dx-YhI3r^BjImT|Xx()`rsp^gin6223%4*`$5)B~Mat&z#GDcLEpuPRQ1-Jcjy{}Iz2#;(d2ywvI9%;VMe_-(vk?i8 zl+@8=hxGJQLEA`C?5ck~wLRbCvmZ$`fZ0O6J^g8mQwDCa0@milgbk$@K zV4J2f*Y!i=jtD^bf|(y6 z1nB;}J_d%9=>AYT^f^O04)BWa&zXwq4=STp$X7q^(%}b5(G&W6-;V)UC<&D@i!6Wy z1avLY0uvwgWBKl6dz{CO&jmyjhl6|p=*IL0A>0U6YjoZR*pQOMM$DdFdf> z*j_rA7Yc|4>8IFOYw6mTV?+wA>SXoUi0bm=RSO-Mrp&CMnmkpczG-ULCu+$*(Wb?d zsl1S36wZxWB5?|r+cmu79#MvlNRz!)q)Wzp~2pTzjQ{agm$}&cGHRQ#>8g8D|7x`4AJNw{J1-a znx2UPn0djAqO34F=n$ z@rCIcGEtHA%eO(LHZbMSJggqKPP55iwfcRETazJ2TMc|*`XdW7e}`2Ed42;v^wF02 zq{63mCwb7^#g96B9iXC)R`LyjTmMO9#2=m9Df_-4yooN|rhIZ1eDinHH}J?xgXKeXeqib_v(ar0QSG%b zyKQzU$I?v_8P5CWuLR63*&WOLbpypskR4amFEpCD6Uu~5t}dyX_dw+v&fvt%&+yaD z9#fv3%<87SSJ&xAk7|&2V@t#NyMTlld}F%vjJ#;vT~jJGqpIt#ax8Eq10j;SQS{uK4?7cwUnVs{@$i-jXA^J-kfu4MXhIOBN z_cCVG8R6@z3_?Yx)ChjEimte=vETbJ^E1@>OZD6Q^WAv$&6-4`d@+~lT+=?dlPCV@ z%wyvrZhB%biLsVw$Zr*y=CGF7K}3Ds@oycKz*ODcvk(!4EAQ7nXf6LfJSIbgEVG4; z%Ee7HGd}~m`j^VL>>dtqZbcmF@GEvbhg>eLHzQ3c$=&c@HZ^@nI};ZHsZ5nGz5wAN z<@TV~O|X03(P-i9=K6^HVFm}=s`D3E(YxI${==BoP4^FB_t>gicGI>@3@(mpvzB<6 z&Qy0yq1lS{!GEJ)gWqVwkFaBE2CLIF0Dt?H9m?(qeNnFPNP;T^t|U=Bq(+@0TZdH< zvf_V`Dl4^dXEo~r*EQX{si#HVE1Ygfz zNXvl?r{x#2=H5H5qMs+Kq+aPvJs+4GBl+j#WbPl6lUH}mi)h=6NVRA_w1ON(gj!em zRVvrc_g_fN0`GA9Wys80_Dy$TQXNoc9Ru&jxuqQ zqagNQQQ*$+93^1XaiIMTlF0lQo1-17z5l7CwKWCb9c=y^#l{aR)?c(4To%lc&kHt5*tsEwmW5c@yba{ zLQU=w-EZZ_5`Xfpv$6>P9nKMLq{gTFJ4hZGy8BbXu(M@vwLwM)_?sWWHC12){+yV>!U@zGBTD3&W)Ts%V`XR z%iZhM+HfbjXe8<|co6BFNE#*nV8t#)KF_8sy(`|I$bjQYOtw4{>MQWjs@F@T+gq*R z07rowm`(!R9OB-9pC4abGT1h%wnALTRu{Z%WBJ0uuZ8&K8joh~P!BkBnG-FfoAi|8 z#lY7jr95jks2ql41L3P6b4mIu_&w4Yx*cenCVEB5p($AFfsKQ1I*#3RKT^l}gA#tee#3Nd$M=+*J&Y9Kvi*)=ey)lhk zcTD4lZqBw_PwCK-5R;Z#6DmG%8rAItH6&45aO%;-VDmqxDHG3zY?d6JGxy1pTQ4$A zog@)DzpE)7)i$tc!rPe-A3J=H(rz92BR6^l5yBTw2&>I)=@5&(pIHXGzuWFApx-Rw zYPuTeoP$+0b5yzX&{)uR!yS~PLpd&)zPlNjR&!xDO5Y~@pQvVjRz5dXsKo^X$Osrf z^cNb3rfMAdOZ{G=zx7V=K)FAp-BAx_Yxd9FTsL10;;w>muyNmHr70ccU5{RZ@yMe8 z!bH`O2Y*UmeIL_4t7Go8G$Qj7Mb!S`RWXh~%R~Mom4B9p=1^@wDTwQ-br9`~AIRcG z018xHShM^zSMLmpe9qM(5U#3}(%ZbR%r? z#^x*4v(G%Z^29s^R94^L&@Zae^CfHR%csBqP-D;~Cc2qBjE zzO#c_&_~?1xe(nHeF{Q7j3TJjaPBbH{T<6KOYnF9e$Fi$N$}_Xm|R4kVs6b-ytw`i ztRCH(!n#G|fsyr(vfhWDLhwIjKb^I`kbLZA{y?xIYYTIrY5Z)A%v*o^I zwtxTJ^{#sRCk|yGY=&#=@7$gyss~!OM(gIJ+dy{^POT^j*YMV-(*p?bcs?qOdAfor$t$*(*ZbKEi!VrjS&Gb9oG1R&}*(k;&RnXr~&z-5yLM&~y)ogkY zInB-}_);vK2c2NC6kZ?hI>Z`t_sgMr@D){Rnh0obJQ+dNTipe#vLJ&gmUj$TSqQ(S zB@W^X7Fi*r~ny^tW zoU3dXgBGkygzEhOz6uhGEWuA0;6gvfV~~3cg1Dgt3emuhp8m~iI7~jx-?p5V9xXf% zR8PcbE>A^a<`;7{NmV$N$H?={<3Iv>1=+SD7a55=+_=dygH)Ct2fX zf@}maE%~=KplOhdM9kQ{8|cg$VUfpIc9ADZ;!im{{YLt4PQN*xWeOqrm!Etp?E1mI zN9gW_8li@!Y2WBVpsFkQzdY+UoetrR9x0T^S9^$G`d<{EUZN#q`Eu!*`IAR^Mh2XT05O(I6?M&Ud=~zeFiDZ0Elb6Cuz_yIo

gH3e+yPaELfq&Os;UMEtV^|F0=>X%Mp7S$|EX~vG003cPG7eDw6aK!9QxJJt3 za|!ssN}BbHrI@SC#ms1ic#JSQ;F*s%qR9>Jk4#6Y3eJngCrw}e^s;Ie^lhKg)*^7g zq969rbOAs})~2+WZ?jzuJzfA>T~_t`_u-V#w`IB&;~HMNz6drvc7#8YzC(zWfTszx zR|gt@_X@UpXucJtHlcLl-UM_&2|4C0-8e}!PIHg{w?= 1MHz - -trig0 = monitor.trigger(0) -twait = 20 - -async def test(): - while True: - await asyncio.sleep_ms(100) - trig0(True) - await asyncio.sleep_ms(twait) - trig0(False) - -async def lengthen(): - global twait - while twait < 200: - twait += 1 - await asyncio.sleep(1) - -async def main(): - monitor.init() - asyncio.create_task(lengthen()) - await test() - -try: - asyncio.run(main()) -finally: - asyncio.new_event_loop() From 6be8902a80d9d1aaf8594cdf442bf2e9312de509 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 21 Oct 2021 18:07:01 +0100 Subject: [PATCH 108/305] monitor: Now moved to new location. --- v3/as_demos/monitor/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 v3/as_demos/monitor/README.md diff --git a/v3/as_demos/monitor/README.md b/v3/as_demos/monitor/README.md new file mode 100644 index 0000000..9595dc7 --- /dev/null +++ b/v3/as_demos/monitor/README.md @@ -0,0 +1,3 @@ +# This repo has moved + +[new location](https://github.com/peterhinch/micropython-monitor) From c10a5c214c65e3a2a8c26f6685818dd3a53cac0b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 1 Nov 2021 11:04:02 +0000 Subject: [PATCH 109/305] DRIVERS.md: Add link to Encoder code. --- v3/docs/DRIVERS.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 07acb84..9df62d4 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -340,8 +340,9 @@ this for applications requiring rapid response. # 6. Quadrature encoders -The `Encoder` class is an asynchronous driver for control knobs based on -quadrature encoder switches such as +The [Encoder](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/encoder.py) +class is an asynchronous driver for control knobs based on quadrature encoder +switches such as [this Adafruit product](https://www.adafruit.com/product/377). The driver is not intended for applications such as CNC machines where [a solution such as this one](https://github.com/peterhinch/micropython-samples#47-rotary-incremental-encoder) From 6f730871a8534fef23ba10b817056a44d7eab061 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 8 Jan 2022 10:03:13 +0000 Subject: [PATCH 110/305] as_gps.py: Adapt course for Ublox ZED-F9P --- v3/as_drivers/as_GPS/as_GPS.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/as_drivers/as_GPS/as_GPS.py b/v3/as_drivers/as_GPS/as_GPS.py index 5e4b6dc..b37a311 100644 --- a/v3/as_drivers/as_GPS/as_GPS.py +++ b/v3/as_drivers/as_GPS/as_GPS.py @@ -324,8 +324,8 @@ def _gprmc(self, gps_segments): # Parse RMC sentence self._fix(gps_segments, 3, 5) # Speed spd_knt = float(gps_segments[7]) - # Course - course = float(gps_segments[8]) + # Course: adapt for Ublox ZED-F9P + course = float(gps_segments[8]) if gps_segments[8] else 0.0 # Add Magnetic Variation if firmware supplies it if gps_segments[10]: mv = float(gps_segments[10]) # Float conversions can throw ValueError, caught by caller. From f172579a528109f04509664364bc27deaae994c6 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 8 Jan 2022 10:06:34 +0000 Subject: [PATCH 111/305] Switch and Pushbutton: add comment re logic. --- v3/primitives/pushbutton.py | 1 + v3/primitives/switch.py | 1 + 2 files changed, 2 insertions(+) diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index 1e2a616..cac5982 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -104,4 +104,5 @@ async def buttoncheck(self): self._ld.stop() # Avoid interpreting a second click as a long push self._dblran = False # Ignore state changes until switch has settled + # See https://github.com/peterhinch/micropython-async/issues/69 await asyncio.sleep_ms(Pushbutton.debounce_ms) diff --git a/v3/primitives/switch.py b/v3/primitives/switch.py index 87ce8d5..5cd51c5 100644 --- a/v3/primitives/switch.py +++ b/v3/primitives/switch.py @@ -39,4 +39,5 @@ async def switchcheck(self): elif state == 1 and self._open_func: launch(self._open_func, self._open_args) # Ignore further state changes until switch has settled + # See https://github.com/peterhinch/micropython-async/issues/69 await asyncio.sleep_ms(Switch.debounce_ms) From dae2062279706078a91e8fe7dccfb86e8e902da2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 15 Jan 2022 10:37:32 +0000 Subject: [PATCH 112/305] Tutorial: add note re asynchronous comprehensions. --- v3/docs/TUTORIAL.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 2fdb312..8ba537b 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1453,6 +1453,9 @@ The `__aiter__` method was formerly an asynchronous method. CPython 3.6 accepts synchronous or asynchronous methods. CPython 3.8 and MicroPython require synchronous code [ref](https://github.com/micropython/micropython/pull/6272). +Asynchronous comprehensions [PEP530](https://www.python.org/dev/peps/pep-0530/), +supported in CPython 3.6, are not yet supported in MicroPython. + ###### [Contents](./TUTORIAL.md#contents) ## 4.3 Asynchronous context managers From aebcbd5d915762ecb8165b5410b3798b63ed26a0 Mon Sep 17 00:00:00 2001 From: Brian Cooke Date: Sun, 23 Jan 2022 00:09:26 +0100 Subject: [PATCH 113/305] Update description of __aiter__ The `__aiter__` method was formerly an asynchronous method but this is no longer the case. The requirement that `__aiter__` be defined with `async def` can therefore be removed. --- v3/docs/TUTORIAL.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 8ba537b..8dc474f 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1410,8 +1410,7 @@ and could be used as a means of retrieving successive data items as they arrive from a read-only device. An asynchronous iterable calls asynchronous code in its `next` method. The class must conform to the following requirements: - * It has an `__aiter__` method defined with `async def`and returning the - asynchronous iterator. + * It has an `__aiter__` method returning the asynchronous iterator. * It has an ` __anext__` method which is a task - i.e. defined with `async def` and containing at least one `await` statement. To stop iteration it must raise a `StopAsyncIteration` exception. From 83e0d71b7e8a69a3a5ae24006993f0e6b1cd5d9e Mon Sep 17 00:00:00 2001 From: ssmith Date: Mon, 24 Jan 2022 18:48:40 +0000 Subject: [PATCH 114/305] added delay_ms deinit --- v3/primitives/delay_ms.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py index 6fd11fb..5f9066a 100644 --- a/v3/primitives/delay_ms.py +++ b/v3/primitives/delay_ms.py @@ -27,7 +27,7 @@ def __init__(self, func=None, args=(), duration=1000): self._tout = asyncio.Event() # Timeout event self.wait = self._tout.wait # Allow: await wait_ms.wait() self._ttask = self._fake # Timer task - asyncio.create_task(self._run()) + self._mtask = asyncio.create_task(self._run()) #Main task async def _run(self): while True: @@ -69,3 +69,7 @@ def rvalue(self): def callback(self, func=None, args=()): self._func = func self._args = args + + def deinit(self): + self._ttask.cancel() + self._mtask.cancel() From 26d025910b67a3032065af3626c75fc7cb6b5be8 Mon Sep 17 00:00:00 2001 From: ssmith Date: Mon, 24 Jan 2022 18:53:16 +0000 Subject: [PATCH 115/305] calling stop instead of directlying canceling ttask --- v3/primitives/delay_ms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py index 5f9066a..9d358f0 100644 --- a/v3/primitives/delay_ms.py +++ b/v3/primitives/delay_ms.py @@ -71,5 +71,5 @@ def callback(self, func=None, args=()): self._args = args def deinit(self): - self._ttask.cancel() + self.stop() self._mtask.cancel() From e02bd0c862ae4b629b6b6b654a148a7ad563f902 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 25 Jan 2022 10:34:49 +0000 Subject: [PATCH 116/305] TUTORIAL.md Add note on Delay_ms.deinit(). --- v3/docs/TUTORIAL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 8dc474f..573354d 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1134,6 +1134,7 @@ Methods: proceed when the instance has timed out. 7. `callback` args `func=None`, `args=()`. Allows the callable and its args to be assigned, reassigned or disabled at run time. + 8. `deinit` No args. Cancels the coroutine. In this example a `Delay_ms` instance is created with the default duration of 1s. It is repeatedly triggered for 5 secs, preventing the callback from From a289cbff777e1475db7a1fc5a362ebd463e80575 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 27 Jan 2022 18:23:46 +0000 Subject: [PATCH 117/305] Delay_ms: trig after deinit raises exception. --- v3/docs/TUTORIAL.md | 13 ++++++++++++- v3/primitives/delay_ms.py | 3 +++ v3/primitives/tests/delay_test.py | 29 ++++++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 573354d..d473053 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -43,6 +43,7 @@ REPL.      4.1.2 [Portable code](./TUTORIAL.md#412-portable-code) 4.2 [Asynchronous iterators](./TUTORIAL.md#42-asynchronous-iterators) 4.3 [Asynchronous context managers](./TUTORIAL.md#43-asynchronous-context-managers) + 4.4 [Object scope](./TUTORIAL.md#44-object-scope) What happens when an object goes out of scope. 5. [Exceptions timeouts and cancellation](./TUTORIAL.md#5-exceptions-timeouts-and-cancellation) 5.1 [Exceptions](./TUTORIAL.md#51-exceptions)      5.1.1 [Global exception handler](./TUTORIAL.md#511-global-exception-handler) @@ -1134,7 +1135,7 @@ Methods: proceed when the instance has timed out. 7. `callback` args `func=None`, `args=()`. Allows the callable and its args to be assigned, reassigned or disabled at run time. - 8. `deinit` No args. Cancels the coroutine. + 8. `deinit` No args. Cancels the running task. See [Object scope](./TUTORIAL.md#44-object-scope). In this example a `Delay_ms` instance is created with the default duration of 1s. It is repeatedly triggered for 5 secs, preventing the callback from @@ -1522,6 +1523,16 @@ asyncio.run(bar()) ###### [Contents](./TUTORIAL.md#contents) +## 4.4 Object scope + +If an object launches a task and that object goes out of scope, the task will +continue to be scheduled. The task will run to completion or until cancelled. +If this is undesirable consider writing a `deinit` method to cancel associated +running tasks. Applications can call `deinit`, for example in a `try...finally` +block or in a context manager. + +###### [Contents](./TUTORIAL.md#contents) + # 5 Exceptions timeouts and cancellation These topics are related: `uasyncio` enables the cancellation of tasks, and the diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py index 9d358f0..4cc53a7 100644 --- a/v3/primitives/delay_ms.py +++ b/v3/primitives/delay_ms.py @@ -48,6 +48,8 @@ async def _timer(self, dt): # API # trigger may be called from hard ISR. def trigger(self, duration=0): # Update absolute end time, 0-> ctor default + if self._mtask is None: + raise RuntimeError("Delay_ms.deinit() has run.") self._tend = ticks_add(ticks_ms(), duration if duration > 0 else self._durn) self._retn = None # Default in case cancelled. self._busy = True @@ -73,3 +75,4 @@ def callback(self, func=None, args=()): def deinit(self): self.stop() self._mtask.cancel() + self._mtask = None diff --git a/v3/primitives/tests/delay_test.py b/v3/primitives/tests/delay_test.py index b007f01..17dde72 100644 --- a/v3/primitives/tests/delay_test.py +++ b/v3/primitives/tests/delay_test.py @@ -91,6 +91,7 @@ async def reduce_test(): # Test reducing a running delay Callback should run cb callback Callback should run +cb callback Done ''' printexp(s, 11) @@ -174,6 +175,31 @@ def timer_cb(_): await asyncio.sleep(1) print('Done') +async def err_test(): # Test triggering de-initialised timer + s = ''' +Running (runtime = 3s): +Trigger 1 sec delay +cb callback +Success: error was raised. +Done + ''' + printexp(s, 3) + def cb(v): + print('cb', v) + return 42 + + d = Delay_ms(cb, ('callback',)) + + print('Trigger 1 sec delay') + d.trigger(1000) + await asyncio.sleep(2) + d.deinit() + try: + d.trigger(1000) + except RuntimeError: + print("Success: error was raised.") + print('Done') + av = ''' Run a test by issuing delay_test.test(n) @@ -184,11 +210,12 @@ def timer_cb(_): 2 Test reducing the duration of a running timer 3 Test delay defined by constructor arg 4 Test triggering a Task +5 Attempt to trigger de-initialised instance \x1b[39m ''' print(av) -tests = (isr_test, stop_test, reduce_test, ctor_test, launch_test) +tests = (isr_test, stop_test, reduce_test, ctor_test, launch_test, err_test) def test(n=0): try: asyncio.run(tests[n]()) From b94be59bc2341390d4156e952b865d32d9ea2132 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 2 Feb 2022 13:38:25 +0000 Subject: [PATCH 118/305] DRIVERS.md: Add code sample for Pushbutton suppress arg. --- v3/docs/DRIVERS.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 9df62d4..711246f 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -244,6 +244,27 @@ the case of a single short press, `release_func` will be delayed until the expiry of the double-click timer (because until that time a second click might occur). +The following script may be used to demonstrate the effect of this arg. As +written it assumes a Pi Pico with a pushbutton between GPIO 18 and Gnd, with +the primitives installed. +```python +from machine import Pin +import uasyncio as asyncio +from primitives.pushbutton import Pushbutton + +btn = Pin(18, Pin.IN, Pin.PULL_UP) # Adapt for your hardware +pb = Pushbutton(btn, suppress=True) + +async def main(): + short_press = pb.release_func(print, ("SHORT",)) + double_press = pb.double_func(print, ("DOUBLE",)) + long_press = pb.long_func(print, ("LONG",)) + while True: + await asyncio.sleep(1) + +asyncio.run(main()) +``` + ### 4.1.2 The sense constructor argument In most applications it can be assumed that, at power-up, pushbuttons are not From 859360bac1038fc4b2e24e173e5b8626e2e7a57a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 3 Feb 2022 13:22:25 +0000 Subject: [PATCH 119/305] DRIVERS.md: Changes offered by @bai-yi-bai. --- v3/docs/DRIVERS.md | 97 ++++++++++++++++++++++++++++++---------------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 711246f..1f19200 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -20,7 +20,7 @@ goes outside defined bounds. 2. [Installation and usage](./DRIVERS.md#2-installation-and-usage) 3. [Interfacing switches](./DRIVERS.md#3-interfacing-switches) Switch debouncer with callbacks. 3.1 [Switch class](./DRIVERS.md#31-switch-class) - 4. [Interfacing pushbuttons](./DRIVERS.md#4-interfacing-pushbuttons) Extends Switch for long and double click events + 4. [Interfacing pushbuttons](./DRIVERS.md#4-interfacing-pushbuttons) Extends Switch for long and double-click events 4.1 [Pushbutton class](./DRIVERS.md#41-pushbutton-class)      4.1.1 [The suppress constructor argument](./DRIVERS.md#411-the-suppress-constructor-argument)      4.1.2 [The sense constructor argument](./DRIVERS.md#412-the-sense-constructor-argument) @@ -131,10 +131,18 @@ asyncio.run(my_app()) # Run main application code # 4. Interfacing pushbuttons -The `primitives.pushbutton` module provides the `Pushbutton` class. This is a -generalisation of `Switch` to support normally open or normally closed switches -connected to ground or 3V3. Can run a `callable` on on press, release, -double-click or long press events. +The `primitives.pushbutton` module provides the `Pushbutton` class for use with +simple mechanical, spring-loaded push buttons. This class is a generalisation +of the `Switch` class. `Pushbutton` supports open or normally closed buttons +connected to ground or 3V3. To a human, pushing a button is seen as a single +event, but the micro-controller sees voltage changes corresponding to two +events: press and release. A long button press adds the component of time and a +double-click appears as four voltage changes. The asynchronous `Pushbutton` +class provides the logic required to handle these user interactions by +monitoring these events over time. + +Instances of this class can run a `callable` on on press, release, double-click +or long press events. ## 4.1 Pushbutton class @@ -183,8 +191,9 @@ any existing callback will be disabled. Class attributes: 1. `debounce_ms` Debounce time in ms. Default 50. 2. `long_press_ms` Threshold time in ms for a long press. Default 1000. - 3. `double_click_ms` Threshold time in ms for a double click. Default 400. + 3. `double_click_ms` Threshold time in ms for a double-click. Default 400. +A simple Pyboard demo: ```python from pyb import LED from machine import Pin @@ -211,42 +220,62 @@ number of coroutines. ### 4.1.1 The suppress constructor argument -When the button is pressed `press_func` runs immediately. This minimal latency -is ideal for applications such as games. Consider a long press: `press_func` -runs initially, then `long_func`, and finally `release_func`. In the case of a -double-click `press_func` and `release_func` will run twice; `double_func` runs -once. - -There can be a need for a `callable` which runs if a button is pressed but -only if a doubleclick or long press function does not run. The `suppress` arg -changes the behaviour of `release_func` to fill that role. This has timing -implications. - -The soonest that the absence of a long press can be detected is on button -release. Absence of a double click can only be detected when the double click -timer times out without a second press occurring. - -Note `suppress` affects the behaviour of `release_func` only. Other callbacks -including `press_func` behave normally. - -If the `suppress` constructor arg is set, `release_func` will be launched as -follows: - 1. If `double_func` does not exist on rapid button release. - 2. If `double_func` exists, after the expiration of the doubleclick timer. - 3. If `long_func` exists and the press duration causes `long_func` to be +The purpose of the `suppress` argument is to disambiguate the response when an +application requires either, or both, long-press and double-click events. It +works by modifying the behavior of the `release_func`. By design, whenever a +button is pressed, the `press_func` runs immediately. This minimal latency is +ideal for applications such as games. The `Pushbutton` class provides the +ability to suppress 'intermediate' events and reduce them down to one single +event. The `suppress` argument is useful for applications where long-press, +single-press, and double-click events are desired, such as clocks, watches, or +menu navigation. However, long-press and double-click detection introduces +additional latency to ensure correct classification of events and is therefore +not suitable for all applications. To illustrate the default library behavior, +consider how long button presses and double-clicks are interpreted. + +A long press is seen as three events: + + * `press_func` + * `long_func` + * `release_func` + +Similarly, a double-click is seen as five events: + + * `press_func` + * `release_func` + * `press_func` + * `release_func` + * `double_func` + +There can be a need for a callable which runs if a button is pressed, but only +if a double-click or long-press function does not run. The suppress argument +changes the behaviour of the `release_func` to fill that role. This has timing +implications. The soonest that the absence of a long press can be detected is +on button release. Absence of a double-click can only be detected when the +double-click timer times out without a second press occurring. + +Note: `suppress` affects the behaviour of the `release_func` only. Other +callbacks including `press_func` behave normally. + +If the `suppress = True` constructor argument is set, the `release_func` will +be launched as follows: + + * If `double_func` does not exist on rapid button release. + * If `double_func` exists, after the expiration of the double-click timer. + * If `long_func` exists and the press duration causes `long_func` to be launched, `release_func` will not be launched. - 4. If `double_func` exists and a double click occurs, `release_func` will not + * If `double_func` exists and a double-click occurs, `release_func` will not be launched. In the typical case where `long_func` and `double_func` are both defined, this ensures that only one of `long_func`, `double_func` and `release_func` run. In -the case of a single short press, `release_func` will be delayed until the +the case of a single short press, the `release_func` will be delayed until the expiry of the double-click timer (because until that time a second click might occur). -The following script may be used to demonstrate the effect of this arg. As -written it assumes a Pi Pico with a pushbutton between GPIO 18 and Gnd, with -the primitives installed. +The following script may be used to demonstrate the effect of this argument. As +written, it assumes a Pi Pico with a push button attached between GPIO 18 and +Gnd, with the primitives installed. ```python from machine import Pin import uasyncio as asyncio From e9e734e395baf7cfc7db1f8c9ada5dd18c683882 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 4 Feb 2022 11:28:19 +0000 Subject: [PATCH 120/305] Add INTERRUPTS.md FAQ. --- v3/docs/INTERRUPTS.md | 191 ++++++++++++++++++++++++++++++++++++++++++ v3/docs/TUTORIAL.md | 5 +- 2 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 v3/docs/INTERRUPTS.md diff --git a/v3/docs/INTERRUPTS.md b/v3/docs/INTERRUPTS.md new file mode 100644 index 0000000..9925945 --- /dev/null +++ b/v3/docs/INTERRUPTS.md @@ -0,0 +1,191 @@ +# Interfacing uasyncio to interrupts + +This note aims to provide guidance in resolving common queries about the use of +interrupts in `uasyncio` applications. + +# 1. Does the requirement warrant an interrupt? + +Writing an interrupt service routine (ISR) requires care: see the +[official docs](https://docs.micropython.org/en/latest/reference/isr_rules.html). +There are restrictions (detailed below) on the way an ISR can interface with +`uasyncio`. Finally, on many platforms interrupts are a limited resource. In +short interrupts are extremely useful but, if a practical alternative exists, +it should be seriously considered. + +Requirements that warrant an interrupt along with a `uasyncio` interface are +ones that require a microsecond-level response, followed by later processing. +Examples are: + * Where the event requires an accurate timestamp. + * Where a device supplies data and needs to be rapidly serviced. Data is put + in a pre-allocated buffer for later processing. + +Examples needing great care: + * Where arrival of data triggers an interrupt and subsequent interrupts may + occur after a short period of time. + * Where arrival of an interrupt triggers complex application behaviour: see + notes on [context](./INTERRUPTS.md#32-context) + +# 2. Alternatives to interrupts + +## 2.1 Polling + +An alternative to interrupts is to use polling. For values that change slowly +such as ambient temperature or pressure this simplification is achieved with no +discernible impact on performance. +```python +temp = 0 +async def read_temp(): + global temp + while True: + temp = thermometer.read() + await asyncio.sleep(60) +``` +In cases where interrupts arrive slowly it is worth considering whether there +is any gain in using an interrupt rather than polling the hardware: + +```python +async def read_data(): + while True: + while not device.ready(): + await uasyncio.sleep_ms(0) + data = device.read() + # process the data +``` +The overhead of polling is typically low. The MicroPython VM might use +300μs to determine that the device is not ready. This will occur once per +iteration of the scheduler, during which time every other pending task gets a +slice of execution. If there were five tasks, each of which used 5ms of VM time, +the overhead would be `0.3*100/(5*5)=1.2%` - see [latency](./INTERRUPTS.md#31-latency-in-uasyncio). + +Devices such as pushbuttons and switches are best polled as, in most +applications, latency of (say) 100ms is barely detectable. Interrupts lead to +difficulties with +[contact bounce](http://www.ganssle.com/debouncing.htm) which is readily +handled using a simple [uasyncio driver](./DRIVERS.md). There may be exceptions +which warrant an interrupt such as fast games or cases where switches are +machine-operated such as limit switches. + +## 2.2 The I/O mechanism + +Devices such as UARTs and sockets are supported by the `uasyncio` stream +mechanism. The UART driver uses interrupts at a firmware level, but exposes +its interface to `uasyncio` by the `StreamReader` and `StreamWriter` classes. +These greatly simplify the use of such devices. + +It is also possible to write device drivers in Python enabling the use of the +stream mechanism. This is covered in +[the tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#64-writing-streaming-device-drivers). + +# 3. Using interrupts + +This section details some of the issues to consider where interrupts are to be +used with `uasyncio`. + +## 3.1 Latency in uasyncio + +Consider an application with four continuously running tasks, plus a fifth +which is paused waiting on an interrupt. Each of the four tasks will yield to +the scheduler at intervals. Each task will have a worst-case period +of blocking between yields. Assume that the worst-case times for each task are +50, 30, 25 and 10ms. If the program logic allows it, the situation may arise +where all of these tasks are queued for execution, and all are poised to block +for the maximum period. Assume that at that moment the fifth task is triggered. + +With current `uasyncio` design that fifth task will be queued for execution +after the four pending tasks. It will therefore run after +(50+30+25+10) = 115ms +An enhancement to `uasyncio` has been discussed that would reduce that to 50ms, +but that is the irreduceable minimum for any cooperative scheduler. + +The key issue with latency is the case where a second interrupt occurs while +the first is still waiting for its `uasyncio` handler to be scheduled. If this +is a possibility, mechanisms such as buffering or queueing must be considered. + +## 3.2 Context + +Consider an incremental encoder providing input to a GUI. Owing to the need to +track phase information an interrupt must be used for the encoder's two +signals. An ISR determines the current position of the encoder, and if it has +changed, calls a method in the GUI code. + +The consequences of this can be complex. A widget's visual appearance may +change. User callbacks may be triggered, running arbitrary Python code. +Crucially all of this occurs in an ISR context. This is unacceptable for all +the reasons identified in +[this doc](https://docs.micropython.org/en/latest/reference/isr_rules.html). + +Note that using `micropython.schedule` does not address every issue associated +with ISR context. In particular restictions remain on the use of `uasyncio` +operations. This is because such code can pre-empt the `uasyncio` scheduler. +This is discussed further below. + +A solution to the encoder problem is to have the ISR maintain a value of the +encoder's position, with a `uasyncio` task polling this and triggering the GUI +callback. This ensures that the callback runs in a `uasyncio` context and can +run any Python code, including `uasyncio` operations such as creating and +cancelling tasks. This will work if the position value is stored in a single +word, because changes to a word are atomic (non-interruptible). A more general +solution is to use `uasyncio.ThreadSafeFlag`. + +## 3.3 Interfacing an ISR with uasyncio + +This should be read in conjunction with the discussion of the `ThreadSafeFlag` +in [the tutorial](./TUTORIAL.md#36-threadsafeflag). + +Assume a hardware device capable of raising an interrupt when data is +available. The requirement is to read the device fast and subsequently process +the data using a `uasyncio` task. An obvious (but wrong) approach is: + +```python +data = bytearray(4) +# isr runs in response to an interrupt from device +def isr(): + device.read_into(data) # Perform a non-allocating read + uasyncio.create_task(process_data()) # BUG +``` + +This is incorrect because when an ISR runs, it can pre-empt the `uasyncio` +scheduler with the result that `uasyncio.create_task()` may disrupt the +scheduler. This applies whether the interrupt is hard or soft and also applies +if the ISR has passed execution to another function via `micropython.schedule`: +as described above, all such code runs in an ISR context. + +The safe way to interface between ISR-context code and `uasyncio` is to have a +coroutine with synchronisation performed by `uasyncio.ThreadSafeFlag`. The +following fragment illustrates the creation of a task in response to an +interrupt: +```python +tsf = uasyncio.ThreadSafeFlag() +data = bytearray(4) + +def isr(_): # Interrupt handler + device.read_into(data) # Perform a non-allocating read + tsf.set() # Trigger task creation + +async def check_for_interrupts(): + while True: + await tsf.wait() + uasyncio.create_task(process_data()) +``` +It is worth considering whether there is any point in creating a task rather +than using this template: +```python +tsf = uasyncio.ThreadSafeFlag() +data = bytearray(4) + +def isr(_): # Interrupt handler + device.read_into(data) # Perform a non-allocating read + tsf.set() # Trigger task creation + +async def process_data(): + while True: + await tsf.wait() + # Process the data here before waiting for the next interrupt +``` + +# 4. Conclusion + +The key take-away is that `ThreadSafeFlag` is the only `uasyncio` construct +which can safely be used in an ISR context. + +###### [Main tutorial](./TUTORIAL.md#contents) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index d473053..e4d0372 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -31,7 +31,7 @@ REPL. 3.4 [Semaphore](./TUTORIAL.md#34-semaphore)      3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore) 3.5 [Queue](./TUTORIAL.md#35-queue) - 3.6 [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) Synchronisation with asynchronous events. + 3.6 [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) Synchronisation with asynchronous events and interrupts. 3.7 [Barrier](./TUTORIAL.md#37-barrier) 3.8 [Delay_ms](./TUTORIAL.md#38-delay_ms-class) Software retriggerable delay. 3.9 [Message](./TUTORIAL.md#39-message) @@ -893,7 +893,8 @@ asyncio.run(queue_go(4)) ## 3.6 ThreadSafeFlag -This requires firmware V1.15 or later. +This requires firmware V1.15 or later. +See also [Interfacing uasyncio to interrupts](./INTERRUPTS.md). This official class provides an efficient means of synchronising a task with a truly asynchronous event such as a hardware interrupt service routine or code From f2ce1e8e953b329e1d3bb126b1f7693ff68d5a06 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 4 Feb 2022 12:08:25 +0000 Subject: [PATCH 121/305] Add INTERRUPTS.md FAQ. --- v3/docs/INTERRUPTS.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/v3/docs/INTERRUPTS.md b/v3/docs/INTERRUPTS.md index 9925945..05d7acd 100644 --- a/v3/docs/INTERRUPTS.md +++ b/v3/docs/INTERRUPTS.md @@ -23,7 +23,7 @@ Examples needing great care: * Where arrival of data triggers an interrupt and subsequent interrupts may occur after a short period of time. * Where arrival of an interrupt triggers complex application behaviour: see - notes on [context](./INTERRUPTS.md#32-context) + notes on [context](./INTERRUPTS.md#32-context). # 2. Alternatives to interrupts @@ -40,8 +40,9 @@ async def read_temp(): temp = thermometer.read() await asyncio.sleep(60) ``` -In cases where interrupts arrive slowly it is worth considering whether there -is any gain in using an interrupt rather than polling the hardware: +In cases where interrupts arrive at a low frequency it is worth considering +whether there is any gain in using an interrupt rather than polling the +hardware: ```python async def read_data(): @@ -69,8 +70,8 @@ machine-operated such as limit switches. Devices such as UARTs and sockets are supported by the `uasyncio` stream mechanism. The UART driver uses interrupts at a firmware level, but exposes -its interface to `uasyncio` by the `StreamReader` and `StreamWriter` classes. -These greatly simplify the use of such devices. +its interface to `uasyncio` by means of the `StreamReader` and `StreamWriter` +classes. These greatly simplify the use of such devices. It is also possible to write device drivers in Python enabling the use of the stream mechanism. This is covered in @@ -115,7 +116,7 @@ the reasons identified in [this doc](https://docs.micropython.org/en/latest/reference/isr_rules.html). Note that using `micropython.schedule` does not address every issue associated -with ISR context. In particular restictions remain on the use of `uasyncio` +with ISR context because restictions remain on the use of `uasyncio` operations. This is because such code can pre-empt the `uasyncio` scheduler. This is discussed further below. @@ -130,7 +131,8 @@ solution is to use `uasyncio.ThreadSafeFlag`. ## 3.3 Interfacing an ISR with uasyncio This should be read in conjunction with the discussion of the `ThreadSafeFlag` -in [the tutorial](./TUTORIAL.md#36-threadsafeflag). +in [the official docs](https://docs.micropython.org/en/latest/library/uasyncio.html#class-threadsafeflag) +and [the tutorial](./TUTORIAL.md#36-threadsafeflag). Assume a hardware device capable of raising an interrupt when data is available. The requirement is to read the device fast and subsequently process From d97410b9615ad287b3f32c2b15608a981886afb7 Mon Sep 17 00:00:00 2001 From: Brian Cooke Date: Sat, 5 Feb 2022 16:26:43 +0100 Subject: [PATCH 122/305] iorw-py: fix typo in comment The comment being modified by this PR currently states that timer cb `do_input` clears `ready_rd` but it actually sets it. --- v3/as_demos/iorw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/as_demos/iorw.py b/v3/as_demos/iorw.py index be53965..a5f9fa5 100644 --- a/v3/as_demos/iorw.py +++ b/v3/as_demos/iorw.py @@ -62,7 +62,7 @@ def ioctl(self, req, arg): # see ports/stm32/uart.c # Test of device that produces one character at a time def readline(self): - self.ready_rd = False # Cleared by timer cb do_input + self.ready_rd = False # Set by timer cb do_input ch = self.rbuf[self.ridx] if ch == ord('\n'): self.ridx = 0 From 0926d503adb2243de061d6fe3bf3f99023d91b24 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 6 Feb 2022 09:08:28 +0000 Subject: [PATCH 123/305] switch.py: Add deinit method. --- v3/docs/DRIVERS.md | 2 ++ v3/primitives/switch.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 1f19200..a86f724 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -100,6 +100,7 @@ Methods: `args` a tuple of arguments for the `callable` (default `()`) 3. `__call__` Call syntax e.g. `myswitch()` returns the physical debounced state of the switch i.e. 0 if grounded, 1 if connected to `3V3`. + 4. `deinit` No args. Cancels the running task. Methods 1 and 2 should be called before starting the scheduler. @@ -184,6 +185,7 @@ Methods: state of the button (`True` corresponds to pressed). 6. `rawstate()` Returns the logical instantaneous state of the button. There is probably no reason to use this. + 7. `deinit` No args. Cancels the running task. Methods 1 - 4 may be called at any time. If `False` is passed for a callable, any existing callback will be disabled. diff --git a/v3/primitives/switch.py b/v3/primitives/switch.py index 5cd51c5..8b49cee 100644 --- a/v3/primitives/switch.py +++ b/v3/primitives/switch.py @@ -14,7 +14,7 @@ def __init__(self, pin): self._open_func = False self._close_func = False self.switchstate = self.pin.value() # Get initial state - asyncio.create_task(self.switchcheck()) # Thread runs forever + self._run = asyncio.create_task(self.switchcheck()) # Thread runs forever def open_func(self, func, args=()): self._open_func = func @@ -39,5 +39,7 @@ async def switchcheck(self): elif state == 1 and self._open_func: launch(self._open_func, self._open_args) # Ignore further state changes until switch has settled - # See https://github.com/peterhinch/micropython-async/issues/69 await asyncio.sleep_ms(Switch.debounce_ms) + + def deinit(self): + self._run.cancel() From b94b15117cbd7001eb70dd5b5c1e44322888b1ed Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 8 Feb 2022 10:03:32 +0000 Subject: [PATCH 124/305] rate.py: update ESP32 benchmark result. --- v3/as_demos/rate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/v3/as_demos/rate.py b/v3/as_demos/rate.py index fecd759..ea27ba8 100644 --- a/v3/as_demos/rate.py +++ b/v3/as_demos/rate.py @@ -10,9 +10,13 @@ # Results for 100 coros on other platforms at standard clock rate: # Pyboard D SF2W 124μs # Pico 481μs -# ESP32 920μs +# ESP32 322μs # ESP8266 1495μs (could not run 500 or 1000 coros) +# Note that ESP32 benchmarks are notoriously fickle. Above figure was for +# the reference board running MP V1.18. Results may vary with firmware +# depending on the layout of code in RAM/IRAM + import uasyncio as asyncio num_coros = (100, 200, 500, 1000) From e637d8d24a0ac45183f702c8d1697dc260f8b295 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 8 Feb 2022 14:20:20 +0000 Subject: [PATCH 125/305] Primitives: lazy loader and deinit methods. --- v3/docs/DRIVERS.md | 14 ++++++-------- v3/primitives/__init__.py | 26 ++++++++++++++++++++++++++ v3/primitives/pushbutton.py | 7 +++++-- v3/primitives/switch.py | 2 +- v3/primitives/tests/adctest.py | 2 +- v3/primitives/tests/switches.py | 24 +++++++++++++----------- 6 files changed, 52 insertions(+), 23 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index a86f724..8c070d4 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -43,9 +43,7 @@ directory and its contents to the target hardware. Drivers are imported with: ```python -from primitives.switch import Switch -from primitives.pushbutton import Pushbutton -from primitives.aadc import AADC +from primitives import Switch, Pushbutton, AADC ``` There is a test/demo program for the Switch and Pushbutton classes. On import this lists available tests. It assumes a Pyboard with a switch or pushbutton @@ -111,7 +109,7 @@ Class attribute: from pyb import LED from machine import Pin import uasyncio as asyncio -from primitives.switch import Switch +from primitives import Switch async def pulse(led, ms): led.on() @@ -200,7 +198,7 @@ A simple Pyboard demo: from pyb import LED from machine import Pin import uasyncio as asyncio -from primitives.pushbutton import Pushbutton +from primitives import Pushbutton def toggle(led): led.toggle() @@ -281,7 +279,7 @@ Gnd, with the primitives installed. ```python from machine import Pin import uasyncio as asyncio -from primitives.pushbutton import Pushbutton +from primitives import Pushbutton btn = Pin(18, Pin.IN, Pin.PULL_UP) # Adapt for your hardware pb = Pushbutton(btn, suppress=True) @@ -327,7 +325,7 @@ or log data, if the value goes out of range. Typical usage: import uasyncio as asyncio from machine import ADC import pyb -from primitives.aadc import AADC +from primitives import AADC aadc = AADC(ADC(pyb.Pin.board.X1)) async def foo(): @@ -367,7 +365,7 @@ until it goes out of range. ```python import uasyncio as asyncio from machine import ADC -from primitives.aadc import AADC +from primitives import AADC aadc = AADC(ADC('X1')) async def foo(): diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index 0274fc2..bed9f9b 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -29,3 +29,29 @@ def _handle_exception(loop, context): sys.exit() loop = asyncio.get_event_loop() loop.set_exception_handler(_handle_exception) + +_attrs = { + "AADC": "aadc", + "Barrier": "barrier", + "Condition": "condition", + "Delay_ms": "delay_ms", + "Encode": "encoder_async", + "Message": "message", + "Pushbutton": "pushbutton", + "Queue": "queue", + "Semaphore": "semaphore", + "BoundedSemaphore": "semaphore", + "Switch": "switch", +} + +# Copied from uasyncio.__init__.py +# Lazy loader, effectively does: +# global attr +# from .mod import attr +def __getattr__(attr): + mod = _attrs.get(attr, None) + if mod is None: + raise AttributeError(attr) + value = getattr(__import__(mod, None, None, True, 1), attr) + globals()[attr] = value + return value diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index cac5982..225557e 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -1,6 +1,6 @@ # pushbutton.py -# Copyright (c) 2018-2021 Peter Hinch +# Copyright (c) 2018-2022 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file import uasyncio as asyncio @@ -27,7 +27,7 @@ def __init__(self, pin, suppress=False, sense=None): self._dd = False # Ditto for doubleclick self.sense = pin.value() if sense is None else sense # Convert from electrical to logical value self.state = self.rawstate() # Initial state - asyncio.create_task(self.buttoncheck()) # Thread runs forever + self._run = asyncio.create_task(self.buttoncheck()) # Thread runs forever def press_func(self, func=False, args=()): self._tf = func @@ -106,3 +106,6 @@ async def buttoncheck(self): # Ignore state changes until switch has settled # See https://github.com/peterhinch/micropython-async/issues/69 await asyncio.sleep_ms(Pushbutton.debounce_ms) + + def deinit(self): + self._run.cancel() diff --git a/v3/primitives/switch.py b/v3/primitives/switch.py index 8b49cee..cb1b51c 100644 --- a/v3/primitives/switch.py +++ b/v3/primitives/switch.py @@ -1,6 +1,6 @@ # switch.py -# Copyright (c) 2018-2020 Peter Hinch +# Copyright (c) 2018-2022 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file import uasyncio as asyncio diff --git a/v3/primitives/tests/adctest.py b/v3/primitives/tests/adctest.py index 2d17e54..8e3a194 100644 --- a/v3/primitives/tests/adctest.py +++ b/v3/primitives/tests/adctest.py @@ -6,7 +6,7 @@ import uasyncio as asyncio from machine import ADC import pyb -from primitives.aadc import AADC +from primitives import AADC async def signal(): # Could use write_timed but this prints values dac = pyb.DAC(1, bits=12, buffering=True) diff --git a/v3/primitives/tests/switches.py b/v3/primitives/tests/switches.py index c55711a..82e77c9 100644 --- a/v3/primitives/tests/switches.py +++ b/v3/primitives/tests/switches.py @@ -2,8 +2,9 @@ # Tested on Pyboard but should run on other microcontroller platforms # running MicroPython with uasyncio library. -# Copyright (c) 2018-2020 Peter Hinch +# Copyright (c) 2018-2022 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file +# Now executes .deinit() # To run: # from primitives.tests.switches import * @@ -11,8 +12,7 @@ from machine import Pin from pyb import LED -from primitives.switch import Switch -from primitives.pushbutton import Pushbutton +from primitives import Switch, Pushbutton import uasyncio as asyncio helptext = ''' @@ -41,14 +41,16 @@ def toggle(led): led.toggle() # Quit test by connecting X2 to ground -async def killer(): +async def killer(obj): pin = Pin('X2', Pin.IN, Pin.PULL_UP) while pin.value(): await asyncio.sleep_ms(50) + obj.deinit() + await asyncio.sleep_ms(0) -def run(): +def run(obj): try: - asyncio.run(killer()) + asyncio.run(killer(obj)) except KeyboardInterrupt: print('Interrupted') finally: @@ -72,7 +74,7 @@ def test_sw(): # Register coros to launch on contact close and open sw.close_func(pulse, (green, 1000)) sw.open_func(pulse, (red, 1000)) - run() + run(sw) # Test for the switch class with a callback def test_swcb(): @@ -90,7 +92,7 @@ def test_swcb(): # Register a coro to launch on contact close sw.close_func(toggle, (red,)) sw.open_func(toggle, (green,)) - run() + run(sw) # Test for the Pushbutton class (coroutines) # Pass True to test suppress @@ -118,7 +120,7 @@ def test_btn(suppress=False, lf=True, df=True): if lf: print('Long press enabled') pb.long_func(pulse, (blue, 1000)) - run() + run(pb) # Test for the Pushbutton class (callbacks) def test_btncb(): @@ -141,7 +143,7 @@ def test_btncb(): pb.release_func(toggle, (green,)) pb.double_func(toggle, (yellow,)) pb.long_func(toggle, (blue,)) - run() + run(pb) # Test for the Pushbutton class where callback coros change dynamically def setup(pb, press, release, dbl, lng, t=1000): @@ -178,4 +180,4 @@ def btn_dynamic(): pb = Pushbutton(pin) setup(pb, red, green, yellow, None) pb.long_func(setup, (pb, blue, red, green, yellow, 2000)) - run() + run(pb) From 23bd05b52b613c168ec37a93017c763722d1f01d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 10 Feb 2022 16:48:36 +0000 Subject: [PATCH 126/305] DRIVERS.md notes on cancellation and result retrieval. TUTORIAL improve 5.2. --- v3/docs/DRIVERS.md | 121 ++++++++++++++++++++++++++++++++++++++++++++ v3/docs/TUTORIAL.md | 35 ++++++++----- 2 files changed, 142 insertions(+), 14 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 8c070d4..a07e17f 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -32,6 +32,11 @@ goes outside defined bounds. 7. [Additional functions](./DRIVERS.md#7-additional-functions) 7.1 [launch](./DRIVERS.md#71-launch) Run a coro or callback interchangeably 7.2 [set_global_exception](./DRIVERS.md#72-set_global_exception) Simplify debugging with a global exception handler + 8. [Advanced use of callbacks](./DRIVERS.md#8-advanced-use-of-callbacks) + 8.1 [Retrieve result from synchronous function](./DRIVERS.md#81-retrieve-result-from-synchronous-function) + 8.2 [Cancel a task](./DRIVERS.md#82-cancel-a-task) + 8.3 [Retrieve result from a task](./DRIVERS.md#83-retrieve-result-from-a-task) + 8.4 [A complete example](./DRIVERS.md#84-a-complete-example) ###### [Tutorial](./TUTORIAL.md#contents) @@ -125,6 +130,8 @@ sw = Switch(pin) sw.close_func(pulse, (red, 1000)) # Note how coro and args are passed asyncio.run(my_app()) # Run main application code ``` +See [Advanced use of callbacks](./DRIVERS.md#8-advanced-use-of-callbacks) for +ways to retrieve a result from a callback and to cancel a task. ###### [Contents](./DRIVERS.md#1-contents) @@ -311,6 +318,9 @@ When the pin value changes, the new value is compared with `sense` to determine if the button is closed or open. This is to allow the designer to specify if the `closed` state of the button is active `high` or active `low`. +See [Advanced use of callbacks](./DRIVERS.md#8-advanced-use-of-callbacks) for +ways to retrieve a result from a callback and to cancel a task. + ###### [Contents](./DRIVERS.md#1-contents) # 5. ADC monitoring @@ -520,3 +530,114 @@ events can be hard to deduce. A global handler ensures that the entire application stops allowing the traceback and other debug prints to be studied. ###### [Contents](./DRIVERS.md#1-contents) + +# 8. Advanced use of callbacks + +The `Switch` and `Pushbutton` classes respond to state changes by launching +callbacks. These which can be functions, methods or coroutines. The classes +provide no means of retrieving the result of a synchronous function, nor of +cancelling a coro. Further, after a coro is launched there is no means of +awaiting it and accessing its return value. This is by design, firstly to keep +the classes as minimal as possible and secondly because these issues are easily +overcome. + +## 8.1 Retrieve result from synchronous function + +The following is a way to run a synchronous function returning a value. In this +case `bar` is a synchronous function taking a numeric arg which is a button +reference: +```python +pb = Pushbutton(Pin(1, Pin.IN, Pin.PULL_UP)) +pb.press_func(run, (bar, 1)) + +def run(func, button_no): + res = func(button_no) + # Do something that needs the result + +def bar(n): # This is the function we want to run + return 42*n +``` + +## 8.2 Cancel a task + +Assume a coroutine `foo` with a single arg. The coro is started by a button +press and may be cancelled by another task. We need to retrieve a reference to +the `foo` task and store it such that it is available to the cancelling code: +```python +pb = Pushbutton(Pin(1, Pin.IN, Pin.PULL_UP)) +pb.press_func(start, (foo, 1)) +tasks = {1: None} # Support for multiple buttons +def start(func, button_no): + tasks[button_no] = asyncio.create_task(func(button_no)) +``` +The cancelling code checks that the appropriate entry in `tasks` is not `None` +and cancels it. + +## 8.3 Retrieve result from a task + +In this case we need to await the `foo` task so `start` is a coroutine: +```python +pb = Pushbutton(Pin(1, Pin.IN, Pin.PULL_UP)) +pb.press_func(start, (foo, 1)) +async def start(func, button_no): + result = await func(button_no) + # Process result +``` + +## 8.4 A complete example + +In fragments 8.2 and 8.3, if the button is pressed again before `foo` has run +to completion, a second `foo` instance will be launched. This may be +undesirable. + +The following script is a complete example which can be run on a Pyboard (or +other target with changes to pin numbers). It illustrates + 1. Logic to ensure that only one `foo` task instance runs at a time. + 2. The `start` task retrieves the result from `foo`. + 3. The `foo` task may be cancelled by a button press. + 4. The `foo` task returns a meaningful value whether cancelled or run to + completion. + 5. Use of an `Event` to stop the script. + +```python +import uasyncio as asyncio +from primitives import Pushbutton +from machine import Pin +tasks = {1: None} # Allow extension to multiple buttons +complete = asyncio.Event() # Stop the demo on cancellation + +async def start(asfunc, button_no): + if tasks[button_no] is None: # Only one instance + tasks[button_no] = asyncio.create_task(asfunc(button_no)) + result = await tasks[button_no] + print("Result", result) + complete.set() + +async def foo(button_no): + n = 0 + try: + while n < 20: + print(f"Button {button_no} count {n}") + n += 1 + await asyncio.sleep(1) + except asyncio.CancelledError: + pass # Trap cancellation so that n is returned + return n + +def killer(button_no): + if tasks[button_no] is not None: + tasks[button_no].cancel() + tasks[button_no] = None # Allow to run again + +async def main(): + pb1 = Pushbutton(Pin("X1", Pin.IN, Pin.PULL_UP)) + pb2 = Pushbutton(Pin("X2", Pin.IN, Pin.PULL_UP)) + pb1.press_func(start, (foo, 1)) + pb2.press_func(killer, (1,)) + await complete.wait() + +try: + asyncio.run(main()) +finally: + _ = asyncio.new_event_loop() +``` diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index e4d0372..19b8c64 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1663,16 +1663,24 @@ scope. ## 5.2 Cancellation and Timeouts -Cancellation and timeouts work by throwing an exception to the task. Unless -explicitly trapped this is transparent to the user: the task simply stops when -next scheduled. It is possible to trap the exception, for example to perform -cleanup code, typically in a `finally` clause. The exception thrown to the task -is `uasyncio.CancelledError` in both cancellation and timeout. There is no way -for the task to distinguish between these two cases. - -The `uasyncio.CancelledError` can be trapped, but it is wise to re-raise it: if -the task is `await`ed, the exception can be trapped in the outer scope to -determne the reason for the task's ending. +Cancellation and timeouts work by throwing an exception to the task. This is +unlike a normal exception. If a task cancels another, the running task +continues to execute until it yields to the scheduler. Task cancellation occurs +at that point, whether or not the cancelled task is scheduled for execution: a +task waiting on (say) an `Event` or a `sleep` will be cancelled. + +For tasks launched with `.create_task` the exception is transparent to the +user: the task simply stops when next scheduled. It is possible to trap the +exception, for example to perform cleanup code, typically in a `finally` +clause. The exception thrown to the task is `uasyncio.CancelledError` in both +cancellation and timeout. There is no way for the task to distinguish between +these two cases. + +As stated above, for a task launched with `.create_task` trapping the error is +optional. Where a task is `await`ed, to avoid a halt it must be trapped within +the task, within the `await`ing scope, or both. In the last case the task must +re-raise it after trapping so that the exception can again be trapped in the +outer scope. ## 5.2.1 Task cancellation @@ -1724,10 +1732,9 @@ async def bar(): print('Task is now cancelled') asyncio.run(bar()) ``` -As of [PR6883](https://github.com/micropython/micropython/pull/6883) the -`current_task()` method is supported. This enables a task to pass itself to -other tasks, enabling them to cancel it. It also facilitates the following -pattern: +As of firmware V1.18 the `current_task()` method is supported. This enables a +task to pass itself to other tasks, enabling them to cancel it. It also +facilitates the following pattern: ```python class Foo: From 5c5e6ba44a4b88a382329c178ff8d018e6ae73b5 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 10 Feb 2022 17:00:51 +0000 Subject: [PATCH 127/305] DRIVERS.md notes on cancellation and result retrieval. TUTORIAL improve 5.2. --- v3/docs/DRIVERS.md | 2 ++ v3/docs/TUTORIAL.md | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index a07e17f..e667bbf 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -641,3 +641,5 @@ try: finally: _ = asyncio.new_event_loop() ``` + +###### [Contents](./DRIVERS.md#1-contents) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 19b8c64..07a5696 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1670,7 +1670,7 @@ at that point, whether or not the cancelled task is scheduled for execution: a task waiting on (say) an `Event` or a `sleep` will be cancelled. For tasks launched with `.create_task` the exception is transparent to the -user: the task simply stops when next scheduled. It is possible to trap the +user: the task simply stops as described above. It is possible to trap the exception, for example to perform cleanup code, typically in a `finally` clause. The exception thrown to the task is `uasyncio.CancelledError` in both cancellation and timeout. There is no way for the task to distinguish between @@ -1679,8 +1679,8 @@ these two cases. As stated above, for a task launched with `.create_task` trapping the error is optional. Where a task is `await`ed, to avoid a halt it must be trapped within the task, within the `await`ing scope, or both. In the last case the task must -re-raise it after trapping so that the exception can again be trapped in the -outer scope. +re-raise the exception after trapping so that the error can again be trapped in +the outer scope. ## 5.2.1 Task cancellation From bf5407b9ea3d6a7d24687e874faff1f7b4481434 Mon Sep 17 00:00:00 2001 From: Brant Winter Date: Wed, 6 Apr 2022 20:30:05 +1000 Subject: [PATCH 128/305] fixed typo in encoder.py primitive --- v3/primitives/encoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index 8c206be..b3eadaa 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -18,7 +18,7 @@ def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, div=1, self._pin_y = pin_y self._v = 0 # Hardware value always starts at 0 self._cv = v # Current (divided) value - if ((vmin is not None) and v < min) or ((vmax is not None) and v > vmax): + if ((vmin is not None) and v < vmin) or ((vmax is not None) and v > vmax): raise ValueError('Incompatible args: must have vmin <= v <= vmax') self._tsf = asyncio.ThreadSafeFlag() trig = Pin.IRQ_RISING | Pin.IRQ_FALLING From b8910980c4a07892ca3064ff32aff24862c57a47 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 13 Apr 2022 10:30:02 +0100 Subject: [PATCH 129/305] Add encoder_conditioner schematic. --- v3/docs/encoder_conditioner.png | Bin 0 -> 85140 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 v3/docs/encoder_conditioner.png diff --git a/v3/docs/encoder_conditioner.png b/v3/docs/encoder_conditioner.png new file mode 100644 index 0000000000000000000000000000000000000000..9b4818f4093785a9aa01caa28af6071c2205c5a5 GIT binary patch literal 85140 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�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 z℘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 LGbv%hw?F(Bkgpc+ literal 0 HcmV?d00001 From d850519cf55d3df5253687eb8134430b50b8db0e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 13 Apr 2022 12:39:27 +0100 Subject: [PATCH 130/305] Remove encoder_conditioner schematic. --- v3/docs/encoder_conditioner.png | Bin 85140 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 v3/docs/encoder_conditioner.png diff --git a/v3/docs/encoder_conditioner.png b/v3/docs/encoder_conditioner.png deleted file mode 100644 index 9b4818f4093785a9aa01caa28af6071c2205c5a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85140 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�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 z℘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 LGbv%hw?F(Bkgpc+ From 4c38f3df6c5a9283a858fe8879a3a41638760931 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 14 Apr 2022 12:10:00 +0100 Subject: [PATCH 131/305] DRIVERS.md Improve section on Encoder class. --- v3/docs/DRIVERS.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index e667bbf..c23b681 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -409,7 +409,9 @@ not intended for applications such as CNC machines where is required. Drivers for NC machines must never miss an edge. Contact bounce or vibration induced jitter can cause transitions to occur at a high rate; these must be tracked. Consequently callbacks occur in an interrupt context with the -associated concurrency issues. +associated concurrency issues. These issues, along with general discussion of +MicroPython encoder drivers, are covered +[in this doc](https://github.com/peterhinch/micropython-samples/blob/master/encoders/ENCODERS.md). This driver runs the user supplied callback in an `asyncio` context, so that the callback runs only when other tasks have yielded to the scheduler. This @@ -471,18 +473,15 @@ Class variable: The driver works by maintaining an internal value `._v` which uses hardware interrupts to track the absolute position of the physical encoder. In theory this should be precise with jitter caused by contact bounce being tracked. With -the Adafruit encoder it is imprecise: returning the dial to a given detent -after repeated movements shows a gradual "drift" in position. This occurs on -hosts with hard or soft IRQ's. I attempted to investigate this with various -hardware and software techniques and suspect there may be mechanical issues in -the device. Possibly pulses may occasionally missed with direction-dependent -probability. Unlike optical encoders these low cost controls make no claim to -absolute accuracy. - -This is of little practical consequence as encoder knobs are usually used in -systems where there is user feedback. In a practical application +mechanical encoders it is imprecise unless Schmitt trigger pre-conditioning is +used. The reasons for this and solutions are discussed +[in this doc](https://github.com/peterhinch/micropython-samples/blob/master/encoders/ENCODERS.md). + +An absence of pre-conditioning is often of little practical consequence as +encoder knobs are usually used in systems where there is user feedback. In a +practical application ([micro-gui](https://github.com/peterhinch/micropython-micro-gui)) there is no -obvious evidence of the missed pulses. +obvious evidence of the missed pulses which do occasionally occur. ###### [Contents](./DRIVERS.md#1-contents) From c48520736753ffbf3ef185356ede1cb2f20911c1 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 16 Apr 2022 13:54:56 +0100 Subject: [PATCH 132/305] DRIVERS.md Improve section on Encoder class. --- v3/docs/DRIVERS.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index c23b681..b714fe8 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -461,12 +461,13 @@ Synchronous method: Class variable: * `delay=100` After motion is detected the driver waits for `delay` ms before - reading the current position. This was found useful with the Adafruit encoder - which has mechanical detents, which span multiple increments or decrements. A - delay gives time for motion to stop in the event of a single click movement. - If this occurs the delay ensures just one call to the callback. With no delay - a single click typically gives rise to two callbacks, the second of which can - come as a surprise in visual applications. + reading the current position. A delay can be used to limit the rate at which + the callback is invoked. However where mechanical detents must be tracked, + rapid motion can cause tracking to fail. In this instance the value should be + set to zero. Hardware pre-conditioning must also be used if perfect tracking + is to be achieved - see + [this doc](https://github.com/peterhinch/micropython-samples/blob/master/encoders/ENCODERS.md) + for the reason and for circuit schematics. #### Note on accuracy From 5fb8a4257c9535476d72c3760b434de08ebb27df Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 17 Apr 2022 11:41:52 +0100 Subject: [PATCH 133/305] Improve encoder.py IRQs. --- v3/primitives/encoder.py | 23 ++++++++++++++--------- v3/primitives/tests/encoder_test.py | 6 +++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index b3eadaa..71005f9 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -1,11 +1,8 @@ # encoder.py Asynchronous driver for incremental quadrature encoder. -# Copyright (c) 2021 Peter Hinch +# Copyright (c) 2021-2022 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -# This driver is intended for encoder-based control knobs. It is -# unsuitable for NC machine applications. Please see the docs. - import uasyncio as asyncio from machine import Pin @@ -16,6 +13,8 @@ def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, div=1, callback=lambda a, b : None, args=()): self._pin_x = pin_x self._pin_y = pin_y + self._x = pin_x() + self._y = pin_y() self._v = 0 # Hardware value always starts at 0 self._cv = v # Current (divided) value if ((vmin is not None) and v < vmin) or ((vmax is not None) and v > vmax): @@ -30,14 +29,20 @@ def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, div=1, yirq = pin_y.irq(trigger=trig, handler=self._y_cb) asyncio.create_task(self._run(vmin, vmax, div, callback, args)) - # Hardware IRQ's - def _x_cb(self, pin): - fwd = pin() ^ self._pin_y() + # Hardware IRQ's. Duration 36μs on Pyboard 1. + def _x_cb(self, pin_x): + if (x := pin_x()) == self._x: # IRQ latency: if 2nd edge has + return # occurred there is no movement. + self._x = x + fwd = x ^ self._pin_y() self._v += 1 if fwd else -1 self._tsf.set() - def _y_cb(self, pin): - fwd = pin() ^ self._pin_x() ^ 1 + def _y_cb(self, pin_y): + if (y := pin_y()) == self._y: + return + self._y = y + fwd = y ^ self._pin_x() ^ 1 self._v += 1 if fwd else -1 self._tsf.set() diff --git a/v3/primitives/tests/encoder_test.py b/v3/primitives/tests/encoder_test.py index 78a6ad6..15b919d 100644 --- a/v3/primitives/tests/encoder_test.py +++ b/v3/primitives/tests/encoder_test.py @@ -1,6 +1,6 @@ # encoder_test.py Test for asynchronous driver for incremental quadrature encoder. -# Copyright (c) 2021 Peter Hinch +# Copyright (c) 2021-2022 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file from machine import Pin @@ -8,8 +8,8 @@ from primitives.encoder import Encoder -px = Pin(33, Pin.IN) -py = Pin(25, Pin.IN) +px = Pin(33, Pin.IN, Pin.PULL_UP) +py = Pin(25, Pin.IN, Pin.PULL_UP) def cb(pos, delta): print(pos, delta) From c4f1f83f9cd83e6638990b7e2547fac37c94fdf1 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 17 Apr 2022 14:44:30 +0100 Subject: [PATCH 134/305] Encoder: minimise ISR code. --- v3/primitives/encoder.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index 71005f9..60fbac9 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -29,22 +29,20 @@ def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, div=1, yirq = pin_y.irq(trigger=trig, handler=self._y_cb) asyncio.create_task(self._run(vmin, vmax, div, callback, args)) - # Hardware IRQ's. Duration 36μs on Pyboard 1. + # Hardware IRQ's. Duration 36μs on Pyboard 1 ~50μs on ESP32. + # IRQ latency: 2nd edge may have occured by the time ISR runs, in + # which case there is no movement. def _x_cb(self, pin_x): - if (x := pin_x()) == self._x: # IRQ latency: if 2nd edge has - return # occurred there is no movement. - self._x = x - fwd = x ^ self._pin_y() - self._v += 1 if fwd else -1 - self._tsf.set() + if (x := pin_x()) != self._x: + self._x = x + self._v += 1 if x ^ self._pin_y() else -1 + self._tsf.set() def _y_cb(self, pin_y): - if (y := pin_y()) == self._y: - return - self._y = y - fwd = y ^ self._pin_x() ^ 1 - self._v += 1 if fwd else -1 - self._tsf.set() + if (y := pin_y()) != self._y: + self._y = y + self._v += 1 if y ^ self._pin_x() ^ 1 else -1 + self._tsf.set() async def _run(self, vmin, vmax, div, cb, args): pv = self._v # Prior hardware value From 5c7da153e00289b466f3fc5285733ace21bdae33 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 17 Apr 2022 18:04:25 +0100 Subject: [PATCH 135/305] encoder.py value can be reduced modulo N --- v3/docs/DRIVERS.md | 36 ++++++++++++++---------------------- v3/primitives/encoder.py | 11 ++++++----- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index b714fe8..69b4e72 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -447,13 +447,16 @@ Constructor arguments: 5. `vmax=None` As above. If `vmin` and/or `vmax` are specified, a `ValueError` will be thrown if the initial value `v` does not conform with the limits. 6. `div=1` A value > 1 causes the motion rate of the encoder to be divided - down, to produce a virtual encoder with lower resolution. This was found usefl - in some applications with the Adafruit encoder. + down, to produce a virtual encoder with lower resolution. This can enable + tracking of mechanical detents - typical values are then 4 or 2 pulses per + click. 7. `callback=lambda a, b : None` Optional callback function. The callback receives two integer args, `v` being the virtual encoder's current value and `delta` being the signed difference between the current value and the previous one. Further args may be appended by the following. 8. `args=()` An optional tuple of positionl args for the callback. + 9. `mod=0` An integer `N > 0` causes the divided value to be reduced modulo + `N` - useful for controlling rotary devices. Synchronous method: * `value` No args. Returns an integer being the virtual encoder's current @@ -462,27 +465,16 @@ Synchronous method: Class variable: * `delay=100` After motion is detected the driver waits for `delay` ms before reading the current position. A delay can be used to limit the rate at which - the callback is invoked. However where mechanical detents must be tracked, - rapid motion can cause tracking to fail. In this instance the value should be - set to zero. Hardware pre-conditioning must also be used if perfect tracking - is to be achieved - see - [this doc](https://github.com/peterhinch/micropython-samples/blob/master/encoders/ENCODERS.md) - for the reason and for circuit schematics. - -#### Note on accuracy - -The driver works by maintaining an internal value `._v` which uses hardware -interrupts to track the absolute position of the physical encoder. In theory -this should be precise with jitter caused by contact bounce being tracked. With -mechanical encoders it is imprecise unless Schmitt trigger pre-conditioning is -used. The reasons for this and solutions are discussed -[in this doc](https://github.com/peterhinch/micropython-samples/blob/master/encoders/ENCODERS.md). + the callback is invoked. + +Not all combinations of arguments make mathematical sense. The order in which +operations are applied is: + 1. Apply division if specified. + 2. Restrict the divided value by any maximum or minimum. + 3. Reduce modulo N if specified. -An absence of pre-conditioning is often of little practical consequence as -encoder knobs are usually used in systems where there is user feedback. In a -practical application -([micro-gui](https://github.com/peterhinch/micropython-micro-gui)) there is no -obvious evidence of the missed pulses which do occasionally occur. +See [this doc](https://github.com/peterhinch/micropython-samples/blob/master/encoders/ENCODERS.md) +for further information on encoders and their limitations. ###### [Contents](./DRIVERS.md#1-contents) diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index 60fbac9..8370c5d 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -10,7 +10,7 @@ class Encoder: delay = 100 # Pause (ms) for motion to stop def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, div=1, - callback=lambda a, b : None, args=()): + callback=lambda a, b : None, args=(), mod=0): self._pin_x = pin_x self._pin_y = pin_y self._x = pin_x() @@ -27,7 +27,7 @@ def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, div=1, except TypeError: # hard arg is unsupported on some hosts xirq = pin_x.irq(trigger=trig, handler=self._x_cb) yirq = pin_y.irq(trigger=trig, handler=self._y_cb) - asyncio.create_task(self._run(vmin, vmax, div, callback, args)) + asyncio.create_task(self._run(vmin, vmax, div, mod, callback, args)) # Hardware IRQ's. Duration 36μs on Pyboard 1 ~50μs on ESP32. # IRQ latency: 2nd edge may have occured by the time ISR runs, in @@ -41,14 +41,13 @@ def _x_cb(self, pin_x): def _y_cb(self, pin_y): if (y := pin_y()) != self._y: self._y = y - self._v += 1 if y ^ self._pin_x() ^ 1 else -1 + self._v -= 1 if y ^ self._pin_x() else -1 self._tsf.set() - async def _run(self, vmin, vmax, div, cb, args): + async def _run(self, vmin, vmax, div, modulo, cb, args): pv = self._v # Prior hardware value cv = self._cv # Current divided value as passed to callback pcv = cv # Prior divided value passed to callback - mod = 0 delay = self.delay while True: await self._tsf.wait() @@ -66,6 +65,8 @@ async def _run(self, vmin, vmax, div, cb, args): cv = min(cv, vmax) if vmin is not None: cv = max(cv, vmin) + if modulo: + cv %= modulo self._cv = cv # For value() if cv != pcv: cb(cv, cv - pcv, *args) # User CB in uasyncio context From 60547a7a21b7be09b5df7c17bf1a93ef4eab6352 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 21 Apr 2022 13:49:38 +0100 Subject: [PATCH 136/305] encoder.py: improve tracking of detents. --- v3/docs/DRIVERS.md | 18 +++++++-------- v3/primitives/encoder.py | 50 +++++++++++++++++++--------------------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 69b4e72..0600316 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -442,21 +442,21 @@ Constructor arguments: as `Pin.IN` and have pullups. 2. `pin_y` Ditto. 3. `v=0` Initial value. - 4. `vmin=None` By default the `value` of the encoder can vary without limit. - Optionally maximum and/or minimum limits can be set. - 5. `vmax=None` As above. If `vmin` and/or `vmax` are specified, a `ValueError` - will be thrown if the initial value `v` does not conform with the limits. - 6. `div=1` A value > 1 causes the motion rate of the encoder to be divided + 4. `div=1` A value > 1 causes the motion rate of the encoder to be divided down, to produce a virtual encoder with lower resolution. This can enable tracking of mechanical detents - typical values are then 4 or 2 pulses per click. - 7. `callback=lambda a, b : None` Optional callback function. The callback + 5. `vmin=None` By default the `value` of the encoder can vary without limit. + Optionally maximum and/or minimum limits can be set. + 6. `vmax=None` As above. If `vmin` and/or `vmax` are specified, a `ValueError` + will be thrown if the initial value `v` does not conform with the limits. + 7. `mod=None` An integer `N > 0` causes the divided value to be reduced modulo + `N` - useful for controlling rotary devices. + 8. `callback=lambda a, b : None` Optional callback function. The callback receives two integer args, `v` being the virtual encoder's current value and `delta` being the signed difference between the current value and the previous one. Further args may be appended by the following. - 8. `args=()` An optional tuple of positionl args for the callback. - 9. `mod=0` An integer `N > 0` causes the divided value to be reduced modulo - `N` - useful for controlling rotary devices. + 9. `args=()` An optional tuple of positionl args for the callback. Synchronous method: * `value` No args. Returns an integer being the virtual encoder's current diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index 8370c5d..289b877 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -7,15 +7,15 @@ from machine import Pin class Encoder: - delay = 100 # Pause (ms) for motion to stop + delay = 100 # Pause (ms) for motion to stop/limit callback frequency - def __init__(self, pin_x, pin_y, v=0, vmin=None, vmax=None, div=1, - callback=lambda a, b : None, args=(), mod=0): + def __init__(self, pin_x, pin_y, v=0, div=1, vmin=None, vmax=None, + mod=None, callback=lambda a, b : None, args=()): self._pin_x = pin_x self._pin_y = pin_y self._x = pin_x() self._y = pin_y() - self._v = 0 # Hardware value always starts at 0 + self._v = v * div # Initialise hardware value self._cv = v # Current (divided) value if ((vmin is not None) and v < vmin) or ((vmax is not None) and v > vmax): raise ValueError('Incompatible args: must have vmin <= v <= vmax') @@ -44,33 +44,31 @@ def _y_cb(self, pin_y): self._v -= 1 if y ^ self._pin_x() else -1 self._tsf.set() - async def _run(self, vmin, vmax, div, modulo, cb, args): + async def _run(self, vmin, vmax, div, mod, cb, args): pv = self._v # Prior hardware value - cv = self._cv # Current divided value as passed to callback - pcv = cv # Prior divided value passed to callback + pcv = self._cv # Prior divided value passed to callback + lcv = pcv # Current value after limits applied + plcv = pcv # Previous value after limits applied delay = self.delay while True: await self._tsf.wait() - await asyncio.sleep_ms(delay) # Wait for motion to stop - new = self._v # Sample hardware (atomic read) - a = new - pv # Hardware change - # Ensure symmetrical bahaviour for + and - values - q, r = divmod(abs(a), div) - if a < 0: - r = -r - q = -q - pv = new - r # Hardware value when local value was updated - cv += q - if vmax is not None: - cv = min(cv, vmax) - if vmin is not None: - cv = max(cv, vmin) - if modulo: - cv %= modulo - self._cv = cv # For value() - if cv != pcv: - cb(cv, cv - pcv, *args) # User CB in uasyncio context + await asyncio.sleep_ms(delay) # Wait for motion to stop. + hv = self._v # Sample hardware (atomic read). + if hv == pv: # A change happened but was negated before + continue # this got scheduled. Nothing to do. + pv = hv + cv = round(hv / div) # cv is divided value. + if not (dv := cv - pcv): # dv is change in divided value. + continue # No change + lcv += dv # lcv: divided value with limits/mod applied + lcv = lcv if vmax is None else min(vmax, lcv) + lcv = lcv if vmin is None else max(vmin, lcv) + lcv = lcv if mod is None else lcv % mod + self._cv = lcv # update ._cv for .value() before CB. + if lcv != plcv: + cb(lcv, lcv - plcv, *args) # Run user CB in uasyncio context pcv = cv + plcv = lcv def value(self): return self._cv From 3dab4b5cb267db3aa2f1c9feafa4408b15104df4 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 21 Apr 2022 17:55:51 +0100 Subject: [PATCH 137/305] encoder.py: improve tracking of detents. --- v3/primitives/encoder.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index 289b877..d0c5517 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -3,6 +3,12 @@ # Copyright (c) 2021-2022 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file +# Thanks are due to @ilium007 for identifying the issue of tracking detents, +# https://github.com/peterhinch/micropython-async/issues/82. +# Also to Mike Teachman (@miketeachman) for design discussions and testing +# against a state table design +# https://github.com/miketeachman/micropython-rotary/blob/master/rotary.py + import uasyncio as asyncio from machine import Pin From 85dd861c84dce165414492325ee3241be434c97c Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 8 May 2022 18:40:01 +0100 Subject: [PATCH 138/305] DRIVERS.md: Improve encoder doc, add encoder_stop.py --- v3/docs/DRIVERS.md | 4 ++- v3/primitives/tests/encoder_stop.py | 39 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 v3/primitives/tests/encoder_stop.py diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 0600316..03f4318 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -465,7 +465,9 @@ Synchronous method: Class variable: * `delay=100` After motion is detected the driver waits for `delay` ms before reading the current position. A delay can be used to limit the rate at which - the callback is invoked. + the callback is invoked. This is a minimal approach. See + [this script](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/tests/encoder_stop.py) + for a way to create a callback which runs only when the encoder stops moving. Not all combinations of arguments make mathematical sense. The order in which operations are applied is: diff --git a/v3/primitives/tests/encoder_stop.py b/v3/primitives/tests/encoder_stop.py new file mode 100644 index 0000000..ed75e8d --- /dev/null +++ b/v3/primitives/tests/encoder_stop.py @@ -0,0 +1,39 @@ +# encoder_stop.py Demo of callback which occurs after motion has stopped. + +from machine import Pin +import uasyncio as asyncio +from primitives.encoder import Encoder +from primitives.delay_ms import Delay_ms + +px = Pin('X1', Pin.IN, Pin.PULL_UP) +py = Pin('X2', Pin.IN, Pin.PULL_UP) + +tim = Delay_ms(duration=400) # High value for test +d = 0 + +def tcb(pos, delta): # User callback gets args of encoder cb + global d + d = 0 + print(pos, delta) + +def cb(pos, delta): # Encoder callback occurs rapidly + global d + tim.trigger() # Postpone the user callback + tim.callback(tcb, (pos, d := d + delta)) # and update its args + +async def main(): + while True: + await asyncio.sleep(1) + +def test(): + print('Running encoder test. Press ctrl-c to teminate.') + Encoder.delay = 0 # No need for this delay + enc = Encoder(px, py, callback=cb) + try: + asyncio.run(main()) + except KeyboardInterrupt: + print('Interrupted') + finally: + asyncio.new_event_loop() + +test() From 00d9e9fe38cc2ad53b0ee126e3d4a1a871fa8731 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 8 Jun 2022 16:31:53 +0100 Subject: [PATCH 139/305] encoder.py: delay is now a constructor arg. --- v3/docs/DRIVERS.md | 15 ++++++++------- v3/primitives/encoder.py | 5 +++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 03f4318..224c4f1 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -437,6 +437,9 @@ value since the previous time the callback ran. ## 6.1 Encoder class +Existing users: the `delay` parameter is now a constructor arg rather than a +class varaiable. + Constructor arguments: 1. `pin_x` Initialised `machine.Pin` instances for the switch. Should be set as `Pin.IN` and have pullups. @@ -457,18 +460,16 @@ Constructor arguments: `delta` being the signed difference between the current value and the previous one. Further args may be appended by the following. 9. `args=()` An optional tuple of positionl args for the callback. - -Synchronous method: - * `value` No args. Returns an integer being the virtual encoder's current - value. - -Class variable: - * `delay=100` After motion is detected the driver waits for `delay` ms before + 10. `delay=100` After motion is detected the driver waits for `delay` ms before reading the current position. A delay can be used to limit the rate at which the callback is invoked. This is a minimal approach. See [this script](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/tests/encoder_stop.py) for a way to create a callback which runs only when the encoder stops moving. + Synchronous method: + * `value` No args. Returns an integer being the virtual encoder's current + value. + Not all combinations of arguments make mathematical sense. The order in which operations are applied is: 1. Apply division if specified. diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index d0c5517..759422b 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -13,16 +13,17 @@ from machine import Pin class Encoder: - delay = 100 # Pause (ms) for motion to stop/limit callback frequency def __init__(self, pin_x, pin_y, v=0, div=1, vmin=None, vmax=None, - mod=None, callback=lambda a, b : None, args=()): + mod=None, callback=lambda a, b : None, args=(), delay=100): self._pin_x = pin_x self._pin_y = pin_y self._x = pin_x() self._y = pin_y() self._v = v * div # Initialise hardware value self._cv = v # Current (divided) value + self.delay = delay # Pause (ms) for motion to stop/limit callback frequency + if ((vmin is not None) and v < vmin) or ((vmax is not None) and v > vmax): raise ValueError('Incompatible args: must have vmin <= v <= vmax') self._tsf = asyncio.ThreadSafeFlag() From 4d5c05040e9f611d08fcfd3f55709539c83bc97d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 25 Jun 2022 13:43:25 +0100 Subject: [PATCH 140/305] Add note about Stream.drain concurrency. --- v3/README.md | 4 +++- v3/docs/TUTORIAL.md | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/v3/README.md b/v3/README.md index 27b73f5..9b269b0 100644 --- a/v3/README.md +++ b/v3/README.md @@ -50,7 +50,9 @@ useful in their own right: This [monitor](https://github.com/peterhinch/micropython-monitor) enables a running `uasyncio` application to be monitored using a Pi Pico, ideally with a -scope or logic analyser. +scope or logic analyser. If designing hardware it is suggested to provide +access to a UART tx pin, or alternatively to three GPIO pins, to enable this to +be used if required. ![Image](https://github.com/peterhinch/micropython-monitor/raw/master/images/monitor.jpg) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 07a5696..62b07e4 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2000,7 +2000,10 @@ asyncio.run(main()) Writing to a `StreamWriter` occurs in two stages. The synchronous `.write` method concatenates data for later transmission. The asynchronous `.drain` causes transmission. To avoid allocation call `.drain` after each call to -`.write`. +`.write`. Do not have multiple tasks calling `.drain` concurrently: this can +result in data corruption for reasons detailed +[here](https://github.com/micropython/micropython/issues/6621). The solution is +to use a `Queue` or a `Lock`. The mechanism works because the device driver (written in C) implements the following methods: `ioctl`, `read`, `readline` and `write`. See From e9be6bffb6c9813793a9798ae612c07220dcaef9 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 25 Jun 2022 18:08:40 +0100 Subject: [PATCH 141/305] Tutorial: Note about Stream.drain concurrency and Barrier. --- v3/docs/TUTORIAL.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 62b07e4..20bfb5d 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -987,9 +987,9 @@ the [Message class](./TUTORIAL.md#39-message) uses this approach to provide an ## 3.7 Barrier -This is an unofficial primitive and has no counterpart in CPython asyncio. It -is based on a Microsoft primitive. While similar in purpose to `gather` there -are differences described below. +This is an unofficial implementation of a primitive supported in +[CPython 3.11](https://docs.python.org/3.11/library/asyncio-sync.html#asyncio.Barrier). +While similar in purpose to `gather` there are differences described below. Its principal purpose is to cause multiple coros to rendezvous at a particular point. For example producer and consumer coros can synchronise at a point where @@ -2000,10 +2000,12 @@ asyncio.run(main()) Writing to a `StreamWriter` occurs in two stages. The synchronous `.write` method concatenates data for later transmission. The asynchronous `.drain` causes transmission. To avoid allocation call `.drain` after each call to -`.write`. Do not have multiple tasks calling `.drain` concurrently: this can +`.write`. If multiple tasks are to write to the same `StreamWriter`, the best +solution is to implement a shared `Queue`. Each task writes to the `Queue` and +a single task waits on it, issuing `.write` and `.drain` whenever data is +queued. Do not have multiple tasks calling `.drain` concurrently: this can result in data corruption for reasons detailed -[here](https://github.com/micropython/micropython/issues/6621). The solution is -to use a `Queue` or a `Lock`. +[here](https://github.com/micropython/micropython/issues/6621). The mechanism works because the device driver (written in C) implements the following methods: `ioctl`, `read`, `readline` and `write`. See From 1850348e976daa5b0429e6cb0e716a492bebd7a5 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 20 Jul 2022 07:58:45 +0100 Subject: [PATCH 142/305] TUTORIAL.md: Document task group. --- v3/docs/TUTORIAL.md | 83 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 20bfb5d..4eadcbf 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -27,7 +27,9 @@ REPL. 3. [Synchronisation](./TUTORIAL.md#3-synchronisation) 3.1 [Lock](./TUTORIAL.md#31-lock) 3.2 [Event](./TUTORIAL.md#32-event) - 3.3 [gather](./TUTORIAL.md#33-gather) + 3.3 [Coordinating multiple tasks](./TUTORIAL.md#33-coordinating-multiple-tasks) +      3.3.1 [gather](./TUTORIAL.md#331-gather) +      3.3.2 [TaskGroups](./TUTORIAL.md#332-taskgroups) 3.4 [Semaphore](./TUTORIAL.md#34-semaphore)      3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore) 3.5 [Queue](./TUTORIAL.md#35-queue) @@ -701,7 +703,15 @@ constant creation of tasks. Arguably the `Barrier` class is the best approach. ###### [Contents](./TUTORIAL.md#contents) -## 3.3 gather +## 3.3 Coordinating multiple tasks + +Several tasks may be launched together with the launching task pausing until +all have completed. The `gather` mechanism is supported by CPython and +MicroPython. CPython 3.11 adds a `TaskGroup` class which is particularly +suited to applications where runtime exceptions may be encountered. It is not +yet officially supported by MicroPython. + +### 3.3.1 gather This official `uasyncio` asynchronous method causes a number of tasks to run, pausing until all have either run to completion or been terminated by @@ -714,7 +724,7 @@ res = await asyncio.gather(*tasks, return_exceptions=True) The keyword-only boolean arg `return_exceptions` determines the behaviour in the event of a cancellation or timeout of tasks. If `False` the `gather` terminates immediately, raising the relevant exception which should be trapped -by the caller. If `True` the `gather` continues to block until all have either +by the caller. If `True` the `gather` continues to pause until all have either run to completion or been terminated by cancellation or timeout. In this case tasks which have been terminated will return the exception object in the list of return values. @@ -767,6 +777,73 @@ async def main(): print('Cancelled') print('Result: ', res) +asyncio.run(main()) +``` +### 3.3.2 TaskGroups + +The `TaskGroup` class is unofficially provided by +[this PR](https://github.com/micropython/micropython/pull/8791). It is well +suited to applications where one or more of a group of tasks is subject to +runtime exceptions. A `TaskGroup` is instantiated in an asynchronous context +manager. The `TaskGroup` instantiates member tasks. When all have run to +completion the context manager terminates. Return values from member tasks +cannot be retrieved. Results should be passed in other ways such as via bound +variables, queues etc. + +An exception in a member task not trapped by that task is propagated to the +task that created the `TaskGroup`. All tasks in the `TaskGroup` then terminate +in an orderly fashion: cleanup code in any `finally` clause will run. When all +cleanup code has completed, the context manager completes, and execution passes +to an exception handler in an outer scope. + +If a member task is cancelled in code, that task terminates in an orderly way +but the other members continue to run. + +The following illustrates the basic salient points of using a `TaskGroup`: +```python +import uasyncio as asyncio +async def foo(n): + for x in range(10 + n): + print(f"Task {n} running.") + await asyncio.sleep(1 + n/10) + print(f"Task {n} done") + +async def main(): + async with asyncio.TaskGroup() as tg: # Context manager pauses until members terminate + for n in range(4): + tg.create_task(foo(n)) # tg.create_task() creates a member task + print("TaskGroup done") # All tasks have terminated + +asyncio.run(main()) +``` +This more complete example illustrates an exception which is not trapped by the +member task. Cleanup code on all members runs when the exception occurs, +followed by exception handling code in `main()`. +```python +import uasyncio as asyncio +fail = True # Set False to demo normal completion +async def foo(n): + print(f"Task {n} running...") + try: + for x in range(10 + n): + await asyncio.sleep(1 + n/10) + if n==0 and x==5 and fail: + raise OSError("Uncaught exception in task.") + print(f"Task {n} done") + finally: + print(f"Task {n} cleanup") + +async def main(): + try: + async with asyncio.TaskGroup() as tg: + for n in range(4): + tg.create_task(foo(n)) + print("TaskGroup done") # Does not get here if a task throws exception + except Exception as e: + print(f'TaskGroup caught exception: "{e}"') + finally: + print("TaskGroup finally") + asyncio.run(main()) ``` From 40f08b225289b30340b3f1d2ca5cc284538222f2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 20 Jul 2022 08:00:51 +0100 Subject: [PATCH 143/305] TUTORIAL.md: Document task group. --- v3/docs/TUTORIAL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 4eadcbf..837c0d8 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -29,7 +29,7 @@ REPL. 3.2 [Event](./TUTORIAL.md#32-event) 3.3 [Coordinating multiple tasks](./TUTORIAL.md#33-coordinating-multiple-tasks)      3.3.1 [gather](./TUTORIAL.md#331-gather) -      3.3.2 [TaskGroups](./TUTORIAL.md#332-taskgroups) +      3.3.2 [TaskGroups](./TUTORIAL.md#332-taskgroups) Not yet in official build. 3.4 [Semaphore](./TUTORIAL.md#34-semaphore)      3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore) 3.5 [Queue](./TUTORIAL.md#35-queue) From cdeab477ece23752810944885134381f84352d1e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 20 Jul 2022 09:31:00 +0100 Subject: [PATCH 144/305] Doc task group. Callback interface for ESP32 touchpads. --- v3/docs/DRIVERS.md | 37 +++++++++++++++++++++++++++++++++++++ v3/docs/TUTORIAL.md | 2 +- v3/primitives/__init__.py | 1 + v3/primitives/pushbutton.py | 32 +++++++++++++++++++++++++++----- 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 224c4f1..4229576 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -24,6 +24,7 @@ goes outside defined bounds. 4.1 [Pushbutton class](./DRIVERS.md#41-pushbutton-class)      4.1.1 [The suppress constructor argument](./DRIVERS.md#411-the-suppress-constructor-argument)      4.1.2 [The sense constructor argument](./DRIVERS.md#412-the-sense-constructor-argument) + 4.2 [ESP32Touch class](./DRIVERS.md#42-esp32touch-class) 5. [ADC monitoring](./DRIVERS.md#5-adc-monitoring) Pause until an ADC goes out of bounds 5.1 [AADC class](./DRIVERS.md#51-aadc-class) 5.2 [Design note](./DRIVERS.md#52-design-note) @@ -321,6 +322,42 @@ the `closed` state of the button is active `high` or active `low`. See [Advanced use of callbacks](./DRIVERS.md#8-advanced-use-of-callbacks) for ways to retrieve a result from a callback and to cancel a task. +## 4.2 ESP32Touch class + +This subclass of `Pushbutton` supports ESP32 touchpads providing a callback +based interface. See the +[official docs](http://docs.micropython.org/en/latest/esp32/quickref.html#capacitive-touch). + +API and usage are as per `Pushbutton` with the following provisos: + 1. The `sense` constructor arg is not supported. + 2. The `Pin` instance passed to the constructor must support the touch + interface. It is instantiated without args, as per the example below. + 3. There is an additional class variable `sensitivity` which should be a float + in range 0.0..1.0. The value `v` returned by the touchpad is read on + initialisation. The touchpad is polled and if the value drops below + `v * sensitivity` the pad is assumed to be pressed. + +Example usage: +```python +from machine import Pin +from primitives import ESP32Touch +import uasyncio as asyncio + +async def main(): + tb = ESP32Touch(Pin(15), suppress=True) + tb.press_func(lambda : print("press")) + tb.double_func(lambda : print("double")) + tb.long_func(lambda : print("long")) + tb.release_func(lambda : print("release")) + while True: + await asyncio.sleep(1) + +asyncio.run(main()) +``` +If a touchpad is touched on initialisation no callbacks will occur even when +the pad is released. Initial button state is always `False`. Normal behaviour +will commence with subsequent touches. + ###### [Contents](./DRIVERS.md#1-contents) # 5. ADC monitoring diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 837c0d8..d244b89 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -38,7 +38,7 @@ REPL. 3.8 [Delay_ms](./TUTORIAL.md#38-delay_ms-class) Software retriggerable delay. 3.9 [Message](./TUTORIAL.md#39-message) 3.10 [Synchronising to hardware](./TUTORIAL.md#310-synchronising-to-hardware) - Debouncing switches, pushbuttons and encoder knobs. Taming ADC's. + Debouncing switches, pushbuttons, ESP32 touchpads and encoder knobs. Taming ADC's. 4. [Designing classes for asyncio](./TUTORIAL.md#4-designing-classes-for-asyncio) 4.1 [Awaitable classes](./TUTORIAL.md#41-awaitable-classes)      4.1.1 [Use in context managers](./TUTORIAL.md#411-use-in-context-managers) diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index bed9f9b..2a9bca2 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -38,6 +38,7 @@ def _handle_exception(loop, context): "Encode": "encoder_async", "Message": "message", "Pushbutton": "pushbutton", + "ESP32Touch": "pushbutton", "Queue": "queue", "Semaphore": "semaphore", "BoundedSemaphore": "semaphore", diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index 225557e..dd1a386 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -5,12 +5,12 @@ import uasyncio as asyncio import utime as time -from . import launch -from primitives.delay_ms import Delay_ms +from . import launch, Delay_ms +try: + from machine import TouchPad +except ImportError: + pass - -# An alternative Pushbutton solution with lower RAM use is available here -# https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py class Pushbutton: debounce_ms = 50 long_press_ms = 1000 @@ -109,3 +109,25 @@ async def buttoncheck(self): def deinit(self): self._run.cancel() + + +class ESP32Touch(Pushbutton): + sensitivity = 0.9 + def __init__(self, pin, suppress=False): + self._thresh = 0 # Detection threshold + self._rawval = 0 + try: + self._pad = TouchPad(pin) + except ValueError: + raise ValueError(pin) # Let's have a bit of information :) + super().__init__(pin, suppress, False) + + # Current logical button state: True == touched + def rawstate(self): + rv = self._pad.read() # ~220μs + if rv > self._rawval: # Either initialisation or pad was touched + self._rawval = rv # when initialised and has now been released + self._thresh = round(rv * ESP32Touch.sensitivity) + return False # Untouched + return rv < self._thresh + From 56d8fa06df6366a7a71710ba9d5651ab74c49e98 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 20 Jul 2022 09:59:19 +0100 Subject: [PATCH 145/305] Doc task group. Callback interface for ESP32 touchpads. --- v3/docs/DRIVERS.md | 3 ++- v3/docs/TUTORIAL.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 4229576..8f7064c 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -335,7 +335,8 @@ API and usage are as per `Pushbutton` with the following provisos: 3. There is an additional class variable `sensitivity` which should be a float in range 0.0..1.0. The value `v` returned by the touchpad is read on initialisation. The touchpad is polled and if the value drops below - `v * sensitivity` the pad is assumed to be pressed. + `v * sensitivity` the pad is assumed to be pressed. Default `sensitivity` is + 0.9 but this is subject to change. Example usage: ```python diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index d244b89..e6fa29a 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1365,6 +1365,7 @@ The following hardware-related classes are documented [here](./DRIVERS.md): * `Switch` A debounced switch which can trigger open and close user callbacks. * `Pushbutton` Debounced pushbutton with callbacks for pressed, released, long press or double-press. + * `ESP32Touch` Extends `Pushbutton` class to support ESP32 touchpads. * `Encoder` An asynchronous interface for control knobs with switch contacts configured as a quadrature encoder. * `AADC` Asynchronous ADC. A task can pause until the value read from an ADC From 38c24ec057a197840376d266bb0df9f55d1d0ee7 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 21 Jul 2022 13:08:22 +0100 Subject: [PATCH 146/305] pushbutton.py: ESP32Touch uses integer maths. --- v3/docs/DRIVERS.md | 26 +++++++++++++++++--------- v3/primitives/pushbutton.py | 9 ++++++--- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 8f7064c..07d1fba 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -43,9 +43,9 @@ goes outside defined bounds. # 2. Installation and usage -The drivers require a daily build of firmware or a release build >=1.15. The -drivers are in the primitives package. To install copy the `primitives` -directory and its contents to the target hardware. +The drivers require firmware version >=1.15. The drivers are in the primitives +package. To install copy the `primitives` directory and its contents to the +target hardware. Drivers are imported with: ```python @@ -332,17 +332,21 @@ API and usage are as per `Pushbutton` with the following provisos: 1. The `sense` constructor arg is not supported. 2. The `Pin` instance passed to the constructor must support the touch interface. It is instantiated without args, as per the example below. - 3. There is an additional class variable `sensitivity` which should be a float - in range 0.0..1.0. The value `v` returned by the touchpad is read on - initialisation. The touchpad is polled and if the value drops below - `v * sensitivity` the pad is assumed to be pressed. Default `sensitivity` is - 0.9 but this is subject to change. + 3. There is an additional classmethod `threshold` which takes an integer arg. + The arg represents the detection threshold as a percentage. + +The driver determines the untouched state by periodically polling +`machine.TouchPad.read()` and storing its maximum value. If it reads a value +below `maximum * threshold / 100` a touch is deemed to have occurred. Default +threshold is currently 80% but this is subject to change. Example usage: ```python from machine import Pin -from primitives import ESP32Touch import uasyncio as asyncio +from primitives import ESP32Touch + +ESP32Touch.threshold(70) # optional async def main(): tb = ESP32Touch(Pin(15), suppress=True) @@ -359,6 +363,10 @@ If a touchpad is touched on initialisation no callbacks will occur even when the pad is released. Initial button state is always `False`. Normal behaviour will commence with subsequent touches. +The best threshold value depends on physical design. Directly touching a large +pad will result in a low value from `machine.TouchPad.read()`. A small pad +covered with an insulating film will yield a smaller change. + ###### [Contents](./DRIVERS.md#1-contents) # 5. ADC monitoring diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index dd1a386..4859f33 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -112,7 +112,11 @@ def deinit(self): class ESP32Touch(Pushbutton): - sensitivity = 0.9 + thresh = (80 << 8) // 100 + @classmethod + def threshold(cls, val): + cls.thresh = (val << 8) // 100 + def __init__(self, pin, suppress=False): self._thresh = 0 # Detection threshold self._rawval = 0 @@ -127,7 +131,6 @@ def rawstate(self): rv = self._pad.read() # ~220μs if rv > self._rawval: # Either initialisation or pad was touched self._rawval = rv # when initialised and has now been released - self._thresh = round(rv * ESP32Touch.sensitivity) + self._thresh = (rv * ESP32Touch.thresh) >> 8 return False # Untouched return rv < self._thresh - From 9983bc750263420c88d1bad417a446be6450193d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 21 Jul 2022 19:34:29 +0100 Subject: [PATCH 147/305] pushbutton.py: Add sensitivity range check. --- v3/primitives/pushbutton.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index 4859f33..008a0c4 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -115,6 +115,8 @@ class ESP32Touch(Pushbutton): thresh = (80 << 8) // 100 @classmethod def threshold(cls, val): + if not (isinstance(val, int) and 0 < val < 100): + raise ValueError("Threshold must be in range 1-99") cls.thresh = (val << 8) // 100 def __init__(self, pin, suppress=False): From 01612899d800797de4e99abae5070c027c50e8a0 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 23 Jul 2022 08:41:20 +0100 Subject: [PATCH 148/305] TUTORIAL: Clarify that Delay_ms.wait() is async. --- v3/docs/TUTORIAL.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index e6fa29a..3d43095 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1195,7 +1195,7 @@ Constructor arguments (defaults in brackets): 4. `duration` Integer, default 1000ms. The default timer period where no value is passed to the `trigger` method. -Methods: +Synchronous methods: 1. `trigger` optional argument `duration=0`. A timeout will occur after `duration` ms unless retriggered. If no arg is passed the period will be that @@ -1209,11 +1209,13 @@ Methods: 5. `rvalue` No argument. If a timeout has occurred and a callback has run, returns the return value of the callback. If a coroutine was passed, returns the `Task` instance. This allows the `Task` to be cancelled or awaited. - 6. `wait` One or more tasks may wait on a `Delay_ms` instance. Execution will - proceed when the instance has timed out. - 7. `callback` args `func=None`, `args=()`. Allows the callable and its args to + 6. `callback` args `func=None`, `args=()`. Allows the callable and its args to be assigned, reassigned or disabled at run time. - 8. `deinit` No args. Cancels the running task. See [Object scope](./TUTORIAL.md#44-object-scope). + 7. `deinit` No args. Cancels the running task. See [Object scope](./TUTORIAL.md#44-object-scope). + +Asynchronous method: + 1. `wait` One or more tasks may wait on a `Delay_ms` instance. Pause until the + delay instance has timed out. In this example a `Delay_ms` instance is created with the default duration of 1s. It is repeatedly triggered for 5 secs, preventing the callback from From c56b423066acad92acdf62b4a4a8d577cc811c36 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 27 Jul 2022 17:42:24 +0100 Subject: [PATCH 149/305] primitives/Delay_ms: .clear() clears Event. --- v3/docs/TUTORIAL.md | 2 ++ v3/primitives/delay_ms.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 3d43095..b639af0 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1212,6 +1212,7 @@ Synchronous methods: 6. `callback` args `func=None`, `args=()`. Allows the callable and its args to be assigned, reassigned or disabled at run time. 7. `deinit` No args. Cancels the running task. See [Object scope](./TUTORIAL.md#44-object-scope). + 8. `clear` No args. Clears the `Event` decribed in `wait` below. Asynchronous method: 1. `wait` One or more tasks may wait on a `Delay_ms` instance. Pause until the @@ -1251,6 +1252,7 @@ from primitives.delay_ms import Delay_ms async def foo(n, d): await d.wait() + d.clear() # Task waiting on the Event must clear it print('Done in foo no.', n) async def my_app(): diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py index 4cc53a7..d5306ba 100644 --- a/v3/primitives/delay_ms.py +++ b/v3/primitives/delay_ms.py @@ -26,6 +26,7 @@ def __init__(self, func=None, args=(), duration=1000): self._trig = asyncio.ThreadSafeFlag() self._tout = asyncio.Event() # Timeout event self.wait = self._tout.wait # Allow: await wait_ms.wait() + self.clear = self._tout.clear self._ttask = self._fake # Timer task self._mtask = asyncio.create_task(self._run()) #Main task @@ -40,7 +41,6 @@ async def _run(self): async def _timer(self, dt): await asyncio.sleep_ms(dt) self._tout.set() # Only gets here if not cancelled. - self._tout.clear() self._busy = False if self._func is not None: self._retn = launch(self._func, self._args) From 12a7a036b61ac90a718e0e5c8ac4dd24b5e8a715 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 30 Jul 2022 10:55:43 +0100 Subject: [PATCH 150/305] Pushbutton and Switch classes: add event API. --- v3/docs/DRIVERS.md | 192 ++++++++++++++---------------------- v3/primitives/pushbutton.py | 14 ++- v3/primitives/switch.py | 8 +- 3 files changed, 90 insertions(+), 124 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 07d1fba..4009826 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -3,7 +3,9 @@ Drivers for switches and pushbuttons are provided. Switch and button drivers support debouncing. The switch driver provides for running a callback or launching a coroutine (coro) on contact closure and/or opening. The pushbutton -driver extends this to support long-press and double-click events. +driver extends this to support long-press and double-click events. The drivers +now support an optional event driven interface as a more flexible alternative +to callbacks. An `Encoder` class is provided to support rotary control knobs based on quadrature encoder switches. This is not intended for high throughput encoders @@ -20,6 +22,7 @@ goes outside defined bounds. 2. [Installation and usage](./DRIVERS.md#2-installation-and-usage) 3. [Interfacing switches](./DRIVERS.md#3-interfacing-switches) Switch debouncer with callbacks. 3.1 [Switch class](./DRIVERS.md#31-switch-class) + 3.2 [Event interface](./DRIVERS.md#32-event-interface) 4. [Interfacing pushbuttons](./DRIVERS.md#4-interfacing-pushbuttons) Extends Switch for long and double-click events 4.1 [Pushbutton class](./DRIVERS.md#41-pushbutton-class)      4.1.1 [The suppress constructor argument](./DRIVERS.md#411-the-suppress-constructor-argument) @@ -32,12 +35,8 @@ goes outside defined bounds. 6.1 [Encoder class](./DRIVERS.md#61-encoder-class) 7. [Additional functions](./DRIVERS.md#7-additional-functions) 7.1 [launch](./DRIVERS.md#71-launch) Run a coro or callback interchangeably - 7.2 [set_global_exception](./DRIVERS.md#72-set_global_exception) Simplify debugging with a global exception handler - 8. [Advanced use of callbacks](./DRIVERS.md#8-advanced-use-of-callbacks) - 8.1 [Retrieve result from synchronous function](./DRIVERS.md#81-retrieve-result-from-synchronous-function) - 8.2 [Cancel a task](./DRIVERS.md#82-cancel-a-task) - 8.3 [Retrieve result from a task](./DRIVERS.md#83-retrieve-result-from-a-task) - 8.4 [A complete example](./DRIVERS.md#84-a-complete-example) + 7.2 [set_global_exception](./DRIVERS.md#72-set_global_exception) Simplify debugging with a global exception handler. + 8. [Event based interface](./DRIVERS.md#8-event-based-interface) An alternative interface to Switch and Pushbutton objects. ###### [Tutorial](./TUTORIAL.md#contents) @@ -71,7 +70,9 @@ test() The `primitives.switch` module provides the `Switch` class. This supports debouncing a normally open switch connected between a pin and ground. Can run -callbacks or schedule coros on contact closure and/or opening. +callbacks or schedule coros on contact closure and/or opening. As an +alternative to a callback based interface, bound `Event` objects may be +triggered on switch state changes. In the following text the term `callable` implies a Python `callable`: namely a function, bound method, coroutine or bound coroutine. The term implies that any @@ -106,8 +107,6 @@ Methods: state of the switch i.e. 0 if grounded, 1 if connected to `3V3`. 4. `deinit` No args. Cancels the running task. -Methods 1 and 2 should be called before starting the scheduler. - Class attribute: 1. `debounce_ms` Debounce time in ms. Default 50. @@ -123,16 +122,25 @@ async def pulse(led, ms): led.off() async def my_app(): + pin = Pin('X1', Pin.IN, Pin.PULL_UP) # Hardware: switch to gnd + red = LED(1) + sw = Switch(pin) + sw.close_func(pulse, (red, 1000)) # Note how coro and args are passed await asyncio.sleep(60) # Dummy application code -pin = Pin('X1', Pin.IN, Pin.PULL_UP) # Hardware: switch to gnd -red = LED(1) -sw = Switch(pin) -sw.close_func(pulse, (red, 1000)) # Note how coro and args are passed asyncio.run(my_app()) # Run main application code ``` -See [Advanced use of callbacks](./DRIVERS.md#8-advanced-use-of-callbacks) for -ways to retrieve a result from a callback and to cancel a task. + +## 3.2 Event interface + +This enables a task to wait on a switch state as represented by a bound `Event` +instance. A bound contact closure `Event` is created by passing `None` to +`.close_func`, in which case the `Event` is named `.close`. Likewise a `.open` +`Event` is created by passing `None` to `open_func`. + +This is discussed further in +[Event based interface](./DRIVERS.md#8-event-based-interface) which includes a +code example. This API is recommended for new projects. ###### [Contents](./DRIVERS.md#1-contents) @@ -151,6 +159,11 @@ monitoring these events over time. Instances of this class can run a `callable` on on press, release, double-click or long press events. +As an alternative to callbacks bound `Event` instances may be created which are +triggered by press, release, double-click or long press events. This mode of +operation is more flexible than the use of callbacks and is covered in +[Event based interface](./DRIVERS.md#8-event-based-interface). + ## 4.1 Pushbutton class This can support normally open or normally closed switches, connected to `gnd` @@ -167,7 +180,7 @@ implementation. click or long press events; where the `callable` is a coroutine it will be converted to a `Task` and will run asynchronously. -Please see the note on timing in section 3. +Please see the note on timing in [section 3](./DRIVERS.md#3-interfacing-switches). Constructor arguments: @@ -194,7 +207,8 @@ Methods: 7. `deinit` No args. Cancels the running task. Methods 1 - 4 may be called at any time. If `False` is passed for a callable, -any existing callback will be disabled. +any existing callback will be disabled. If `None` is passed, a bound `Event` is +created. See [Event based interface](./DRIVERS.md#8-event-based-interface). Class attributes: 1. `debounce_ms` Debounce time in ms. Default 50. @@ -221,7 +235,7 @@ async def my_app(): asyncio.run(my_app()) # Run main application code ``` -An alternative `Pushbutton` implementation is available +A `Pushbutton` subset is available [here](https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py): this implementation avoids the use of the `Delay_ms` class to minimise the number of coroutines. @@ -319,9 +333,6 @@ When the pin value changes, the new value is compared with `sense` to determine if the button is closed or open. This is to allow the designer to specify if the `closed` state of the button is active `high` or active `low`. -See [Advanced use of callbacks](./DRIVERS.md#8-advanced-use-of-callbacks) for -ways to retrieve a result from a callback and to cancel a task. - ## 4.2 ESP32Touch class This subclass of `Pushbutton` supports ESP32 touchpads providing a callback @@ -572,115 +583,56 @@ application stops allowing the traceback and other debug prints to be studied. ###### [Contents](./DRIVERS.md#1-contents) -# 8. Advanced use of callbacks +# 8. Event based interface -The `Switch` and `Pushbutton` classes respond to state changes by launching -callbacks. These which can be functions, methods or coroutines. The classes -provide no means of retrieving the result of a synchronous function, nor of -cancelling a coro. Further, after a coro is launched there is no means of -awaiting it and accessing its return value. This is by design, firstly to keep -the classes as minimal as possible and secondly because these issues are easily -overcome. +The `Switch` and `Pushbutton` classes offer a traditional callback-based +interface. While familiar, it has drawbacks and requires extra code to perform +tasks like retrieving the result of a callback or, where a task is launched, +cancelling that task. The reason for this API is historical; an efficient +`Event` class only materialised with `uasyncio` V3. The class ensures that a +task waiting on an `Event` consumes minimal processor time. -## 8.1 Retrieve result from synchronous function +It is suggested that this API is used in new projects. -The following is a way to run a synchronous function returning a value. In this -case `bar` is a synchronous function taking a numeric arg which is a button -reference: -```python -pb = Pushbutton(Pin(1, Pin.IN, Pin.PULL_UP)) -pb.press_func(run, (bar, 1)) +The event based interface to `Switch` and `Pushbutton` classes is engaged by +passing `None` to the methods used to register callbacks. This causes a bound +`Event` to be instantiated, which may be accessed by user code. -def run(func, button_no): - res = func(button_no) - # Do something that needs the result +The following shows the name of the bound `Event` created when `None` is passed +to a method: -def bar(n): # This is the function we want to run - return 42*n -``` - -## 8.2 Cancel a task +| Class | method | Event | +|:-----------|:-------------|:--------| +| Switch | close_func | close | +| Switch | open_func | open | +| Pushbutton | press_func | press | +| Pushbutton | release_func | release | +| Pushbutton | long_func | long | +| Pushbutton | double_func | double | -Assume a coroutine `foo` with a single arg. The coro is started by a button -press and may be cancelled by another task. We need to retrieve a reference to -the `foo` task and store it such that it is available to the cancelling code: -```python -pb = Pushbutton(Pin(1, Pin.IN, Pin.PULL_UP)) -pb.press_func(start, (foo, 1)) -tasks = {1: None} # Support for multiple buttons -def start(func, button_no): - tasks[button_no] = asyncio.create_task(func(button_no)) -``` -The cancelling code checks that the appropriate entry in `tasks` is not `None` -and cancels it. - -## 8.3 Retrieve result from a task - -In this case we need to await the `foo` task so `start` is a coroutine: -```python -pb = Pushbutton(Pin(1, Pin.IN, Pin.PULL_UP)) -pb.press_func(start, (foo, 1)) -async def start(func, button_no): - result = await func(button_no) - # Process result -``` - -## 8.4 A complete example - -In fragments 8.2 and 8.3, if the button is pressed again before `foo` has run -to completion, a second `foo` instance will be launched. This may be -undesirable. - -The following script is a complete example which can be run on a Pyboard (or -other target with changes to pin numbers). It illustrates - 1. Logic to ensure that only one `foo` task instance runs at a time. - 2. The `start` task retrieves the result from `foo`. - 3. The `foo` task may be cancelled by a button press. - 4. The `foo` task returns a meaningful value whether cancelled or run to - completion. - 5. Use of an `Event` to stop the script. - +Typical usage is as follows: ```python import uasyncio as asyncio -from primitives import Pushbutton -from machine import Pin -tasks = {1: None} # Allow extension to multiple buttons -complete = asyncio.Event() # Stop the demo on cancellation - -async def start(asfunc, button_no): - if tasks[button_no] is None: # Only one instance - tasks[button_no] = asyncio.create_task(asfunc(button_no)) - result = await tasks[button_no] - print("Result", result) - complete.set() - -async def foo(button_no): - n = 0 - try: - while n < 20: - print(f"Button {button_no} count {n}") - n += 1 - await asyncio.sleep(1) - except asyncio.CancelledError: - pass # Trap cancellation so that n is returned - return n - -def killer(button_no): - if tasks[button_no] is not None: - tasks[button_no].cancel() - tasks[button_no] = None # Allow to run again +from primitives import Switch +from pyb import Pin + +async def foo(evt): + while True: + evt.clear() # re-enable the event + await evt.wait() # minimal resources used while paused + print("Switch closed.") + # Omitted code runs each time the switch closes async def main(): - pb1 = Pushbutton(Pin("X1", Pin.IN, Pin.PULL_UP)) - pb2 = Pushbutton(Pin("X2", Pin.IN, Pin.PULL_UP)) - pb1.press_func(start, (foo, 1)) - pb2.press_func(killer, (1,)) - await complete.wait() + sw = Switch(Pin("X1", Pin.IN, Pin.PULL_UP)) + sw.close_func(None) # Use event based interface + await foo(sw.close) # Pass the bound event to foo -try: - asyncio.run(main()) -finally: - _ = asyncio.new_event_loop() +asyncio.run(main()) ``` +With appropriate code the behaviour of the callback based interface may be +replicated, but with added benefits. For example the omitted code in `foo` +could run a callback-style synchronous method, retrieving its value. +Alternatively the code could create a task which could be cancelled. ###### [Contents](./DRIVERS.md#1-contents) diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index 008a0c4..c6207f3 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -30,14 +30,21 @@ def __init__(self, pin, suppress=False, sense=None): self._run = asyncio.create_task(self.buttoncheck()) # Thread runs forever def press_func(self, func=False, args=()): - self._tf = func + if func is None: + self.press = asyncio.Event() + self._tf = self.press.set if func is None else func self._ta = args def release_func(self, func=False, args=()): - self._ff = func + if func is None: + self.release = asyncio.Event() + self._ff = self.release.set if func is None else func self._fa = args def double_func(self, func=False, args=()): + if func is None: + self.double = asyncio.Event() + func = self.double.set self._df = func self._da = args if func: # If double timer already in place, leave it @@ -47,6 +54,9 @@ def double_func(self, func=False, args=()): self._dd = False # Clearing down double func def long_func(self, func=False, args=()): + if func is None: + self.long = asyncio.Event() + func = self.long.set if func: if self._ld: self._ld.callback(func, args) diff --git a/v3/primitives/switch.py b/v3/primitives/switch.py index cb1b51c..1da2435 100644 --- a/v3/primitives/switch.py +++ b/v3/primitives/switch.py @@ -17,11 +17,15 @@ def __init__(self, pin): self._run = asyncio.create_task(self.switchcheck()) # Thread runs forever def open_func(self, func, args=()): - self._open_func = func + if func is None: + self.open = asyncio.Event() + self._open_func = self.open.set if func is None else func self._open_args = args def close_func(self, func, args=()): - self._close_func = func + if func is None: + self.close = asyncio.Event() + self._close_func = self.close.set if func is None else func self._close_args = args # Return current state of switch (0 = pressed) From b8f7fa26584fc0bac8838972b99f31307f4979a6 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 31 Jul 2022 11:56:14 +0100 Subject: [PATCH 151/305] TUTORIAL.md: Add undocumented features. --- v3/docs/TUTORIAL.md | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index b639af0..9161abe 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -74,6 +74,7 @@ REPL.      7.6.1 [WiFi issues](./TUTORIAL.md#761-wifi-issues) 7.7 [CPython compatibility and the event loop](./TUTORIAL.md#77-cpython-compatibility-and-the-event-loop) Compatibility with CPython 3.5+ 7.8 [Race conditions](./TUTORIAL.md#78-race-conditions) + 7.9 [Undocumented uasyncio features](./TUTORIAL.md#79-undocumented-uasyncio-features) 8. [Notes for beginners](./TUTORIAL.md#8-notes-for-beginners) 8.1 [Problem 1: event loops](./TUTORIAL.md#81-problem-1:-event-loops) 8.2 [Problem 2: blocking methods](./TUTORIAL.md#8-problem-2:-blocking-methods) @@ -2296,12 +2297,12 @@ See [aremote.py](../as_drivers/nec_ir/aremote.py) documented an infra red remote controller. The following notes are salient points regarding its `asyncio` usage. -A pin interrupt records the time of a state change (in μs) and sets an event, -passing the time when the first state change occurred. A task waits on the -event, yields for the duration of a data burst, then decodes the stored data -before calling a user-specified callback. +A pin interrupt records the time of a state change (in μs) and sends a +`Message`, passing the time when the first state change occurred. A task waits +on the `Message`, yields for the duration of a data burst, then decodes the +stored data before calling a user-specified callback. -Passing the time to the `Event` instance enables the task to compensate for +Passing the time to the `Message` instance enables the task to compensate for any `asyncio` latency when setting its delay period. ###### [Contents](./TUTORIAL.md#contents) @@ -2461,8 +2462,8 @@ The second approach to socket programming is to use nonblocking sockets. This adds complexity but is necessary in some applications, notably where connectivity is via WiFi (see below). -Support for TLS on nonblocking sockets is platform dependent. It works on ESP32 -and Pyboard D. It does not work on ESP8266. +Support for TLS on nonblocking sockets is platform dependent. It works on ESP32, +Pyboard D and ESP8266. The use of nonblocking sockets requires some attention to detail. If a nonblocking read is performed, because of server latency, there is no guarantee @@ -2517,7 +2518,7 @@ Event loop methods are supported in `uasyncio` and in CPython 3.8 but are deprecated. To quote from the official docs: Application developers should typically use the high-level asyncio functions, -such as asyncio.run(), and should rarely need to reference the loop object or +such as `asyncio.run()`, and should rarely need to reference the loop object or call its methods. This section is intended mostly for authors of lower-level code, libraries, and frameworks, who need finer control over the event loop behavior. [reference](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_event_loop). @@ -2553,6 +2554,18 @@ one is running, or to extend the timer to prolong the LED illumination. Alternatively a subsequent button press might be required to terminate the illumination. The "right" behaviour is application dependent. +## 7.9 Undocumented uasyncio features + +These may be subject to change. + +A `Task` instance has a `.done()` method that returns `True` if the task has +terminated (by running to completion, by throwing an exception or by being +cancelled). + +If a task has completed, a `.data` bound variable holds any result which was +returned by the task. If the task throws an exception or is cancelled `.data` +holds the exception (or `CancelledError`). + ###### [Contents](./TUTORIAL.md#contents) # 8 Notes for beginners From db9977d2f8ceb24bf56bbb73ad2bbee02ee5ca68 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 13 Aug 2022 15:14:18 +0100 Subject: [PATCH 152/305] Minor changes to switch and pushbutton drivers. --- v3/primitives/pushbutton.py | 109 +++++++++++++++++--------------- v3/primitives/tests/switches.py | 84 ++++++++++++++++++++++-- 2 files changed, 139 insertions(+), 54 deletions(-) diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index c6207f3..dff541f 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -6,17 +6,20 @@ import uasyncio as asyncio import utime as time from . import launch, Delay_ms + try: from machine import TouchPad except ImportError: pass + class Pushbutton: debounce_ms = 50 long_press_ms = 1000 double_click_ms = 400 + def __init__(self, pin, suppress=False, sense=None): - self.pin = pin # Initialise for input + self._pin = pin # Initialise for input self._supp = suppress self._dblpend = False # Doubleclick waiting for 2nd click self._dblran = False # Doubleclick executed user function @@ -25,10 +28,59 @@ def __init__(self, pin, suppress=False, sense=None): self._df = False self._ld = False # Delay_ms instance for long press self._dd = False # Ditto for doubleclick - self.sense = pin.value() if sense is None else sense # Convert from electrical to logical value - self.state = self.rawstate() # Initial state - self._run = asyncio.create_task(self.buttoncheck()) # Thread runs forever + # Convert from electrical to logical value + self._sense = pin.value() if sense is None else sense + self._state = self.rawstate() # Initial state + self._run = asyncio.create_task(self._go()) # Thread runs forever + + async def _go(self): + while True: + self._check(self.rawstate()) + # Ignore state changes until switch has settled. Also avoid hogging CPU. + # See https://github.com/peterhinch/micropython-async/issues/69 + await asyncio.sleep_ms(Pushbutton.debounce_ms) + + def _check(self, state): + if state == self._state: + return + # State has changed: act on it now. + self._state = state + if state: # Button pressed: launch pressed func + if self._tf: + launch(self._tf, self._ta) + if self._ld: # There's a long func: start long press delay + self._ld.trigger(Pushbutton.long_press_ms) + if self._df: + if self._dd(): # Second click: timer running + self._dd.stop() + self._dblpend = False + self._dblran = True # Prevent suppressed launch on release + launch(self._df, self._da) + else: + # First click: start doubleclick timer + self._dd.trigger(Pushbutton.double_click_ms) + self._dblpend = True # Prevent suppressed launch on release + else: # Button release. Is there a release func? + if self._ff: + if self._supp: + d = self._ld + # If long delay exists, is running and doubleclick status is OK + if not self._dblpend and not self._dblran: + if (d and d()) or not d: + launch(self._ff, self._fa) + else: + launch(self._ff, self._fa) + if self._ld: + self._ld.stop() # Avoid interpreting a second click as a long push + self._dblran = False + + def _ddto(self): # Doubleclick timeout: no doubleclick occurred + self._dblpend = False + if self._supp and not self._state: + if not self._ld or (self._ld and not self._ld()): + launch(self._ff, self._fa) + # ****** API ****** def press_func(self, func=False, args=()): if func is None: self.press = asyncio.Event() @@ -67,55 +119,11 @@ def long_func(self, func=False, args=()): # Current non-debounced logical button state: True == pressed def rawstate(self): - return bool(self.pin.value() ^ self.sense) + return bool(self._pin() ^ self._sense) # Current debounced state of button (True == pressed) def __call__(self): - return self.state - - def _ddto(self): # Doubleclick timeout: no doubleclick occurred - self._dblpend = False - if self._supp and not self.state: - if not self._ld or (self._ld and not self._ld()): - launch(self._ff, self._fa) - - async def buttoncheck(self): - while True: - state = self.rawstate() - # State has changed: act on it now. - if state != self.state: - self.state = state - if state: # Button pressed: launch pressed func - if self._tf: - launch(self._tf, self._ta) - if self._ld: # There's a long func: start long press delay - self._ld.trigger(Pushbutton.long_press_ms) - if self._df: - if self._dd(): # Second click: timer running - self._dd.stop() - self._dblpend = False - self._dblran = True # Prevent suppressed launch on release - launch(self._df, self._da) - else: - # First click: start doubleclick timer - self._dd.trigger(Pushbutton.double_click_ms) - self._dblpend = True # Prevent suppressed launch on release - else: # Button release. Is there a release func? - if self._ff: - if self._supp: - d = self._ld - # If long delay exists, is running and doubleclick status is OK - if not self._dblpend and not self._dblran: - if (d and d()) or not d: - launch(self._ff, self._fa) - else: - launch(self._ff, self._fa) - if self._ld: - self._ld.stop() # Avoid interpreting a second click as a long push - self._dblran = False - # Ignore state changes until switch has settled - # See https://github.com/peterhinch/micropython-async/issues/69 - await asyncio.sleep_ms(Pushbutton.debounce_ms) + return self._state def deinit(self): self._run.cancel() @@ -123,6 +131,7 @@ def deinit(self): class ESP32Touch(Pushbutton): thresh = (80 << 8) // 100 + @classmethod def threshold(cls, val): if not (isinstance(val, int) and 0 < val < 100): diff --git a/v3/primitives/tests/switches.py b/v3/primitives/tests/switches.py index 82e77c9..59fa779 100644 --- a/v3/primitives/tests/switches.py +++ b/v3/primitives/tests/switches.py @@ -21,12 +21,16 @@ ''' tests = ''' +\x1b[32m Available tests: -test_sw Switch test -test_swcb Switch with callback -test_btn Pushutton launching coros -test_btncb Pushbutton launching callbacks +test_sw Switch test. +test_swcb Switch with callback. +test_sw_event Switch with event. +test_btn Pushutton launching coros. +test_btncb Pushbutton launching callbacks. btn_dynamic Change coros launched at runtime. +btn_event Pushbutton event interface. +\x1b[39m ''' print(tests) @@ -36,6 +40,15 @@ async def pulse(led, ms): await asyncio.sleep_ms(ms) led.off() +# Pulse an LED when an event triggered +async def evt_pulse(event, led): + while True: + event.clear() + await event.wait() + led.on() + await asyncio.sleep_ms(500) + led.off() + # Toggle an LED (callback) def toggle(led): led.toggle() @@ -94,6 +107,35 @@ def test_swcb(): sw.open_func(toggle, (green,)) run(sw) +# Test for the Switch class (events) +async def do_sw_event(): + pin = Pin('X1', Pin.IN, Pin.PULL_UP) + sw = Switch(pin) + sw.open_func(None) + sw.close_func(None) + tasks = [] + for event, led in ((sw.close, 1), (sw.open, 2)): + tasks.append(asyncio.create_task(evt_pulse(event, LED(led)))) + await killer(sw) + for task in tasks: + task.cancel() + +def test_sw_event(): + s = ''' +close pulse red +open pulses green +''' + print('Test of switch triggering events.') + print(helptext) + print(s) + try: + asyncio.run(do_sw_event()) + except KeyboardInterrupt: + print('Interrupted') + finally: + asyncio.new_event_loop() + print(tests) + # Test for the Pushbutton class (coroutines) # Pass True to test suppress def test_btn(suppress=False, lf=True, df=True): @@ -181,3 +223,37 @@ def btn_dynamic(): setup(pb, red, green, yellow, None) pb.long_func(setup, (pb, blue, red, green, yellow, 2000)) run(pb) + +# Test for the Pushbutton class (events) +async def do_btn_event(): + pin = Pin('X1', Pin.IN, Pin.PULL_UP) + pb = Pushbutton(pin) + pb.press_func(None) + pb.release_func(None) + pb.double_func(None) + pb.long_func(None) + tasks = [] + for event, led in ((pb.press, 1), (pb.release, 2), (pb.double, 3), (pb.long, 4)): + tasks.append(asyncio.create_task(evt_pulse(event, LED(led)))) + await killer(pb) + for task in tasks: + task.cancel() + +def btn_event(): + s = ''' +press pulse red +release pulses green +double click pulses yellow +long press pulses blue +''' + print('Test of pushbutton triggering events.') + print(helptext) + print(s) + try: + asyncio.run(do_btn_event()) + except KeyboardInterrupt: + print('Interrupted') + finally: + asyncio.new_event_loop() + print(tests) + From d326236d98ef8b37b99eee4603b2bb2bbe41f410 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 6 Sep 2022 11:24:04 +0100 Subject: [PATCH 153/305] TUTORIAL.md, auart.py: Add timeout value. --- v3/as_demos/auart.py | 4 ++-- v3/docs/TUTORIAL.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/as_demos/auart.py b/v3/as_demos/auart.py index e3379d4..5e77123 100644 --- a/v3/as_demos/auart.py +++ b/v3/as_demos/auart.py @@ -1,11 +1,11 @@ # Test of uasyncio stream I/O using UART # Author: Peter Hinch -# Copyright Peter Hinch 2017-2020 Released under the MIT license +# Copyright Peter Hinch 2017-2022 Released under the MIT license # Link X1 and X2 to test. import uasyncio as asyncio from machine import UART -uart = UART(4, 9600) +uart = UART(4, 9600, timeout=0) async def sender(): swriter = asyncio.StreamWriter(uart, {}) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 9161abe..f74d729 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2053,7 +2053,7 @@ demonstrates concurrent I/O on one UART. To run, link Pyboard pins X1 and X2 ```python import uasyncio as asyncio from machine import UART -uart = UART(4, 9600) +uart = UART(4, 9600, timeout=0) # timeout=0 prevents blocking at low baudrates async def sender(): swriter = asyncio.StreamWriter(uart, {}) From 97569a3c3120702425dea3ab61baa8ba09585771 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 13 Sep 2022 09:28:14 +0100 Subject: [PATCH 154/305] primitives: Add wait_any primitive. --- v3/docs/TUTORIAL.md | 26 ++++++++++++++++++++++++-- v3/primitives/__init__.py | 14 ++++++++++++++ v3/primitives/delay_ms.py | 5 +++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index f74d729..b20bd3e 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -27,6 +27,7 @@ REPL. 3. [Synchronisation](./TUTORIAL.md#3-synchronisation) 3.1 [Lock](./TUTORIAL.md#31-lock) 3.2 [Event](./TUTORIAL.md#32-event) +      3.2.1 [Wait on multiple events](./TUTORIAL.md#321-wait_on_multiple_events) Pause until 1 of N events is set. 3.3 [Coordinating multiple tasks](./TUTORIAL.md#33-coordinating-multiple-tasks)      3.3.1 [gather](./TUTORIAL.md#331-gather)      3.3.2 [TaskGroups](./TUTORIAL.md#332-taskgroups) Not yet in official build. @@ -702,6 +703,26 @@ constant creation of tasks. Arguably the `Barrier` class is the best approach. `Event` methods must not be called from an interrupt service routine (ISR). The `Event` class is not thread safe. See [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag). +### 3.2.1 Wait on multiple events + +The `wait_any` primitive allows a task to wait on a list of events. When one +of the events is triggered, the task continues. It is effectively a logical +`or` of events. +```python +from primitives import wait_any +evt1 = Event() +evt2 = Event() +# Launch tasks that might trigger these events +evt = await wait_any((evt1, evt2)) +# One or other was triggered +if evt == evt1: + evt1.clear() + # evt1 was triggered +else: + evt2.clear() + # evt2 was triggered +``` + ###### [Contents](./TUTORIAL.md#contents) ## 3.3 Coordinating multiple tasks @@ -1204,7 +1225,8 @@ Synchronous methods: hard or soft ISR. It is now valid for `duration` to be less than the current time outstanding. 2. `stop` No argument. Cancels the timeout, setting the `running` status - `False`. The timer can be restarted by issuing `trigger` again. + `False`. The timer can be restarted by issuing `trigger` again. Also clears + the `Event` described in `wait` below. 3. `running` No argument. Returns the running status of the object. 4. `__call__` Alias for running. 5. `rvalue` No argument. If a timeout has occurred and a callback has run, @@ -1213,7 +1235,7 @@ Synchronous methods: 6. `callback` args `func=None`, `args=()`. Allows the callable and its args to be assigned, reassigned or disabled at run time. 7. `deinit` No args. Cancels the running task. See [Object scope](./TUTORIAL.md#44-object-scope). - 8. `clear` No args. Clears the `Event` decribed in `wait` below. + 8. `clear` No args. Clears the `Event` described in `wait` below. Asynchronous method: 1. `wait` One or more tasks may wait on a `Delay_ms` instance. Pause until the diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index 2a9bca2..e1aa278 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -30,6 +30,20 @@ def _handle_exception(loop, context): loop = asyncio.get_event_loop() loop.set_exception_handler(_handle_exception) +async def wait_any(events): + evt = asyncio.Event() + trig_event = None + async def wt(event): + nonlocal trig_event + await event.wait() + evt.set() + trig_event = event + tasks = [asyncio.create_task(wt(event)) for event in events] + await evt.wait() + for task in tasks: + task.cancel() + return trig_event + _attrs = { "AADC": "aadc", "Barrier": "barrier", diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py index d5306ba..ac36235 100644 --- a/v3/primitives/delay_ms.py +++ b/v3/primitives/delay_ms.py @@ -1,8 +1,8 @@ # delay_ms.py Now uses ThreadSafeFlag and has extra .wait() API # Usage: -# from primitives.delay_ms import Delay_ms +# from primitives import Delay_ms -# Copyright (c) 2018-2021 Peter Hinch +# Copyright (c) 2018-2022 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file import uasyncio as asyncio @@ -59,6 +59,7 @@ def stop(self): self._ttask.cancel() self._ttask = self._fake self._busy = False + self._tout.clear() def __call__(self): # Current running status return self._busy From 23153313774eb12f66772ae7a785c8d423407d5f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 13 Sep 2022 09:41:53 +0100 Subject: [PATCH 155/305] primitives: Add wait_any primitive. --- v3/docs/TUTORIAL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index b20bd3e..8397b03 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -27,7 +27,7 @@ REPL. 3. [Synchronisation](./TUTORIAL.md#3-synchronisation) 3.1 [Lock](./TUTORIAL.md#31-lock) 3.2 [Event](./TUTORIAL.md#32-event) -      3.2.1 [Wait on multiple events](./TUTORIAL.md#321-wait_on_multiple_events) Pause until 1 of N events is set. +      3.2.1 [Wait on multiple events](./TUTORIAL.md#321-wait-on-multiple-events) Pause until 1 of N events is set. 3.3 [Coordinating multiple tasks](./TUTORIAL.md#33-coordinating-multiple-tasks)      3.3.1 [gather](./TUTORIAL.md#331-gather)      3.3.2 [TaskGroups](./TUTORIAL.md#332-taskgroups) Not yet in official build. From bdd98cd311d5d70fde9abeb247d850d98edb506b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 16 Sep 2022 14:21:00 +0100 Subject: [PATCH 156/305] Add wait_all, ESwitch and Ebutton. --- v3/docs/TUTORIAL.md | 18 ++- v3/primitives/__init__.py | 20 +-- v3/primitives/events.py | 154 ++++++++++++++++++++++ v3/primitives/tests/event_test.py | 204 ++++++++++++++++++++++++++++++ 4 files changed, 378 insertions(+), 18 deletions(-) create mode 100644 v3/primitives/events.py create mode 100644 v3/primitives/tests/event_test.py diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 8397b03..a748a24 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -705,15 +705,15 @@ constant creation of tasks. Arguably the `Barrier` class is the best approach. ### 3.2.1 Wait on multiple events -The `wait_any` primitive allows a task to wait on a list of events. When one +The `WaitAny` primitive allows a task to wait on a list of events. When one of the events is triggered, the task continues. It is effectively a logical `or` of events. ```python -from primitives import wait_any +from primitives import WaitAny evt1 = Event() evt2 = Event() # Launch tasks that might trigger these events -evt = await wait_any((evt1, evt2)) +evt = await WaitAny((evt1, evt2)) # One or other was triggered if evt == evt1: evt1.clear() @@ -722,6 +722,18 @@ else: evt2.clear() # evt2 was triggered ``` +The `WaitAll` primitive is similar except that the calling task will pause +until all passed `Event`s have been set: +```python +from primitives import WaitAll +evt1 = Event() +evt2 = Event() +wa = WaitAll((evt1, evt2)) # +# Launch tasks that might trigger these events +await wa +# Both were triggered +``` +Awaiting `WaitAll` or `WaitAny` may be cancelled or subject to a timeout. ###### [Contents](./TUTORIAL.md#contents) diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index e1aa278..fa6b163 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -1,6 +1,6 @@ # __init__.py Common functions for uasyncio primitives -# Copyright (c) 2018-2020 Peter Hinch +# Copyright (c) 2018-2022 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file try: @@ -30,20 +30,6 @@ def _handle_exception(loop, context): loop = asyncio.get_event_loop() loop.set_exception_handler(_handle_exception) -async def wait_any(events): - evt = asyncio.Event() - trig_event = None - async def wt(event): - nonlocal trig_event - await event.wait() - evt.set() - trig_event = event - tasks = [asyncio.create_task(wt(event)) for event in events] - await evt.wait() - for task in tasks: - task.cancel() - return trig_event - _attrs = { "AADC": "aadc", "Barrier": "barrier", @@ -57,6 +43,10 @@ async def wt(event): "Semaphore": "semaphore", "BoundedSemaphore": "semaphore", "Switch": "switch", + "WaitAll": "events", + "WaitAny": "events", + "ESwitch": "events", + "EButton": "events", } # Copied from uasyncio.__init__.py diff --git a/v3/primitives/events.py b/v3/primitives/events.py new file mode 100644 index 0000000..b3e9a4c --- /dev/null +++ b/v3/primitives/events.py @@ -0,0 +1,154 @@ +# events.py Event based primitives + +# Copyright (c) 2022 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +from . import Delay_ms + +# An Event-like class that can wait on an iterable of Event instances. +# .wait pauses until any passed event is set. +class WaitAny: + def __init__(self, events): + self.events = events + self.trig_event = None + self.evt = asyncio.Event() + + async def wait(self): + tasks = [asyncio.create_task(self.wt(event)) for event in self.events] + try: + await self.evt.wait() + finally: + self.evt.clear() + for task in tasks: + task.cancel() + return self.trig_event + + async def wt(self, event): + await event.wait() + self.evt.set() + self.trig_event = event + + def event(self): + return self.trig_event + +# An Event-like class that can wait on an iterable of Event instances, +# .wait pauses until all passed events have been set. +class WaitAll: + def __init__(self, events): + self.events = events + + async def wait(self): + async def wt(event): + await event.wait() + tasks = (asyncio.create_task(wt(event)) for event in self.events) + try: + await asyncio.gather(*tasks) + finally: # May be subject to timeout or cancellation + for task in tasks: + task.cancel() + +# Minimal switch class having an Event based interface +class ESwitch: + debounce_ms = 50 + + def __init__(self, pin, lopen=1): # Default is n/o switch returned to gnd + self._pin = pin # Should be initialised for input with pullup + self._lopen = lopen # Logic level in "open" state + self.open = asyncio.Event() + self.close = asyncio.Event() + self._state = self._pin() ^ self._lopen # Get initial state + asyncio.create_task(self._poll(ESwitch.debounce_ms)) + + async def _poll(self, dt): # Poll the button + while True: + if (s := self._pin() ^ self._lopen) != self._state: + self._state = s + self._of() if s else self._cf() + await asyncio.sleep_ms(dt) # Wait out bounce + + def _of(self): + self.open.set() + + def _cf(self): + self.close.set() + + # ***** API ***** + # Return current state of switch (0 = pressed) + def __call__(self): + return self._state + + def deinit(self): + self._poll.cancel() + +# Minimal pushbutton class having an Event based interface +class EButton: + debounce_ms = 50 # Attributes can be varied by user + long_press_ms = 1000 + double_click_ms = 400 + + def __init__(self, pin, suppress=False, sense=None): + self._pin = pin # Initialise for input + self._supp = suppress + self._sense = pin() if sense is None else sense + self._state = self.rawstate() # Initial logical state + self._ltim = Delay_ms(duration = EButton.long_press_ms) + self._dtim = Delay_ms(duration = EButton.double_click_ms) + self.press = asyncio.Event() # *** API *** + self.double = asyncio.Event() + self.long = asyncio.Event() + self.release = asyncio.Event() # *** END API *** + self._tasks = [asyncio.create_task(self._poll(EButton.debounce_ms))] # Tasks run forever. Poll contacts + self._tasks.append(asyncio.create_task(self._ltf())) # Handle long press + if suppress: + self._tasks.append(asyncio.create_task(self._dtf())) # Double timer + + async def _poll(self, dt): # Poll the button + while True: + if (s := self.rawstate()) != self._state: + self._state = s + self._pf() if s else self._rf() + await asyncio.sleep_ms(dt) # Wait out bounce + + def _pf(self): # Button press + if not self._supp: + self.press.set() # User event + if not self._ltim(): # Don't retrigger long timer if already running + self._ltim.trigger() + if self._dtim(): # Press occurred while _dtim is running + self.double.set() # User event + self._dtim.stop() # _dtim's Event is only used if suppress + else: + self._dtim.trigger() + + def _rf(self): # Button release + self._ltim.stop() + if not self._supp or not self._dtim(): # If dtim running postpone release otherwise it + self.release.set() # is set before press + + async def _ltf(self): # Long timeout + while True: + await self._ltim.wait() + self._ltim.clear() # Clear the event + self.long.set() # User event + + async def _dtf(self): # Double timeout (runs if suppress is set) + while True: + await self._dtim.wait() + self._dtim.clear() # Clear the event + if not self._ltim(): # Button was released + self.press.set() # User events + self.release.set() + + # ****** API ****** + # Current non-debounced logical button state: True == pressed + def rawstate(self): + return bool(self._pin() ^ self._sense) + + # Current debounced state of button (True == pressed) + def __call__(self): + return self._state + + def deinit(self): + for task in self._tasks: + task.cancel() diff --git a/v3/primitives/tests/event_test.py b/v3/primitives/tests/event_test.py new file mode 100644 index 0000000..8a2aca5 --- /dev/null +++ b/v3/primitives/tests/event_test.py @@ -0,0 +1,204 @@ +# event_test.py Test WaitAll, WaitAny, ESwwitch, EButton + +# Copyright (c) 2022 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +# from primitives.tests.event_test import * + +import uasyncio as asyncio +from primitives import Delay_ms, WaitAny, ESwitch, WaitAll, EButton +from pyb import Pin + +events = [asyncio.Event() for _ in range(4)] + +async def set_events(*ev): + for n in ev: + await asyncio.sleep(1) + print("Setting", n) + events[n].set() + +def clear(msg): + print(msg) + for e in events: + e.clear() + +async def can(obj, tim): + await asyncio.sleep(tim) + print("About to cancel") + obj.cancel() + +async def foo(tsk): + print("Waiting") + await tsk + +async def wait_test(): + msg = """ +\x1b[32m +Expected output: +Setting 0 +Tested WaitAny 0 +Setting 1 +Tested WaitAny 1 +Setting 2 +Setting 3 +Tested WaitAll 2, 3 +Setting 0 +Setting 3 +Tested WaitAny 0, 3 +Cancel in 3s +Setting 0 +Setting 1 +About to cancel +Cancelled. +Waiting for 4s +Timeout +done +\x1b[39m +""" + print(msg) + wa = WaitAny((events[0], events[1], WaitAll((events[2], events[3])))) + asyncio.create_task(set_events(0)) + await wa.wait() + clear("Tested WaitAny 0") + asyncio.create_task(set_events(1)) + await wa.wait() + clear("Tested WaitAny 1") + asyncio.create_task(set_events(2, 3)) + await wa.wait() + clear("Tested WaitAll 2, 3") + wa = WaitAll((WaitAny((events[0], events[1])), WaitAny((events[2], events[3])))) + asyncio.create_task(set_events(0, 3)) + await wa.wait() + clear("Tested WaitAny 0, 3") + task = asyncio.create_task(wa.wait()) + asyncio.create_task(set_events(0, 1)) # Does nothing + asyncio.create_task(can(task, 3)) + print("Cancel in 3s") + try: + await task + except asyncio.CancelledError: # TODO why must we trap this? + print("Cancelled.") + print("Waiting for 4s") + try: + await asyncio.wait_for(wa.wait(), 4) + except asyncio.TimeoutError: + print("Timeout") + print("done") + +val = 0 +fail = False +pout = None +polarity = 0 + +async def monitor(evt, v, verbose): + global val + while True: + await evt.wait() + evt.clear() + val += v + verbose and print("Got", hex(v), hex(val)) + +async def pulse(ms=100): + pout(1 ^ polarity) + await asyncio.sleep_ms(ms) + pout(polarity) + +def expect(v, e): + global fail + if v == e: + print("Pass") + else: + print(f"Fail: expected {e} got {v}") + fail = True + +async def btest(btn, verbose, supp): + global val, fail + val = 0 + events = btn.press, btn.release, btn.double, btn.long + tasks = [] + for n, evt in enumerate(events): + tasks.append(asyncio.create_task(monitor(evt, 1 << 3 * n, verbose))) + await asyncio.sleep(1) + print("Start short press test") + await pulse() + await asyncio.sleep(1) + verbose and print("Test of short press", hex(val)) + expect(val, 0x09) + val = 0 + await asyncio.sleep(1) + print("Start long press test") + await pulse(2000) + await asyncio.sleep(4) + verbose and print("Long press", hex(val)) + exp = 0x208 if supp else 0x209 + expect(val, exp) + val = 0 + await asyncio.sleep(1) + print("Start double press test") + await pulse() + await asyncio.sleep_ms(100) + await pulse() + await asyncio.sleep(4) + verbose and print("Double press", hex(val)) + exp = 0x48 if supp else 0x52 + expect(val, exp) + for task in tasks: + task.cancel() + +async def stest(sw, verbose): + global val, fail + val = 0 + events = sw.open, sw.close + tasks = [] + for n, evt in enumerate(events): + tasks.append(asyncio.create_task(monitor(evt, 1 << 3 * n, verbose))) + await pulse(1000) + await asyncio.sleep(4) # Wait for any spurious events + verbose and print("Switch close and open", hex(val)) + expect(val, 0x09) + for task in tasks: + task.cancel() + +async def switch_test(pol, verbose): + global val, pout, polarity + polarity = pol + pin = Pin('Y1', Pin.IN) + pout = Pin('Y2', Pin.OUT, value=pol) + print("Testing EButton.") + print("suppress == False") + btn = EButton(pin) + await btest(btn, verbose, False) + print("suppress == True") + btn = EButton(pin, suppress=True) + await btest(btn, verbose, True) + print("Testing ESwitch") + sw = ESwitch(pin, pol) + await stest(sw, verbose) + print("Failures occurred.") if fail else print("All tests passed.") + +def tests(): + txt=""" + \x1b[32m + Available tests: + 1. test_switches(polarity=1, verbose=False) Test the ESwitch and Ebutton classe. + 2. test_wait() Test the WaitAny and WaitAll primitives. + + Switch tests assume a Pyboard with a link between Y1 and Y2. + \x1b[39m + """ + print(txt) + +tests() +def test_switches(polarity=1, verbose=False): + try: + asyncio.run(switch_test(polarity, verbose)) # polarity 1/0 is normal (off) electrical state. + finally: + asyncio.new_event_loop() + tests() + +def test_wait(): + try: + asyncio.run(wait_test()) + finally: + asyncio.new_event_loop() + tests() From c26efb26b637eb0f3428940bb4aa15d9efe4e04a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 18 Sep 2022 12:18:20 +0100 Subject: [PATCH 157/305] TUTORIAL.md: Minor fixes. --- v3/docs/TUTORIAL.md | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index a748a24..a87882a 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -10,7 +10,7 @@ REPL. # Contents 0. [Introduction](./TUTORIAL.md#0-introduction) - 0.1 [Installing uasyncio on bare metal](./TUTORIAL.md#01-installing-uasyncio-on-bare-metal) + 0.1 [Installing uasyncio](./TUTORIAL.md#01-installing-uasyncio) 1. [Cooperative scheduling](./TUTORIAL.md#1-cooperative-scheduling) 1.1 [Modules](./TUTORIAL.md#11-modules)      1.1.1 [Primitives](./TUTORIAL.md#111-primitives) @@ -92,7 +92,7 @@ source of confusion. Most of this document assumes some familiarity with asynchronous programming. For those new to it an introduction may be found -[in section 7](./TUTORIAL.md#8-notes-for-beginners). +[in section 8](./TUTORIAL.md#8-notes-for-beginners). The MicroPython `uasyncio` library comprises a subset of Python's `asyncio` library. It is designed for use on microcontrollers. As such it has a small RAM @@ -105,7 +105,7 @@ responsive to events such as user interaction. Another major application area for asyncio is in network programming: many guides to this may be found online. -Note that MicroPython is based on Python 3.4 with minimal Python 3.5 additions. +Note that MicroPython is based on Python 3.4 with additions from later versions. This version of `uasyncio` supports a subset of CPython 3.8 `asyncio`. This document identifies supported features. Except where stated program samples run under MicroPython and CPython 3.8. @@ -113,16 +113,16 @@ under MicroPython and CPython 3.8. This tutorial aims to present a consistent programming style compatible with CPython V3.8 and above. -## 0.1 Installing uasyncio on bare metal +## 0.1 Installing uasyncio -No installation is necessary if a daily build of firmware is installed or -release build V1.13 or later. The version may be checked by issuing at -the REPL: +Firmware builds after V1.13 incorporate `uasyncio`. The version may be checked +by issuing at the REPL: ```python import uasyncio print(uasyncio.__version__) ``` -Version 3 will print a version number. Older versions will throw an exception. +Version 3 will print a version number. Older versions will throw an exception: +installing updated firmware is highly recommended. ###### [Main README](../README.md) @@ -143,11 +143,12 @@ The directory `primitives` contains a Python package containing the following: * Additional Python primitives including an ISR-compatible version of `Event` and a software retriggerable delay class. * Primitives for interfacing hardware. These comprise classes for debouncing - switches and pushbuttonsand an asynchronous ADC class. These are documented + switches and pushbuttons and an asynchronous ADC class. These are documented [here](./DRIVERS.md). To install this Python package copy the `primitives` directory tree and its -contents to your hardware's filesystem. +contents to your hardware's filesystem. There is no need to copy the `tests` +subdirectory. ### 1.1.2 Demo programs @@ -545,8 +546,8 @@ A further set of primitives for synchronising hardware are detailed in To install the primitives, copy the `primitives` directory and contents to the target. A primitive is loaded by issuing (for example): ```python -from primitives.semaphore import Semaphore, BoundedSemaphore -from primitives.queue import Queue +from primitives import Semaphore, BoundedSemaphore +from primitives import Queue ``` When `uasyncio` acquires official versions of the CPython primitives the invocation lines alone should be changed. e.g. : @@ -715,7 +716,7 @@ evt2 = Event() # Launch tasks that might trigger these events evt = await WaitAny((evt1, evt2)) # One or other was triggered -if evt == evt1: +if evt is evt1: evt1.clear() # evt1 was triggered else: @@ -909,7 +910,7 @@ following illustrates tasks accessing a resource one at a time: ```python import uasyncio as asyncio -from primitives.semaphore import Semaphore +from primitives import Semaphore async def foo(n, sema): print('foo {} waiting for semaphore'.format(n)) @@ -973,7 +974,7 @@ Asynchronous methods: ```python import uasyncio as asyncio -from primitives.queue import Queue +from primitives import Queue async def slow_process(): await asyncio.sleep(2) @@ -1132,7 +1133,7 @@ run at different speeds. The `Barrier` synchronises these loops. This can run on a Pyboard. ```python import uasyncio as asyncio -from primitives.barrier import Barrier +from primitives import Barrier from machine import UART import ujson @@ -1259,7 +1260,7 @@ running. One second after the triggering ceases, the callback runs. ```python import uasyncio as asyncio -from primitives.delay_ms import Delay_ms +from primitives import Delay_ms async def my_app(): d = Delay_ms(callback, ('Callback running',)) @@ -1283,7 +1284,7 @@ This example illustrates multiple tasks waiting on a `Delay_ms`. No callback is used. ```python import uasyncio as asyncio -from primitives.delay_ms import Delay_ms +from primitives import Delay_ms async def foo(n, d): await d.wait() @@ -1334,7 +1335,7 @@ using it: ```python import uasyncio as asyncio -from primitives.message import Message +from primitives import Message async def waiter(msg): print('Waiting for message') @@ -1373,7 +1374,7 @@ Asynchronous Method: The following example shows multiple tasks awaiting a `Message`. ```python -from primitives.message import Message +from primitives import Message import uasyncio as asyncio async def bar(msg, n): From 92804fefb0367b9d08bfe4e732c4ebab3ee21dd8 Mon Sep 17 00:00:00 2001 From: Nick Lamprianidis Date: Tue, 20 Sep 2022 20:27:54 +0200 Subject: [PATCH 158/305] TUTORIAL.md: Minor fixes. --- v3/docs/TUTORIAL.md | 146 ++++++++++++++++++++++---------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index a87882a..ca66357 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -204,7 +204,7 @@ target. They have their own documentation as follows: # 2. uasyncio The asyncio concept is of cooperative multi-tasking based on coroutines -(coros). A coro is similar to a function, but is intended to run concurrently +(coros). A coro is similar to a function but is intended to run concurrently with other coros. The illusion of concurrency is achieved by periodically yielding to the scheduler, enabling other coros to be scheduled. @@ -225,10 +225,10 @@ asyncio.run(bar()) ``` Program execution proceeds normally until the call to `asyncio.run(bar())`. At -this point execution is controlled by the scheduler. A line after +this point, execution is controlled by the scheduler. A line after `asyncio.run(bar())` would never be executed. The scheduler runs `bar` because this has been placed on the scheduler's queue by `asyncio.run(bar())`. -In this trivial example there is only one task: `bar`. If there were others, +In this trivial example, there is only one task: `bar`. If there were others, the scheduler would schedule them in periods when `bar` was paused: ```python @@ -272,9 +272,9 @@ async def bar(): V3 `uasyncio` introduced the concept of a `Task`. A `Task` instance is created from a coro by means of the `create_task` method, which causes the coro to be -scheduled for execution and returns a `Task` instance. In many cases coros and +scheduled for execution and returns a `Task` instance. In many cases, coros and tasks are interchangeable: the official docs refer to them as `awaitable`, for -the reason that either may be the target of an `await`. Consider this: +the reason that either of them may be the target of an `await`. Consider this: ```python import uasyncio as asyncio @@ -294,7 +294,7 @@ asyncio.run(main()) ``` There is a crucial difference between `create_task` and `await`: the former is synchronous code and returns immediately, with the passed coro being -converted to a `Task` and queued to run "in the background". By contrast +converted to a `Task` and queued to run "in the background". By contrast, `await` causes the passed `Task` or coro to run to completion before the next line executes. Consider these lines of code: @@ -304,7 +304,7 @@ await asyncio.sleep(0) ``` The first causes the code to pause for the duration of the delay, with other -tasks being scheduled for the duration. A delay of 0 causes any pending tasks +tasks being scheduled for this duration. A delay of 0 causes any pending tasks to be scheduled in round-robin fashion before the following line is run. See the `roundrobin.py` example. @@ -314,8 +314,8 @@ checking or cancellation. In the following code sample three `Task` instances are created and scheduled for execution. The "Tasks are running" message is immediately printed. The -three instances of the task `bar` appear to run concurrently: in fact when one -pauses, the scheduler grants execution to the next giving the illusion of +three instances of the task `bar` appear to run concurrently. In fact, when one +pauses, the scheduler grants execution to the next, giving the illusion of concurrency: ```python @@ -347,13 +347,13 @@ asyncio.run(main()) * `asyncio.run` Arg: the coro to run. Return value: any value returned by the passed coro. The scheduler queues the passed coro to run ASAP. The coro arg is specified with function call syntax with any required arguments passed. In the - current version the `run` call returns when the task terminates. However under - CPython the `run` call does not terminate. + current version the `run` call returns when the task terminates. However, under + CPython, the `run` call does not terminate. * `await` Arg: the task or coro to run. If a coro is passed it must be specified with function call syntax. Starts the task ASAP. The awaiting task blocks until the awaited one has run to completion. As described - [in section 2.2](./TUTORIAL.md#22-coroutines-and-tasks) it is possible to - `await` a task which has already been started. In this instance the `await` is + [in section 2.2](./TUTORIAL.md#22-coroutines-and-tasks), it is possible to + `await` a task which has already been started. In this instance, the `await` is on the `task` object (function call syntax is not used). The above are compatible with CPython 3.8 or above. @@ -439,13 +439,13 @@ Most firmware applications run forever. This requires the coro passed to To ease debugging, and for CPython compatibility, some "boilerplate" code is suggested in the sample below. -By default an exception in a task will not stop the application as a whole from +By default, an exception in a task will not stop the application as a whole from running. This can make debugging difficult. The fix shown below is discussed [in 5.1.1](./TUTORIAL.md#511-global-exception-handler). It is bad practice to create a task prior to issuing `asyncio.run()`. CPython will throw an exception in this case. MicroPython -[does not](https://github.com/micropython/micropython/issues/6174) but it's +[does not](https://github.com/micropython/micropython/issues/6174), but it's wise to avoid doing this. Lastly, `uasyncio` retains state. This means that, by default, you need to @@ -517,10 +517,10 @@ producer generates data which the consumer uses. Asyncio provides the `Queue` object. The producer puts data onto the queue while the consumer waits for its arrival (with other tasks getting scheduled for the duration). The `Queue` guarantees that items are removed in the order in which they were received. -Alternatively a `Barrier` instance can be used if the producer must wait +Alternatively, a `Barrier` instance can be used if the producer must wait until the consumer is ready to access the data. -In simple applications communication may be achieved with global flags or bound +In simple applications, communication may be achieved with global flags or bound variables. A more elegant approach is to use synchronisation primitives. CPython provides the following classes: * `Lock` - already incorporated in new `uasyncio`. @@ -549,8 +549,8 @@ target. A primitive is loaded by issuing (for example): from primitives import Semaphore, BoundedSemaphore from primitives import Queue ``` -When `uasyncio` acquires official versions of the CPython primitives the -invocation lines alone should be changed. e.g. : +When `uasyncio` acquires official versions of the CPython primitives, the +invocation lines alone should be changed. E.g.: ```python from uasyncio import Semaphore, BoundedSemaphore from uasyncio import Queue @@ -558,10 +558,10 @@ from uasyncio import Queue ##### Note on CPython compatibility CPython will throw a `RuntimeError` on first use of a synchronisation primitive -that was instantiated prior to starting the scheduler. By contrast +that was instantiated prior to starting the scheduler. By contrast, `MicroPython` allows instantiation in synchronous code executed before the scheduler is started. Early instantiation can be advantageous in low resource -environments. For example a class might have a large buffer and bound `Event` +environments. For example, a class might have a large buffer and bound `Event` instances. Such a class should be instantiated early, before RAM fragmentation sets in. @@ -633,7 +633,7 @@ asyncio.run(main()) # Run for 10s This describes the use of the official `Event` primitive. -This provides a way for one or more tasks to pause until another flags them to +This provides a way for one or more tasks to pause until another one flags them to continue. An `Event` object is instantiated and made accessible to all tasks using it: @@ -669,14 +669,14 @@ Asynchronous Method: * `wait` Pause until event is set. Tasks wait on the event by issuing `await event.wait()`; execution pauses until -another issues `event.set()`. This causes all tasks waiting on the `Event` to +another one issues `event.set()`. This causes all tasks waiting on the `Event` to be queued for execution. Note that the synchronous sequence ```python event.set() event.clear() ``` will cause any tasks waiting on the event to resume in round-robin order. In -general the waiting task should clear the event, as in the `waiter` example +general, the waiting task should clear the event, as in the `waiter` example above. This caters for the case where the waiting task has not reached the event at the time when it is triggered. In this instance, by the time the task reaches the event, the task will find it clear and will pause. This can lead to @@ -757,14 +757,14 @@ Its call signature is res = await asyncio.gather(*tasks, return_exceptions=True) ``` The keyword-only boolean arg `return_exceptions` determines the behaviour in -the event of a cancellation or timeout of tasks. If `False` the `gather` +the event of a cancellation or timeout of tasks. If `False`, the `gather` terminates immediately, raising the relevant exception which should be trapped -by the caller. If `True` the `gather` continues to pause until all have either -run to completion or been terminated by cancellation or timeout. In this case +by the caller. If `True`, the `gather` continues to pause until all have either +run to completion or been terminated by cancellation or timeout. In this case, tasks which have been terminated will return the exception object in the list of return values. -The following script may be used to demonstrate this behaviour +The following script may be used to demonstrate this behaviour: ```python try: @@ -821,7 +821,7 @@ The `TaskGroup` class is unofficially provided by suited to applications where one or more of a group of tasks is subject to runtime exceptions. A `TaskGroup` is instantiated in an asynchronous context manager. The `TaskGroup` instantiates member tasks. When all have run to -completion the context manager terminates. Return values from member tasks +completion, the context manager terminates. Return values from member tasks cannot be retrieved. Results should be passed in other ways such as via bound variables, queues etc. @@ -1106,10 +1106,10 @@ While similar in purpose to `gather` there are differences described below. Its principal purpose is to cause multiple coros to rendezvous at a particular point. For example producer and consumer coros can synchronise at a point where the producer has data available and the consumer is ready to use it. At that -point in time the `Barrier` can optionally run a callback before releasing the +point in time, the `Barrier` can optionally run a callback before releasing the barrier to allow all waiting coros to continue. -Secondly it can allow a task to pause until one or more other tasks have +Secondly, it can allow a task to pause until one or more other tasks have terminated or passed a particular point. For example an application might want to shut down various peripherals before starting a sleep period. The task wanting to sleep initiates several shut down tasks and waits until they have @@ -1118,7 +1118,7 @@ by `gather`. The key difference between `Barrier` and `gather` is symmetry: `gather` is asymmetrical. One task owns the `gather` and awaits completion of a set of -tasks. By contrast `Barrier` can be used symmetrically with member tasks +tasks. By contrast, `Barrier` can be used symmetrically with member tasks pausing until all have reached the barrier. This makes it suited for use in the `while True:` constructs common in firmware applications. Use of `gather` would imply instantiating a set of tasks on every pass of the loop. @@ -1128,7 +1128,7 @@ passing a barrier does not imply return. `Barrier` now has an efficient implementation using `Event` to suspend waiting tasks. The following is a typical usage example. A data provider acquires data from -some hardware and transmits it concurrently on a number of interefaces. These +some hardware and transmits it concurrently on a number of interfaces. These run at different speeds. The `Barrier` synchronises these loops. This can run on a Pyboard. ```python @@ -1227,7 +1227,7 @@ Constructor arguments (defaults in brackets): 1. `func` The `callable` to call on timeout (default `None`). 2. `args` A tuple of arguments for the `callable` (default `()`). 3. `can_alloc` Unused arg, retained to avoid breaking code. - 4. `duration` Integer, default 1000ms. The default timer period where no value + 4. `duration` Integer, default 1000 ms. The default timer period where no value is passed to the `trigger` method. Synchronous methods: @@ -1255,7 +1255,7 @@ Asynchronous method: delay instance has timed out. In this example a `Delay_ms` instance is created with the default duration of -1s. It is repeatedly triggered for 5 secs, preventing the callback from +1 sec. It is repeatedly triggered for 5 secs, preventing the callback from running. One second after the triggering ceases, the callback runs. ```python @@ -1329,7 +1329,7 @@ The `.set()` method can accept an optional data value of any type. The task waiting on the `Message` can retrieve it by means of `.value()` or by awaiting the `Message` as below. -Like `Event`, `Message` provides a way a task to pause until another flags it +Like `Event`, `Message` provides a way for a task to pause until another flags it to continue. A `Message` object is instantiated and made accessible to the task using it: @@ -1416,9 +1416,9 @@ The following hardware-related classes are documented [here](./DRIVERS.md): # 4 Designing classes for asyncio -In the context of device drivers the aim is to ensure nonblocking operation. +In the context of device drivers, the aim is to ensure nonblocking operation. The design should ensure that other tasks get scheduled in periods while the -driver is waiting for the hardware. For example a task awaiting data arriving +driver is waiting for the hardware. For example, a task awaiting data arriving on a UART or a user pressing a button should allow other tasks to be scheduled until the event occurs. @@ -1432,7 +1432,7 @@ defined: see [Portable code](./TUTORIAL.md#412-portable-code) for a way to write a portable class. This section describes a simpler MicroPython specific solution. -In the following code sample the `__iter__` special method runs for a period. +In the following code sample, the `__iter__` special method runs for a period. The calling coro blocks, but other coros continue to run. The key point is that `__iter__` uses `yield from` to yield execution to another coro, blocking until it has completed. @@ -1468,7 +1468,7 @@ async with awaitable as a: # Asynchronous CM (see below) # do something ``` -To achieve this the `__await__` generator should return `self`. This is passed +To achieve this, the `__await__` generator should return `self`. This is passed to any variable in an `as` clause and also enables the special methods to work. ###### [Contents](./TUTORIAL.md#contents) @@ -1534,7 +1534,7 @@ its `next` method. The class must conform to the following requirements: * It has an `__aiter__` method returning the asynchronous iterator. * It has an ` __anext__` method which is a task - i.e. defined with `async def` and containing at least one `await` statement. To stop - iteration it must raise a `StopAsyncIteration` exception. + the iteration, it must raise a `StopAsyncIteration` exception. Successive values are retrieved with `async for` as below: @@ -1593,7 +1593,7 @@ async def bar(lock): ``` As with normal context managers an exit method is guaranteed to be called when the context manager terminates, whether normally or via an exception. To -achieve this the special methods `__aenter__` and `__aexit__` must be +achieve this, the special methods `__aenter__` and `__aexit__` must be defined, both being tasks waiting on a task or `awaitable` object. This example comes from the `Lock` class: ```python @@ -1696,7 +1696,7 @@ async def main(): asyncio.run(main()) ``` If `main` issued `await foo()` rather than `create_task(foo())` the exception -would propagate to `main`. Being untrapped, the scheduler and hence the script +would propagate to `main`. Being untrapped, the scheduler, and hence the script, would stop. #### Warning @@ -1707,7 +1707,7 @@ queued for execution. ### 5.1.1 Global exception handler -During development it is often best if untrapped exceptions stop the program +During development, it is often best if untrapped exceptions stop the program rather than merely halting a single task. This can be achieved by setting a global exception handler. This debug aid is not CPython compatible: ```python @@ -1738,7 +1738,7 @@ asyncio.run(main()) ### 5.1.2 Keyboard interrupts There is a "gotcha" illustrated by the following code sample. If allowed to run -to completion it works as expected. +to completion, it works as expected. ```python import uasyncio as asyncio @@ -1771,9 +1771,9 @@ except KeyboardInterrupt: asyncio.run(shutdown()) ``` -However issuing a keyboard interrupt causes the exception to go to the +However, issuing a keyboard interrupt causes the exception to go to the outermost scope. This is because `uasyncio.sleep` causes execution to be -transferred to the scheduler. Consequently applications requiring cleanup code +transferred to the scheduler. Consequently, applications requiring cleanup code in response to a keyboard interrupt should trap the exception at the outermost scope. @@ -1794,9 +1794,9 @@ clause. The exception thrown to the task is `uasyncio.CancelledError` in both cancellation and timeout. There is no way for the task to distinguish between these two cases. -As stated above, for a task launched with `.create_task` trapping the error is +As stated above, for a task launched with `.create_task`, trapping the error is optional. Where a task is `await`ed, to avoid a halt it must be trapped within -the task, within the `await`ing scope, or both. In the last case the task must +the task, within the `await`ing scope, or both. In the last case, the task must re-raise the exception after trapping so that the error can again be trapped in the outer scope. @@ -1825,7 +1825,7 @@ async def bar(): asyncio.run(bar()) ``` -The exception may be trapped as follows +The exception may be trapped as follows: ```python import uasyncio as asyncio async def printit(): @@ -1850,7 +1850,7 @@ async def bar(): print('Task is now cancelled') asyncio.run(bar()) ``` -As of firmware V1.18 the `current_task()` method is supported. This enables a +As of firmware V1.18, the `current_task()` method is supported. This enables a task to pass itself to other tasks, enabling them to cancel it. It also facilitates the following pattern: @@ -1870,9 +1870,9 @@ class Foo: Timeouts are implemented by means of `uasyncio` methods `.wait_for()` and `.wait_for_ms()`. These take as arguments a task and a timeout in seconds or ms -respectively. If the timeout expires a `uasyncio.CancelledError` is thrown to +respectively. If the timeout expires, a `uasyncio.CancelledError` is thrown to the task, while the caller receives a `TimeoutError`. Trapping the exception in -the task is optional. The caller must trap the `TimeoutError` otherwise the +the task is optional. The caller must trap the `TimeoutError`, otherwise the exception will interrupt program execution. ```python @@ -1925,7 +1925,7 @@ The behaviour is "correct": CPython `asyncio` behaves identically. Ref # 6 Interfacing hardware -At heart all interfaces between `uasyncio` and external asynchronous events +At heart, all interfaces between `uasyncio` and external asynchronous events rely on polling. This is because of the cooperative nature of `uasyncio` scheduling: the task which is expected to respond to the event can only acquire control after another task has relinquished it. There are two ways to handle @@ -1935,7 +1935,7 @@ this. This is the approach used by `ThreadSafeFlag`. * Explicit polling: a user task does busy-wait polling on the hardware. -At its simplest explicit polling may consist of code like this: +At its simplest, explicit polling may consist of code like this: ```python async def poll_my_device(): global my_flag # Set by device ISR @@ -1951,7 +1951,7 @@ might be used. Explicit polling is discussed further [below](./TUTORIAL.md#62-polling-hardware-with-a-task). Implicit polling is more efficient and may gain further from planned -improvements to I/O scheduling. Aside from the use of `ThreadSafeFlag` it is +improvements to I/O scheduling. Aside from the use of `ThreadSafeFlag`, it is possible to write code which uses the same technique. This is by designing the driver to behave like a stream I/O device such as a socket or UART, using `stream I/O`. This polls devices using Python's `select.poll` system: because @@ -1959,7 +1959,7 @@ polling is done in C it is faster and more efficient than explicit polling. The use of `stream I/O` is discussed [here](./TUTORIAL.md#63-using-the-stream-mechanism). -Owing to its efficiency implicit polling most benefits fast I/O device drivers: +Owing to its efficiency, implicit polling most benefits fast I/O device drivers: streaming drivers can be written for many devices not normally considered as streaming devices [section 6.4](./TUTORIAL.md#64-writing-streaming-device-drivers). @@ -2365,14 +2365,14 @@ import as_drivers.htu21d.htu_test ## 7.1 Program hangs Hanging usually occurs because a task has blocked without yielding: this will -hang the entire system. When developing it is useful to have a task which +hang the entire system. When developing, it is useful to have a task which periodically toggles an onboard LED. This provides confirmation that the scheduler is running. ## 7.2 uasyncio retains state -If a `uasyncio` application terminates, state is retained. Embedded code seldom -terminates, but in testing it is useful to re-run a script without the need for +If a `uasyncio` application terminates, the state is retained. Embedded code seldom +terminates, but in testing, it is useful to re-run a script without the need for a soft reset. This may be done as follows: ```python import uasyncio as asyncio @@ -2389,7 +2389,7 @@ def test(): asyncio.new_event_loop() # Clear retained state ``` It should be noted that clearing retained state is not a panacea. Re-running -complex applications may require state to be retained. +complex applications may require the state to be retained. ###### [Contents](./TUTORIAL.md#contents) @@ -2422,7 +2422,7 @@ async def rr(n): ``` As an example of the type of hazard which can occur, in the `RecordOrientedUart` -example above the `__await__` method was originally written as: +example above, the `__await__` method was originally written as: ```python def __await__(self): @@ -2434,7 +2434,7 @@ example above the `__await__` method was originally written as: self.data = data ``` -In testing this hogged execution until an entire record was received. This was +In testing, this hogged execution until an entire record was received. This was because `uart.any()` always returned a nonzero quantity. By the time it was called, characters had been received. The solution was to yield execution in the outer loop: @@ -2556,14 +2556,14 @@ Application developers should typically use the high-level asyncio functions, such as `asyncio.run()`, and should rarely need to reference the loop object or call its methods. This section is intended mostly for authors of lower-level code, libraries, and frameworks, who need finer control over the event loop -behavior. [reference](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_event_loop). +behavior ([reference](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_event_loop)). This doc offers better alternatives to `get_event_loop` if you can confine support to CPython V3.8+. There is an event loop method `run_forever` which takes no args and causes the event loop to run. This is supported by `uasyncio`. This has use cases, notably -when all an application's tasks are instantiated in other modules. +when all of an application's tasks are instantiated in other modules. ## 7.8 Race conditions @@ -2573,12 +2573,12 @@ resource in a mutually incompatible manner. This behaviour can be demonstrated by running [the switch test](./primitives/tests/switches.py). In `test_sw()` coroutines are scheduled by events. If the switch is cycled rapidly the LED behaviour may seem surprising. This is because each time the -switch is closed a coro is launched to flash the red LED; on each open event -one is launched for the green LED. With rapid cycling a new coro instance will +switch is closed, a coro is launched to flash the red LED, and on each open event, +a coro is launched for the green LED. With rapid cycling a new coro instance will commence while one is still running against the same LED. This race condition leads to the LED behaving erratically. -This is a hazard of asynchronous programming. In some situations it is +This is a hazard of asynchronous programming. In some situations, it is desirable to launch a new instance on each button press or switch closure, even if other instances are still incomplete. In other cases it can lead to a race condition, leading to the need to code an interlock to ensure that the desired @@ -2611,7 +2611,7 @@ the `uasyncio` approach to a solution. [Section 8.5](./TUTORIAL.md#85-why-cooperative-rather-than-pre-emptive) discusses the relative merits of `uasyncio` and the `_thread` module and why -you may prefer use cooperative (`uasyncio`) over pre-emptive (`_thread`) +you may prefer to use cooperative (`uasyncio`) over pre-emptive (`_thread`) scheduling. ###### [Contents](./TUTORIAL.md#contents) @@ -2836,7 +2836,7 @@ When it comes to embedded systems the cooperative model has two advantages. Firstly, it is lightweight. It is possible to have large numbers of tasks because unlike descheduled threads, paused tasks contain little state. Secondly it avoids some of the subtle problems associated with pre-emptive -scheduling. In practice cooperative multi-tasking is widely used, notably in +scheduling. In practice, cooperative multi-tasking is widely used, notably in user interface applications. To make a case for the defence a pre-emptive model has one advantage: if @@ -2847,7 +2847,7 @@ for x in range(1000000): # do something time consuming ``` -it won't lock out other threads. Under cooperative schedulers the loop must +it won't lock out other threads. Under cooperative schedulers, the loop must explicitly yield control every so many iterations e.g. by putting the code in a task and periodically issuing `await asyncio.sleep(0)`. @@ -2874,7 +2874,7 @@ An eloquent discussion of the evils of threading may be found ## 8.6 Communication -In non-trivial applications tasks need to communicate. Conventional Python +In non-trivial applications, tasks need to communicate. Conventional Python techniques can be employed. These include the use of global variables or declaring tasks as object methods: these can then share instance variables. Alternatively a mutable object may be passed as a task argument. @@ -2905,7 +2905,7 @@ between a hardware event occurring and its handling task being scheduled is `N`ms, assuming that the mechanism for detecting the event adds no latency of its own. -In practice `N` is likely to be on the order of many ms. On fast hardware there +In practice, `N` is likely to be on the order of many ms. On fast hardware there will be a negligible performance difference between polling the hardware and polling a flag set by an ISR. On hardware such as ESP8266 and ESP32 the ISR approach will probably be slower owing to the long and variable interrupt From 3361bf8eb1f9c1a9cb604a8ba15aeeaf99b0ca1f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 23 Sep 2022 14:59:57 +0100 Subject: [PATCH 159/305] delay_ms.py: Add set method. --- v3/docs/TUTORIAL.md | 1 + v3/primitives/delay_ms.py | 1 + v3/primitives/events.py | 18 +++++++++++++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index ca66357..3cb5c6e 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1249,6 +1249,7 @@ Synchronous methods: be assigned, reassigned or disabled at run time. 7. `deinit` No args. Cancels the running task. See [Object scope](./TUTORIAL.md#44-object-scope). 8. `clear` No args. Clears the `Event` described in `wait` below. + 9. `set` No args. Sets the `Event` described in `wait` below. Asynchronous method: 1. `wait` One or more tasks may wait on a `Delay_ms` instance. Pause until the diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py index ac36235..bfed02d 100644 --- a/v3/primitives/delay_ms.py +++ b/v3/primitives/delay_ms.py @@ -27,6 +27,7 @@ def __init__(self, func=None, args=(), duration=1000): self._tout = asyncio.Event() # Timeout event self.wait = self._tout.wait # Allow: await wait_ms.wait() self.clear = self._tout.clear + self.set = self._tout.set self._ttask = self._fake # Timer task self._mtask = asyncio.create_task(self._run()) #Main task diff --git a/v3/primitives/events.py b/v3/primitives/events.py index b3e9a4c..1c48501 100644 --- a/v3/primitives/events.py +++ b/v3/primitives/events.py @@ -6,7 +6,7 @@ import uasyncio as asyncio from . import Delay_ms -# An Event-like class that can wait on an iterable of Event instances. +# An Event-like class that can wait on an iterable of Event-like instances. # .wait pauses until any passed event is set. class WaitAny: def __init__(self, events): @@ -32,7 +32,11 @@ async def wt(self, event): def event(self): return self.trig_event -# An Event-like class that can wait on an iterable of Event instances, + def clear(self): + for evt in (x for x in self.events if hasattr(x, 'clear')): + evt.clear() + +# An Event-like class that can wait on an iterable of Event-like instances, # .wait pauses until all passed events have been set. class WaitAll: def __init__(self, events): @@ -48,6 +52,10 @@ async def wt(event): for task in tasks: task.cancel() + def clear(self): + for evt in (x for x in self.events if hasattr(x, 'clear')): + evt.clear() + # Minimal switch class having an Event based interface class ESwitch: debounce_ms = 50 @@ -62,7 +70,7 @@ def __init__(self, pin, lopen=1): # Default is n/o switch returned to gnd async def _poll(self, dt): # Poll the button while True: - if (s := self._pin() ^ self._lopen) != self._state: + if (s := self._pin() ^ self._lopen) != self._state: # 15μs self._state = s self._of() if s else self._cf() await asyncio.sleep_ms(dt) # Wait out bounce @@ -80,6 +88,8 @@ def __call__(self): def deinit(self): self._poll.cancel() + self.open.clear() + self.close.clear() # Minimal pushbutton class having an Event based interface class EButton: @@ -152,3 +162,5 @@ def __call__(self): def deinit(self): for task in self._tasks: task.cancel() + for evt in (self.press, self.double, self.long, self.release): + evt.clear() From c75fb9ef863706b516ac147817e1ec49379ff3ba Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 23 Sep 2022 16:06:26 +0100 Subject: [PATCH 160/305] Add EVENTS.md (provisional). --- v3/docs/EVENTS.md | 454 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 454 insertions(+) create mode 100644 v3/docs/EVENTS.md diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md new file mode 100644 index 0000000..5c15f0c --- /dev/null +++ b/v3/docs/EVENTS.md @@ -0,0 +1,454 @@ +# 1. An alternative to callbacks in uasyncio code + +A hardware device like a pushbutton or a software object like an MQTT client +is designed to respond to an external asynchronous event. At the device driver +level there are two common approaches to handling this (see note below): + 1. The driver provides a method which blocks until the event occurs (e.g. + [machine.uart.read][1r]. + 2. The user specifies a callback function which runs when the event occurs + (e.g.[umqtt.simple][2r]). + +The first approach is incompatible with asynchronous code because the blocking +method stalls the scheduler while it is waiting. The second solves this because +the callback consumes no resources until the event actually occurs. However it +is not without problems. There is no standard way to specify callbacks, nor is +there a standard way to pass arguments to them or to retrieve a result. Further +a user might want to launch a task rather than run a synchronous callback. All +these problems can be solved, but solutions are _ad hoc_ and will vary between +drivers. + +For example, `umqtt.simple` has a `set_callback` method with no way to pass +args. Other drivers will require the callback (and perhaps args) to be passed +as a constructor arg. Some drivers provide a method enabling the callback's +result to be retrieved. + +Further, if a requirement has logic such as to "send a message if event A +is followed by either event B or event C" you are likely to find yourself at +the gates of a place commonly known as "callback hell". + +The one merit of designing callbacks into drivers is that it enables users to +access `uasyncio` code with purely synchronous code. Typical examples are GUIs +such as [micro-gui][1m]). These application frameworks use asynchronous code +internally but may be accessed with conventional synchronous code. + +For asynchronous programmers the API's of drivers, and their internal logic, +can be simplified by abandoning callbacks in favour of `Event` beaviour. In +essence the driver might expose an `Event` instance or be designed to emulate +an `Event`. No capability is lost because the application can launch a callback +or task when the `Event` is set. With the design approach outlined below, the +need for callbacks is much reduced. + +Note the `Stream` mechanism provides another approach which works well with +devices such as sockets and UARTs. It is less well suited to handling arbitrary +events, partly because it relies on polling under the hood. + +# 2. Rationale + +Consider a device driver `Sensor` which has a bound `Event` object `.ready`. +An application might run a task of form: +```python +async def process_sensor(): + while True: + await sensor.ready.wait() + sensor.ready.clear() + # Read and process sensor data +``` +Note that the action taken might be to run a callback or to launch a task: +```python +async def process_sensor(): + while True: + await sensor.ready.wait() + sensor.ready.clear() + result = callback(args) + asyncio.create_task(sensor_coro(args)) +``` +An `Event` interface allows callback-based code and makes straightforward the +passing of arguments and retrieval of return values. However it also enables a +progrmming style that largely eliminates callbacks. Note that all you need to +know to access this driver interface is the name of the bound `Event`. + +This doc aims to demostrate that the event based approach can simplify +application logic by eliminating the need for callbacks. + +The design of `uasyncio` V3 and its `Event` class enables this approach +because: + 1. A task waiting on an `Event` is put on a queue where it consumes no CPU + cycles until the event is triggered. + 2. The design of `uasyncio` can support large numbers of tasks (hundreds) on + a typical microcontroller. Proliferation of tasks is not a problem, especially + where they are small and spend most of the time paused waiting on queues. + +This contrasts with other schedulers (such as `uasyncio` V2) where there was no +built-in `Event` class; typical `Event` implementations used polling and were +convenience objects rather than performance solutions. + +The `Event` class `.clear` method provides additional flexibility relative to +callbacks: + 1. An `Event` can be cleared immediately after being set; if multiple tasks + are waiting on `.wait()`, all will resume running. + 2. Alternatively the `Event` may be cleared later. The timing of clearing the + `Event` determines its behaviour if, at the time when the `Event` is set, a + task waiting on the `await event.wait()` statement has not yet reached it. If + execution reaches `.wait()` before the `Event` is cleared, it will not pause. + If the `Event` is cleared, it will pause until it is set again. + +# 3. Device driver design + +This document introduces the idea of an event-like object (ELO). This is an +object which may be used in place of an `Event` in program code. An ELO must +expose a `.wait` asynchronous method which will pause until an event occurs. +Additionally it can include `.clear` and/or `.set`. A device driver may become +an ELO by implementing `.wait` or by subclassing `Event` or `ThreadSafeFlag`. +Alternatively a driver may expose one or more bound `Event` or ELO instances. + +ELO examples are: + +| Object | wait | clear | set | comments | +|:---------------------|:----:|:-----:|:---:|:------------------| +| [Event][4m] | Y | Y | Y | | +| [ThreadSafeFlag][3m] | Y | N | Y | Self-clearing | +| [Message][7m] | Y | N | Y | Subclass of above | +| [Delay_ms][2m] | Y | Y | Y | Self-setting | +| WaitAll | Y | Y | N | See below | +| WaitAny | Y | Y | N | | + +Drivers exposing `Event` instances include: + + * [ESwitch](./EVENTS.md#61-eswitch) Micro debounced interface to a switch. + * [EButton](./EVENTS.md#62-ebutton) Micro debounced interface to a pushbutton. + * [Switch][5m] Similar but interfaces also expose callbacks. + * [Pushbutton][6m] + +# 4. Primitives + +Applying `Events` to typical logic problems requires two new primitives: +`WaitAny` and `WaitAll`. Each is an ELO. These primitives may be cancelled or +subject to a timeout with `uasyncio.wait_for()`, although judicious use of +`Delay_ms` offers greater flexibility than `wait_for`. + +## 4.1 WaitAny + +The constructor takes an iterable of ELO's. Its `.wait` method pauses until the +first of the ELO's is set; the method returns the object that triggered it, +enabling the application code to determine the reason for its triggering. + +The last ELO to trigger a `WaitAny` instance may also be retrieved by issuing +the instance's `.event()` method. +```python +from primitives import WaitAny +async def foo(elo1, elo2) + evt = WaitAny((elo1, elo2)).wait() + if evt is elo1: + # Handle elo1 +``` +`WaitAny` has a `clear` method which issues `.clear()` to all passed ELO's with +a `.clear` method. + +## 4.2 WaitAll + +The constructor takes an iterable of ELO's. Its `.wait` method pauses until all +of the ELO's is set. + +`WaitAll` has a `clear` method which issues `.clear()` to all passed ELO's with +a `.clear` method. + +## 4.3 Nesting + +The fact that these primitives are ELO's enables nesting: +```Python +await WaitAll((event1, event2, WaitAny(event3, event4))).wait() +``` +This will pause until `event1` and `event2` and either `event3`or `event4` have +been set. + +# 5. Event based programming + +## 5.1 Use of Delay_ms + +The [Delay_ms class](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#38-delay_ms-class) +is an ELO and can be used as an alternative to `asyncio.wait_for`: it has the +advantage that it can be retriggered. It can also be stopped or its duration +changed dynamically. In the following sample `task_a` waits on an `Event` but +it also aborts if `task_b` stops running for any reason: +```python +from primitives import Delay_ms, WaitAny +delay = Delay_ms(duration=1000) +async def task_b(): + while True: + delay.trigger() # Keep task_a alive + # do some work + await asyncio.sleep_ms(0) + +async def task_a(evt): # Called with an event to wait on + while True: + cause = await WaitAny((evt, delay)).wait() + if cause is delay: # task_b has ended + delay.clear() # Clear the Event + return # Abandon the task + # Event has occurred + evt.clear() + # Do some work + await asyncio.sleep_ms(0) +``` +## 5.2 Long and very long button press + +A user had a need to distinguish short, fairly long, and very long presses of a +pushbutton. There was no requirement to detect double clicks, so the minimal +`ESwitch` driver was used. + +This solution does not attempt to disambiguate the press events: if a very long +press occurs, the short press code will run, followed by the "fairly long" +code, and then much later by the "very long" code. Disambiguating implies first +waiting for button release and then determining which application code to run: +in the application this delay was unacceptable. +```python +async def main(): + btn = ESwitch(Pin('X17', Pin.IN, Pin.PULL_UP), lopen=0) + ntim = Delay_ms(duration = 1000) # Fairly long press + ltim = Delay_ms(duration = 8000) # Very long press + while True: + ltim.stop() # Stop any running timers and clear their event + ntim.stop() + await btn.close.wait() + btn.close.clear() + ntim.trigger() # Button pressed, start timers, await release + ltim.trigger() # Run any press code + ev = await WaitAny((btn.open, ntim)).wait() + if ev is btn.open: + # Run "short press" application code + else: # ev is ntim: Fairly long timer timed out + # Run "fairly long" application code + # then check for very long press + ev = await WaitAny((btn.open, ltim)).wait() + if ev is ltim: # Long timer timed out + # Run "very long" application code + # We have not cleared the .open Event, so if the switch is already open + # there will be no delay below. Otherwise we await realease. + # Must await release otherwise the event is cleared before release + # occurs, setting the release event before the next press event. + await btn.open.wait() + btn.open.clear() +``` +Disambiguated version. Wait for button release and decide what to do based on +which timers are still running: +```python +async def main(): + btn = ESwitch(Pin('X17', Pin.IN, Pin.PULL_UP), lopen=0) + ntim = Delay_ms(duration=1000) # Fairly long press + ltim = Delay_ms(duration=8000) # Very long press + while True: + ltim.stop() # Stop any running timers and clear their event + ntim.stop() + await btn.close.wait() + btn.close.clear() + ntim.trigger() # Button pressed, start timers, await release + ltim.trigger() # Run any press code + await btn.open.wait() + btn.open.clear() + # Button released: check for any running timers + if not ltim(): # Very long press timer timed out before button was released + # Run "Very long" code + elif not ntim(): + # Run "Fairly long" code + else: + # Both timers running: run "short press" code +``` + +## 5.3 Application example + +A measuring instrument is started by pressing a button. The measurement +normally runs for five seconds. If the sensor does not detect anything, the +test runs until it does, however it is abandoned if nothing has been detected +after a minute. While running, extra button presses are ignored. During a +normal five second run, extra detections from the sensor are ignored. + +This can readily be coded using callbacks and synchronous or asynchronous code, +however the outcome is likely to have a fair amount of _ad hoc_ logic. + +This event based solution is arguably clearer to read: +```python +from primitives import EButton, WaitAll, Delay_ms +btn = EButton(args) # Has Events for press, release, double, long +bp = btn.press +sn = Sensor(args) # Assumed to have an Event interface. +tm = Delay_ms(duration=5_000) # Exposes .wait and .clear only. +events = (sn, tm) +async def foo(): + while True: + bp.clear() # Ignore prior button press + await bp.wait() # Button pressed + events.clear() # Ignore events that were set prior to this moment + tm.trigger() # Start 5 second timer + try: + await asyncio.wait_for(WaitAll(events).wait(), 60) + except asyncio.TimeoutError: + print("No reading from sensor") + else: + # Normal outcome, process readings +``` + +# 6. Drivers + +This document describes drivers for mechanical switches and pushbuttons. These +have event based interfaces exclusively and support debouncing. The drivers are +simplified alternatives for +[Switch](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/switch.py) +and [Pushbutton](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/pushbutton.py), +which also support callbacks. + +## 6.1 ESwitch + +This provides a debounced interface to a switch connected to gnd or to 3V3. A +pullup or pull down resistor should be supplied to ensure a valid logic level +when the switch is open. The default constructor arg `lopen=1` is for a switch +connected between the pin and gnd, with a pullup to 3V3. Typically the pullup +is internal, the pin being as follows: +```python +from machine import Pin +pin_id = 0 # Depends on hardware +pin = Pin(pin_id, Pin.IN, Pin.PULL_UP) +``` +Constructor arguments: + + 1. `pin` The Pin instance: should be initialised as an input with a pullup or + down as appropriate. + 2. `lopen=1` Electrical level when switch is open circuit i.e. 1 is 3.3V, 0 is + gnd. + +Methods: + + 1. `__call__` Call syntax e.g. `myswitch()` returns the logical debounced + state of the switch i.e. 0 if open, 1 if closed. + 2. `deinit` No args. Cancels the polling task and clears bound `Event`s. + +Bound objects: + 1. `debounce_ms` An `int`. Debounce time in ms. Default 50. + 2. `close` An `Event` instance. Set on contact closure. + 3. `open` An `Event` instance. Set on contact open. + +Application code is responsible for clearing the `Event` instances. + +## 6.2 EButton + +This extends the functionality of `ESwitch` to provide additional events for +long and double presses. + +This can support normally open or normally closed switches, connected to `gnd` +(with a pullup) or to `3V3` (with a pull-down). The `Pin` object should be +initialised appropriately. The default state of the switch can be passed in the +optional "sense" parameter on the constructor, otherwise the assumption is that +on instantiation the button is not pressed. + +The Pushbutton class uses logical rather than physical state: a button's state +is considered `True` if pressed, otherwise `False` regardless of its physical +implementation. + +Constructor arguments: + + 1. `pin` Mandatory. The initialised Pin instance. + 2. `suppress` Default `False`. See [section 6.2.1](./EVENTS.md#621-the-suppress-constructor-argument). + 3. `sense` Default `None`. Optionally define the electrical connection: see + [section 6.2.2](./EVENTS.md#622-the-sense-constructor-argument) + +Methods: + + 1. `__call__` Call syntax e.g. `mybutton()` Returns the logical debounced + state of the button (`True` corresponds to pressed). + 2. `rawstate()` Returns the logical instantaneous state of the button. There + is probably no reason to use this. + 3. `deinit` No args. Cancels the running task and clears all events. + +Bound `Event`s: + + 1. `press` Set on button press. + 2. `release` Set on button release. + 3. `long` Set if button press is longer than `EButton.long_press_ms`. + 4. `double` Set if two button preses occur within `EButton.double_click_ms`. + +Application code is responsible for clearing these `Event`s + +Class attributes: + 1. `debounce_ms` Debounce time in ms. Default 50. + 2. `long_press_ms` Threshold time in ms for a long press. Default 1000. + 3. `double_click_ms` Threshold time in ms for a double-click. Default 400. + +### 6.2.1 The suppress constructor argument + +Consider a button double-click. By default with `suppress=False` this will set +the bound `Event` instances in order, as follows: + + * `press` + * `release` + * `press` + * `release` + * `double` + +Similarly a long press will trigger `press`, `long` and `release` in that +order. Some +applications may require only a single `Event` to be triggered. Setting +`suppress=True` ensures this. Outcomes are as follows: + +| Occurence | Events set | Time of pimary event | +|:-------------|:----------------|:-----------------------------| +| Short press | press, release | After `.double_click_ms` | +| Double press | double, release | When the second press occurs | +| Long press | long, release | After `long_press_ms` | + +The tradeoff is that the `press` and `release` events are delayed: the soonest +it is possible to detect the lack of a double click is `.double_click_ms`ms +after a short button press. Hence in the case of a short press when `suppress` +is `True`, `press` and `release` events are set on expiration of the double +click timer. + +### 6.2.2 The sense constructor argument + +In most applications it can be assumed that, at power-up, pushbuttons are not +pressed. The default `None` value uses this assumption to read the pin state +and to assign the result to the `False` (not pressed) state at power up. This +works with normally open or normally closed buttons wired to either supply +rail; this without programmer intervention. + +In certain use cases this assumption does not hold, and `sense` must explicitly +be specified. This defines the logical state of the un-pressed button. Hence +`sense=0` defines a button connected in such a way that when it is not pressed, +the voltage on the pin is gnd. + +Whenever the pin value changes, the new value is compared with `sense` to +determine whether the button is closed or open. + +# Appendix 1 Polling + +The primitives or drivers referenced here do not use polling with the following +exceptions: + 1. Switch and pushbutton drivers. These poll the `Pin` instance for electrical + reasons described below. + 2. `ThreadSafeFlag` and subclass `Message`: these use the stream mechanism. + +Other drivers and primitives are designed such that paused tasks are waiting on +queues and are therefore using no CPU cycles. + +[This reference][1e] states that bouncing contacts can assume invalid logic +levels for a period. It is a reaonable assumption that `Pin.value()` always +returns 0 or 1: the drivers are designed to cope with any sequence of such +readings. By contrast, the behaviour of IRQ's under such conditions may be +abnormal. It would be hard to prove that IRQ's could never be missed, across +all platforms and input conditions. + +Pin polling aims to use minimal resources, the main overhead being `uasyncio`'s +task switching overhead: typically about 250 μs. The default polling interval +is 50 ms giving an overhead of ~0.5%. + + +[1m]: https://github.com/peterhinch/micropython-micro-gui +[2m]: https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#38-delay_ms-class + +[3m]: https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#36-threadsafeflag +[4m]: https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#32-event +[5m]: https://github.com/peterhinch/micropython-async/blob/master/v3/docs/DRIVERS.md#31-switch-class +[6m]: https://github.com/peterhinch/micropython-async/blob/master/v3/docs/DRIVERS.md#41-pushbutton-class +[7m]: https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#39-message + +[1r]: http://docs.micropython.org/en/latest/library/machine.UART.html#machine.UART.read +[2r]: https://github.com/micropython/micropython-lib/blob/ad9309b669cd4474bcd4bc0a67a630173222dbec/micropython/umqtt.simple/umqtt/simple.py + +[1e](http://www.ganssle.com/debouncing.htm) From f9bed071b5f47d737bbb0e42853f30a19504674d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 23 Sep 2022 16:08:14 +0100 Subject: [PATCH 161/305] Add EVENTS.md (provisional). --- v3/docs/EVENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 5c15f0c..bc76bdd 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -451,4 +451,4 @@ is 50 ms giving an overhead of ~0.5%. [1r]: http://docs.micropython.org/en/latest/library/machine.UART.html#machine.UART.read [2r]: https://github.com/micropython/micropython-lib/blob/ad9309b669cd4474bcd4bc0a67a630173222dbec/micropython/umqtt.simple/umqtt/simple.py -[1e](http://www.ganssle.com/debouncing.htm) +[1e]: http://www.ganssle.com/debouncing.htm From 2b05b39e64ad0aa8f06b15d01c6136b0448dd91e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 25 Sep 2022 14:17:40 +0100 Subject: [PATCH 162/305] EVENTS.md: Add TOC. --- v3/docs/EVENTS.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index bc76bdd..de4a50f 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -1,3 +1,30 @@ +# Synopsis + +Using `Event` instances rather than callbacks in `uasyncio` device drivers can +simplify their design and standardise their APIs. It can also simplify +application logic. + +This document assumes familiarity with `uasyncio`. See [official docs](http://docs.micropython.org/en/latest/library/uasyncio.html) and +[unofficial tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md). + + 1. [An alternative to callbacks in uasyncio code](./EVENTS.md#1-an-alternative-to-callbacks-in-uasyncio-code) + 2. [Rationale](./EVENTS.md#2-rationale) + 3. [Device driver design](./EVENTS.md#3-device-driver-design) + 4. [Primitives](./EVENTS.md#4-primitives) + 4.1 [WaitAny](./EVENTS.md#41-waitany) + 4.2 [WaitAll](./EVENTS.md#42-waitall) + 4.3 [Nesting](./EVENTS.md#43-nesting) + 5. [Event based programming](./EVENTS.md#5-event-based-programming) + 5.1 [Use of Delay_ms](./EVENTS.md#51-use-of-delay_ms) + 5.2 [Long and very long button press](./EVENTS.md#52-long-and-very-long-button-press) + 5.3 [Application example](./EVENTS.md#53-application-example) + 6. [Drivers](./EVENTS.md#6-drivers) + 6.1 [ESwitch](./EVENTS.md#61-eswitch) + 6.2 [EButton](./EVENTS.md#62-ebutton) +      6.2.1 [The suppress constructor argument](./EVENTS.md#621-the-suppress-constructor-argument) +      6.2.2 [The sense constructor argument](./EVENTS.md#622-the-sense-constructor-argument) +[Appendix 1 Polling](./EVENTS.md-appendix-1-polling) + # 1. An alternative to callbacks in uasyncio code A hardware device like a pushbutton or a software object like an MQTT client From 783717a06cf4deb5ad129b41ac1de06e2391eecd Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 25 Sep 2022 14:22:26 +0100 Subject: [PATCH 163/305] EVENTS.md: Add TOC. --- v3/docs/EVENTS.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index de4a50f..9ba2611 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -7,6 +7,8 @@ application logic. This document assumes familiarity with `uasyncio`. See [official docs](http://docs.micropython.org/en/latest/library/uasyncio.html) and [unofficial tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md). +# 0. Contents + 1. [An alternative to callbacks in uasyncio code](./EVENTS.md#1-an-alternative-to-callbacks-in-uasyncio-code) 2. [Rationale](./EVENTS.md#2-rationale) 3. [Device driver design](./EVENTS.md#3-device-driver-design) @@ -23,7 +25,7 @@ This document assumes familiarity with `uasyncio`. See [official docs](http://do 6.2 [EButton](./EVENTS.md#62-ebutton)      6.2.1 [The suppress constructor argument](./EVENTS.md#621-the-suppress-constructor-argument)      6.2.2 [The sense constructor argument](./EVENTS.md#622-the-sense-constructor-argument) -[Appendix 1 Polling](./EVENTS.md-appendix-1-polling) +[Appendix 1 Polling](./EVENTS.md#-appendix-1-polling) # 1. An alternative to callbacks in uasyncio code @@ -69,6 +71,8 @@ Note the `Stream` mechanism provides another approach which works well with devices such as sockets and UARTs. It is less well suited to handling arbitrary events, partly because it relies on polling under the hood. +###### [Contents](./EVENTS.md#0-contents) + # 2. Rationale Consider a device driver `Sensor` which has a bound `Event` object `.ready`. @@ -119,6 +123,8 @@ callbacks: execution reaches `.wait()` before the `Event` is cleared, it will not pause. If the `Event` is cleared, it will pause until it is set again. +###### [Contents](./EVENTS.md#0-contents) + # 3. Device driver design This document introduces the idea of an event-like object (ELO). This is an @@ -146,6 +152,8 @@ Drivers exposing `Event` instances include: * [Switch][5m] Similar but interfaces also expose callbacks. * [Pushbutton][6m] +###### [Contents](./EVENTS.md#0-contents) + # 4. Primitives Applying `Events` to typical logic problems requires two new primitives: @@ -188,6 +196,8 @@ await WaitAll((event1, event2, WaitAny(event3, event4))).wait() This will pause until `event1` and `event2` and either `event3`or `event4` have been set. +###### [Contents](./EVENTS.md#0-contents) + # 5. Event based programming ## 5.1 Use of Delay_ms @@ -281,6 +291,8 @@ async def main(): # Both timers running: run "short press" code ``` +###### [Contents](./EVENTS.md#0-contents) + ## 5.3 Application example A measuring instrument is started by pressing a button. The measurement @@ -314,6 +326,8 @@ async def foo(): # Normal outcome, process readings ``` +###### [Contents](./EVENTS.md#0-contents) + # 6. Drivers This document describes drivers for mechanical switches and pushbuttons. These @@ -355,6 +369,8 @@ Bound objects: Application code is responsible for clearing the `Event` instances. +###### [Contents](./EVENTS.md#0-contents) + ## 6.2 EButton This extends the functionality of `ESwitch` to provide additional events for @@ -443,6 +459,8 @@ the voltage on the pin is gnd. Whenever the pin value changes, the new value is compared with `sense` to determine whether the button is closed or open. +###### [Contents](./EVENTS.md#0-contents) + # Appendix 1 Polling The primitives or drivers referenced here do not use polling with the following From 5adc77e41902252d0754c5a8b95213c36ce35e3a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 25 Sep 2022 14:24:12 +0100 Subject: [PATCH 164/305] EVENTS.md: Add TOC. --- v3/docs/EVENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 9ba2611..793add9 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -25,7 +25,7 @@ This document assumes familiarity with `uasyncio`. See [official docs](http://do 6.2 [EButton](./EVENTS.md#62-ebutton)      6.2.1 [The suppress constructor argument](./EVENTS.md#621-the-suppress-constructor-argument)      6.2.2 [The sense constructor argument](./EVENTS.md#622-the-sense-constructor-argument) -[Appendix 1 Polling](./EVENTS.md#-appendix-1-polling) +[Appendix 1 Polling](./EVENTS.md#100-appendix-1-polling) # 1. An alternative to callbacks in uasyncio code @@ -461,7 +461,7 @@ determine whether the button is closed or open. ###### [Contents](./EVENTS.md#0-contents) -# Appendix 1 Polling +# 100 Appendix 1 Polling The primitives or drivers referenced here do not use polling with the following exceptions: From 6651918183031bdebd204985c39e5b1602afa98d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 25 Sep 2022 14:39:34 +0100 Subject: [PATCH 165/305] EVENTS.md: Add TOC. --- v3/docs/EVENTS.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 793add9..5ced4df 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -12,17 +12,17 @@ This document assumes familiarity with `uasyncio`. See [official docs](http://do 1. [An alternative to callbacks in uasyncio code](./EVENTS.md#1-an-alternative-to-callbacks-in-uasyncio-code) 2. [Rationale](./EVENTS.md#2-rationale) 3. [Device driver design](./EVENTS.md#3-device-driver-design) - 4. [Primitives](./EVENTS.md#4-primitives) - 4.1 [WaitAny](./EVENTS.md#41-waitany) - 4.2 [WaitAll](./EVENTS.md#42-waitall) + 4. [Primitives](./EVENTS.md#4-primitives) Facilitating Event-based application logic + 4.1 [WaitAny](./EVENTS.md#41-waitany) Wait on any of a group of event-like objects + 4.2 [WaitAll](./EVENTS.md#42-waitall) Wait on all of a group of event-like objects 4.3 [Nesting](./EVENTS.md#43-nesting) 5. [Event based programming](./EVENTS.md#5-event-based-programming) - 5.1 [Use of Delay_ms](./EVENTS.md#51-use-of-delay_ms) + 5.1 [Use of Delay_ms](./EVENTS.md#51-use-of-delay_ms) A retriggerable delay 5.2 [Long and very long button press](./EVENTS.md#52-long-and-very-long-button-press) 5.3 [Application example](./EVENTS.md#53-application-example) - 6. [Drivers](./EVENTS.md#6-drivers) - 6.1 [ESwitch](./EVENTS.md#61-eswitch) - 6.2 [EButton](./EVENTS.md#62-ebutton) + 6. [Drivers](./EVENTS.md#6-drivers) Minimal Event-based drivers + 6.1 [ESwitch](./EVENTS.md#61-eswitch) Debounced switch + 6.2 [EButton](./EVENTS.md#62-ebutton) Debounced pushbutton with double and long press events      6.2.1 [The suppress constructor argument](./EVENTS.md#621-the-suppress-constructor-argument)      6.2.2 [The sense constructor argument](./EVENTS.md#622-the-sense-constructor-argument) [Appendix 1 Polling](./EVENTS.md#100-appendix-1-polling) @@ -68,8 +68,9 @@ or task when the `Event` is set. With the design approach outlined below, the need for callbacks is much reduced. Note the `Stream` mechanism provides another approach which works well with -devices such as sockets and UARTs. It is less well suited to handling arbitrary -events, partly because it relies on polling under the hood. +devices such as sockets and UARTs. It is arguably less well suited to handling +arbitrary events, partly because it relies on +[polling](./EVENTS.md#100-appendix-1-polling) under the hood. ###### [Contents](./EVENTS.md#0-contents) @@ -110,8 +111,9 @@ because: where they are small and spend most of the time paused waiting on queues. This contrasts with other schedulers (such as `uasyncio` V2) where there was no -built-in `Event` class; typical `Event` implementations used polling and were -convenience objects rather than performance solutions. +built-in `Event` class; typical `Event` implementations used +[polling](./EVENTS.md#100-appendix-1-polling) and were convenience objects +rather than performance solutions. The `Event` class `.clear` method provides additional flexibility relative to callbacks: @@ -142,8 +144,8 @@ ELO examples are: | [ThreadSafeFlag][3m] | Y | N | Y | Self-clearing | | [Message][7m] | Y | N | Y | Subclass of above | | [Delay_ms][2m] | Y | Y | Y | Self-setting | -| WaitAll | Y | Y | N | See below | -| WaitAny | Y | Y | N | | +| [WaitAll](./EVENTS.md#42-waitall) | Y | Y | N | See below | +| [WaitAny](./EVENTS.md#41-waitany) | Y | Y | N | | Drivers exposing `Event` instances include: From 80e57298263f3d5c0e6e3c2a99c72de6cdd66533 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 25 Sep 2022 15:08:14 +0100 Subject: [PATCH 166/305] v3/README.md: Remove V2 porting guide. --- v3/README.md | 126 ++++++-------------------------------------- v3/docs/TUTORIAL.md | 3 +- 2 files changed, 17 insertions(+), 112 deletions(-) diff --git a/v3/README.md b/v3/README.md index 9b269b0..1fb6c23 100644 --- a/v3/README.md +++ b/v3/README.md @@ -1,9 +1,9 @@ # 1. Guide to uasyncio V3 -The new release of `uasyncio` is pre-installed in current daily firmware -builds and will be found in release builds starting with V1.13. This complete -rewrite of `uasyncio` supports CPython 3.8 syntax. A design aim is that it -should be be a compatible subset of `asyncio`. +This release of `uasyncio` is pre-installed on all platforms except severely +constrained ones such as the 1MB ESP8266. This rewrite of `uasyncio` supports +CPython 3.8 syntax. A design aim is that it should be be a compatible subset of +`asyncio`. The current version is 3.0.0. These notes and the tutorial should be read in conjunction with [the official docs](http://docs.micropython.org/en/latest/library/uasyncio.html) @@ -12,7 +12,10 @@ These notes and the tutorial should be read in conjunction with This repo contains the following: -### [V3 Tutorial](./docs/TUTORIAL.md) +### [V3 Tutorial](./docs/TUTORIAL.md) + +Intended for users with all levels of experience with asynchronous programming. + ### Test/demo scripts Documented in the tutorial. @@ -46,6 +49,11 @@ useful in their own right: * [HD44780](./docs/hd44780.md) Driver for common character based LCD displays based on the Hitachi HD44780 controller. +### Event-based programming + +[A guide](./docs/EVENTS.md) to a writing applications and device drivers which +largely does away with callbacks. + ### A monitor This [monitor](https://github.com/peterhinch/micropython-monitor) enables a @@ -98,9 +106,8 @@ will be addressed in due course. ### 2.1.1 Fast I/O scheduling There is currently no support for this: I/O is scheduled in round robin fashion -with other tasks. There are situations where this is too slow, for example in -I2S applications and ones involving multiple fast I/O streams, e.g. from UARTs. -In these applications there is still a use case for the `fast_io` V2 variant. +with other tasks. There are situations where this is too slow and the scheduler +should be able to poll I/O whenever it gains control. ### 2.1.2 Synchronisation primitives @@ -109,106 +116,3 @@ These CPython primitives are outstanding: * `BoundedSemaphore`. * `Condition`. * `Queue`. - -# 3. Porting applications from V2 - -Many applications using the coding style advocated in the V2 tutorial will work -unchanged. However there are changes, firstly to `uasyncio` itself and secondly -to modules in this repository. - -## 3.1 Changes to uasyncio - -### 3.1.1 Syntax changes - - * Task cancellation: `cancel` is now a method of a `Task` instance. - * Event loop methods: `call_at`, `call_later`, `call_later_ms` and - `call_soon` are no longer supported. In CPython docs these are - [lightly deprecated](https://docs.python.org/3/library/asyncio-eventloop.html#preface) - in application code; there are simple workrounds. - * `yield` in coroutines must be replaced by `await asyncio.sleep_ms(0)`: - this is in accord with CPython where `yield` will produce a syntax error. - * Awaitable classes. The `__iter__` method works but `yield` must be replaced - by `await asyncio.sleep_ms(0)`. - -It is possible to write an awaitable class with code portable between -MicroPython and CPython 3.8. This is discussed -[in the tutorial](./docs/TUTORIAL.md#412-portable-code). - -### 3.1.2 Change to stream I/O - -Classes based on `uio.IOBase` will need changes to the `write` method. See -[tutorial](./docs/TUTORIAL.md#64-writing-streaming-device-drivers). - -### 3.1.3 Early task creation - -It is [bad practice](https://github.com/micropython/micropython/issues/6174) -to create tasks before issuing `asyncio.run()`. CPython 3.8 throws if you do. -Such code can be ported by wrapping functions that create tasks in a -coroutine as below. - -There is a subtlety affecting code that creates tasks early: -`loop.run_forever()` did just that, never returning and scheduling all created -tasks. By contrast `asyncio.run(coro())` terminates when the coro does. Typical -firmware applications run forever so the coroutine started by `.run()` must -`await` a continuously running task. This may imply exposing an asynchronous -method which runs forever: - -```python -async def main(): - obj = MyObject() # Constructor creates tasks - await obj.run_forever() # Never terminates - -def run(): # Entry point - try: - asyncio.run(main()) - finally: - asyncio.new_event_loop() -``` - -## 3.2 Modules from this repository - -Modules `asyn.py` and `aswitch.py` are deprecated for V3 applications. See -[the tutorial](./docs/TUTORIAL.md#3-synchronisation) for V3 replacements which -are more RAM-efficient. - -### 3.2.1 Synchronisation primitives - -These were formerly provided in `asyn.py` and may now be found in the -`primitives` directory, along with additional unofficial primitives. - -The CPython `asyncio` library supports these synchronisation primitives: - * `Lock` - already incorporated in new `uasyncio`. - * `Event` - already incorporated. - * `gather` - already incorporated. - * `Semaphore` and `BoundedSemaphore`. In this repository. - * `Condition`. In this repository. - * `Queue`. In this repository. - -The above unofficial primitives are CPython compatible. Using future official -versions will require a change to the import statement only. - -### 3.2.2 Synchronisation primitives (old asyn.py) - -Applications using `asyn.py` should no longer import that module. Equivalent -functionality may now be found in the `primitives` directory: this is -implemented as a Python package enabling RAM savings. The new versions are also -more efficient, replacing polling with the new `Event` class. - -These features in `asyn.py` were workrounds for bugs in V2 and should not be -used with V3: - * The cancellation decorators and classes (cancellation works as per CPython). - * The nonstandard support for `gather` (now properly supported). - -The `Event` class in `asyn.py` is now replaced by `Message` - this is discussed -in [the tutorial](./docs/TUTORIAL.md#36-message). - -### 3.2.3 Switches, Pushbuttons and delays (old aswitch.py) - -Applications using `aswitch.py` should no longer import that module. Equivalent -functionality may now be found in the `primitives` directory: this is -implemented as a Python package enabling RAM savings. - -New versions are provided in this repository. Classes: - * `Delay_ms` Software retriggerable monostable (watchdog-like object). - * `Switch` Debounced switch with close and open callbacks. - * `Pushbutton` Pushbutton with double-click and long press callbacks. diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 3cb5c6e..4ce3eb5 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2,7 +2,8 @@ This tutorial is intended for users having varying levels of experience with asyncio and includes a section for complete beginners. It is for use with the -new version of `uasyncio`, currently V3.0.0. +new version of `uasyncio`, currently V3.0.0. See [this overview](../README.md) +for a summary of documents and resources for `uasyncio`. Most code samples are now complete scripts which can be cut and pasted at the REPL. From d6dc68c70bf714fad19ef13fa66d18863144b797 Mon Sep 17 00:00:00 2001 From: Nick Lamprianidis Date: Sun, 25 Sep 2022 20:45:08 +0200 Subject: [PATCH 167/305] demos: Minor fixes. --- v3/as_demos/apoll.py | 4 ++-- v3/as_demos/auart.py | 2 +- v3/docs/TUTORIAL.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/v3/as_demos/apoll.py b/v3/as_demos/apoll.py index 40c4233..2dbfeeb 100644 --- a/v3/as_demos/apoll.py +++ b/v3/as_demos/apoll.py @@ -37,7 +37,7 @@ def timed_out(self): # Time since last change or last timeout rep return True return False -async def accel_coro(timeout = 2000): +async def accel_coro(timeout=2000): accelhw = pyb.Accel() # Instantiate accelerometer hardware await asyncio.sleep_ms(30) # Allow it to settle accel = Accelerometer(accelhw, timeout) @@ -53,7 +53,7 @@ async def accel_coro(timeout = 2000): async def main(delay): print('Testing accelerometer for {} secs. Move the Pyboard!'.format(delay)) - print('Test runs for 20s.') + print('Test runs for {}s.'.format(delay)) asyncio.create_task(accel_coro()) await asyncio.sleep(delay) print('Test complete!') diff --git a/v3/as_demos/auart.py b/v3/as_demos/auart.py index 5e77123..f00aa82 100644 --- a/v3/as_demos/auart.py +++ b/v3/as_demos/auart.py @@ -18,7 +18,7 @@ async def receiver(): sreader = asyncio.StreamReader(uart) while True: res = await sreader.readline() - print('Recieved', res) + print('Received', res) async def main(): asyncio.create_task(sender()) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 4ce3eb5..c14d502 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -591,7 +591,7 @@ async def task(i, lock): lock.release() async def main(): - lock = asyncio.Lock() # The Lock instance + lock = Lock() # The Lock instance for n in range(1, 4): asyncio.create_task(task(n, lock)) await asyncio.sleep(10) @@ -620,7 +620,7 @@ async def task(i, lock): await asyncio.sleep(0.5) async def main(): - lock = asyncio.Lock() # The Lock instance + lock = Lock() # The Lock instance for n in range(1, 4): asyncio.create_task(task(n, lock)) await asyncio.sleep(10) From 1da7346a7e8ef495a0341cf205a8809abfae08d5 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 28 Sep 2022 10:06:54 +0100 Subject: [PATCH 168/305] README.md: Remove refs to V2. Add new doc refs. --- v3/README.md | 90 ++++++++++++++++++++++++++------------------- v3/docs/TUTORIAL.md | 10 ++--- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/v3/README.md b/v3/README.md index 1fb6c23..303e7b8 100644 --- a/v3/README.md +++ b/v3/README.md @@ -1,40 +1,71 @@ -# 1. Guide to uasyncio V3 +# 1. Guide to uasyncio -This release of `uasyncio` is pre-installed on all platforms except severely -constrained ones such as the 1MB ESP8266. This rewrite of `uasyncio` supports -CPython 3.8 syntax. A design aim is that it should be be a compatible subset of -`asyncio`. The current version is 3.0.0. +MicroPython's `uasyncio` is pre-installed on all platforms except severely +constrained ones such as the 1MB ESP8266. It supports CPython 3.8 syntax and +aims to be a compatible subset of `asyncio`. The current version is 3.0.0. -These notes and the tutorial should be read in conjunction with -[the official docs](http://docs.micropython.org/en/latest/library/uasyncio.html) +## 1.1 Documents -## 1.1 Resources for V3 +[uasyncio official docs](http://docs.micropython.org/en/latest/library/uasyncio.html) -This repo contains the following: +[Tutorial](./docs/TUTORIAL.md) Intended for users with all levels of experience +(or none) of asynchronous programming. -### [V3 Tutorial](./docs/TUTORIAL.md) +[Drivers](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/DRIVERS.md) +describes device drivers for switches, pushbuttons, ESP32 touch buttons, ADC's +and incremental encoders. -Intended for users with all levels of experience with asynchronous programming. +[Interrupts](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/INTERRUPTS.md) +is a guide to interfacing interrupts to `uasyncio`. -### Test/demo scripts +[Event-based programming](./docs/EVENTS.md) is a guide to a way of writing +applications and device drivers which largely does away with callbacks. Assumes +some knowledge of `uasyncio`. -Documented in the tutorial. +## 1.2 Debugging tools -### Synchronisation primitives +[aiorepl](https://github.com/micropython/micropython-lib/tree/master/micropython/aiorepl) +This official tool enables an application to launch a REPL which is active +while the application is running. From this you can modify and query the +application and run `uasyncio` scripts concurrently with the running +application. -Documented in the tutorial. Comprises: - * CPython primitives not yet officially supported. - * Two additional primitives `Barrier` and `Message`. - * Classes for interfacing switches and pushbuttons. - * A software retriggerable monostable timer class, similar to a watchdog. +[monitor](https://github.com/peterhinch/micropython-monitor) enables a running +`uasyncio` application to be monitored using a Pi Pico, ideally with a scope or +logic analyser. Normally requires only one GPIO pin on the target. -### A scheduler +![Image](https://github.com/peterhinch/micropython-monitor/raw/master/images/monitor.jpg) + +## 1.3 Resources in this repo + +### 1.3.1 Test/demo scripts + +Documented in the [tutorial](./docs/TUTORIAL.md). + +### 1.3.2 Synchronisation primitives + +Documented in the [tutorial](./docs/TUTORIAL.md). Comprises: + * Unsupported CPython primitives including `barrier`, `queue` and others. + * An additional primitive `Message`. + * A software retriggerable monostable timer class `Delay_ms`, similar to a + watchdog. + * Two primitives enabling waiting on groups of `Event` instances. + +### 1.3.3 Asynchronous device drivers + +These are documented +[here](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/DRIVERS.md): + * Classes for interfacing switches, pushbuttons and ESP32 touch buttons. + * Drivers for ADC's + * Drivers for incremental encoders. + +### 1.3.4 A scheduler This [lightweight scheduler](./docs/SCHEDULE.md) enables tasks to be scheduled at future times. These can be assigned in a flexible way: a task might run at 4.10am on Monday and Friday if there's no "r" in the month. -### Asynchronous device drivers +### 1.3.5 Asynchronous interfaces These device drivers are intended as examples of asynchronous code which are useful in their own right: @@ -43,27 +74,12 @@ useful in their own right: * [HTU21D](./docs/HTU21D.md) Temperature and humidity sensor. * [I2C](./docs/I2C.md) Use Pyboard I2C slave mode to implement a UART-like asynchronous stream interface. Uses: communication with ESP8266, or (with - coding) interface a Pyboard to I2C masters. + coding) to interface a Pyboard to I2C masters. * [NEC IR](./docs/NEC_IR.md) A receiver for signals from IR remote controls using the popular NEC protocol. * [HD44780](./docs/hd44780.md) Driver for common character based LCD displays based on the Hitachi HD44780 controller. -### Event-based programming - -[A guide](./docs/EVENTS.md) to a writing applications and device drivers which -largely does away with callbacks. - -### A monitor - -This [monitor](https://github.com/peterhinch/micropython-monitor) enables a -running `uasyncio` application to be monitored using a Pi Pico, ideally with a -scope or logic analyser. If designing hardware it is suggested to provide -access to a UART tx pin, or alternatively to three GPIO pins, to enable this to -be used if required. - -![Image](https://github.com/peterhinch/micropython-monitor/raw/master/images/monitor.jpg) - # 2. V3 Overview These notes are intended for users familiar with `asyncio` under CPython. diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 4ce3eb5..7b9d1de 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1,12 +1,12 @@ # Application of uasyncio to hardware interfaces This tutorial is intended for users having varying levels of experience with -asyncio and includes a section for complete beginners. It is for use with the -new version of `uasyncio`, currently V3.0.0. See [this overview](../README.md) -for a summary of documents and resources for `uasyncio`. +asyncio and includes a section for complete beginners. It is based on the +current version of `uasyncio`, V3.0.0. Most code samples are complete scripts +which can be cut and pasted at the REPL. -Most code samples are now complete scripts which can be cut and pasted at the -REPL. +See [this overview](../README.md) for a summary of resources for `uasyncio` +including device drivers, debugging aids, and documentation. # Contents From 1df12181c159349bd5f3673a1fb8b7282fb76d9b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 28 Sep 2022 10:13:30 +0100 Subject: [PATCH 169/305] README.md: Remove refs to V2. Add new doc refs. --- v3/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/v3/README.md b/v3/README.md index 303e7b8..38d5b44 100644 --- a/v3/README.md +++ b/v3/README.md @@ -9,7 +9,7 @@ aims to be a compatible subset of `asyncio`. The current version is 3.0.0. [uasyncio official docs](http://docs.micropython.org/en/latest/library/uasyncio.html) [Tutorial](./docs/TUTORIAL.md) Intended for users with all levels of experience -(or none) of asynchronous programming. +of asynchronous programming, including beginners. [Drivers](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/DRIVERS.md) describes device drivers for switches, pushbuttons, ESP32 touch buttons, ADC's @@ -45,7 +45,8 @@ Documented in the [tutorial](./docs/TUTORIAL.md). ### 1.3.2 Synchronisation primitives Documented in the [tutorial](./docs/TUTORIAL.md). Comprises: - * Unsupported CPython primitives including `barrier`, `queue` and others. + * Implementations of unsupported CPython primitives including `barrier`, + `queue` and others. * An additional primitive `Message`. * A software retriggerable monostable timer class `Delay_ms`, similar to a watchdog. From 3e738dae85987547f89baf240ada6fb8f6c39459 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 28 Sep 2022 18:49:53 +0100 Subject: [PATCH 170/305] EVENTS.md: Clarify introductory text. --- v3/docs/EVENTS.md | 74 ++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 5ced4df..70b9312 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -29,48 +29,38 @@ This document assumes familiarity with `uasyncio`. See [official docs](http://do # 1. An alternative to callbacks in uasyncio code -A hardware device like a pushbutton or a software object like an MQTT client -is designed to respond to an external asynchronous event. At the device driver -level there are two common approaches to handling this (see note below): - 1. The driver provides a method which blocks until the event occurs (e.g. - [machine.uart.read][1r]. - 2. The user specifies a callback function which runs when the event occurs - (e.g.[umqtt.simple][2r]). - -The first approach is incompatible with asynchronous code because the blocking -method stalls the scheduler while it is waiting. The second solves this because -the callback consumes no resources until the event actually occurs. However it -is not without problems. There is no standard way to specify callbacks, nor is -there a standard way to pass arguments to them or to retrieve a result. Further -a user might want to launch a task rather than run a synchronous callback. All -these problems can be solved, but solutions are _ad hoc_ and will vary between -drivers. - -For example, `umqtt.simple` has a `set_callback` method with no way to pass -args. Other drivers will require the callback (and perhaps args) to be passed -as a constructor arg. Some drivers provide a method enabling the callback's -result to be retrieved. - -Further, if a requirement has logic such as to "send a message if event A -is followed by either event B or event C" you are likely to find yourself at -the gates of a place commonly known as "callback hell". - -The one merit of designing callbacks into drivers is that it enables users to -access `uasyncio` code with purely synchronous code. Typical examples are GUIs -such as [micro-gui][1m]). These application frameworks use asynchronous code -internally but may be accessed with conventional synchronous code. - -For asynchronous programmers the API's of drivers, and their internal logic, -can be simplified by abandoning callbacks in favour of `Event` beaviour. In -essence the driver might expose an `Event` instance or be designed to emulate -an `Event`. No capability is lost because the application can launch a callback -or task when the `Event` is set. With the design approach outlined below, the -need for callbacks is much reduced. - -Note the `Stream` mechanism provides another approach which works well with -devices such as sockets and UARTs. It is arguably less well suited to handling -arbitrary events, partly because it relies on -[polling](./EVENTS.md#100-appendix-1-polling) under the hood. +Callbacks have two merits. They are familiar, and they enable an interface +which allows an asynchronous application to be accessed by synchronous code. +GUI frameworks such as [micro-gui][1m] form a classic example: the callback +interface may be accessed by synchronous or asynchronous code. + +For the programmer of asynchronous applications, callbacks are largely +unnecessary and their use can lead to bugs. + +The idiomatic way to write an asynchronous function that responds to external +events is one where the function pauses while waiting on the event: +```python +async def handle_messages(input_stream): + while True: + msg = await input_stream.readline() + await handle_data(msg) +``` +Callbacks are not a natural fit in this model. Viewing the declaration of a +synchronous function, it is not evident how the function gets called or in what +context the code runs. Is it an ISR? Is it called from another thread or core? +Or is it a callback running in a `uasyncio` context? You cannot tell without +trawling the code. By contrast, a routine such as the above example is a self +contained process whose context and intended behaviour are evident. + +The following steps can facilitate the use of asynchronous functions: + 1. Design device drivers to expose one or more bound `Event` objects. + Alternatively design the driver interface to be that of an `Event`. + 2. Design program logic to operate on objects with an `Event` interface. + +The first simplifies the design of drivers and standardises their interface. +Users only need to know the names of the bound `Event` instances. By contast +there is no standard way to specify callbacks, to define the passing of +callback arguments or to define how to retrieve their return values. ###### [Contents](./EVENTS.md#0-contents) From a2df841c7a4bd3703ae6d448e61dd5d20614d519 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 30 Sep 2022 09:57:52 +0100 Subject: [PATCH 171/305] EVENTS.md Minor corrections. --- v3/docs/EVENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 70b9312..7409c55 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -111,7 +111,7 @@ callbacks: are waiting on `.wait()`, all will resume running. 2. Alternatively the `Event` may be cleared later. The timing of clearing the `Event` determines its behaviour if, at the time when the `Event` is set, a - task waiting on the `await event.wait()` statement has not yet reached it. If + task with an `await event.wait()` statement has not yet reached it. If execution reaches `.wait()` before the `Event` is cleared, it will not pause. If the `Event` is cleared, it will pause until it is set again. @@ -132,7 +132,7 @@ ELO examples are: |:---------------------|:----:|:-----:|:---:|:------------------| | [Event][4m] | Y | Y | Y | | | [ThreadSafeFlag][3m] | Y | N | Y | Self-clearing | -| [Message][7m] | Y | N | Y | Subclass of above | +| [Message][7m] | Y | Y | Y | Subclass of above | | [Delay_ms][2m] | Y | Y | Y | Self-setting | | [WaitAll](./EVENTS.md#42-waitall) | Y | Y | N | See below | | [WaitAny](./EVENTS.md#41-waitany) | Y | Y | N | | From e572a95cf5ddd9cf5683633d13ecb2d29ee5199f Mon Sep 17 00:00:00 2001 From: sandyscott Date: Tue, 1 Nov 2022 12:32:57 +0000 Subject: [PATCH 172/305] ESwitch: fire close event when _state goes high A state transition from 0 to 1 should fire the close event and vice-versa --- v3/primitives/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/primitives/events.py b/v3/primitives/events.py index 1c48501..4e5dd7e 100644 --- a/v3/primitives/events.py +++ b/v3/primitives/events.py @@ -72,7 +72,7 @@ async def _poll(self, dt): # Poll the button while True: if (s := self._pin() ^ self._lopen) != self._state: # 15μs self._state = s - self._of() if s else self._cf() + self._cf() if s else self._of() await asyncio.sleep_ms(dt) # Wait out bounce def _of(self): From 8bceccf8d7578695b64be2410a85e7d78bf56250 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Fri, 4 Nov 2022 09:35:53 +0000 Subject: [PATCH 173/305] EVENTS.md: Add ESwitch demo code. --- v3/docs/EVENTS.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 7409c55..df4e216 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -359,7 +359,32 @@ Bound objects: 2. `close` An `Event` instance. Set on contact closure. 3. `open` An `Event` instance. Set on contact open. -Application code is responsible for clearing the `Event` instances. +Application code is responsible for clearing the `Event` instances. +Usage example: +```python +import uasyncio as asyncio +from machine import Pin +from primitives import ESwitch +es = ESwitch(Pin("Y1", Pin.IN, Pin.PULL_UP)) + +async def closure(): + while True: + es.close.clear() + await es.close.wait() + print("Closed") + +async def open(): + while True: + es.open.clear() + await es.open.wait() + print("Open") + +async def main(): + asyncio.create_task(open()) + await closure() + +asyncio.run(main()) +``` ###### [Contents](./EVENTS.md#0-contents) From 423154de2fb56e95fab5f2e416ddbff0665d130a Mon Sep 17 00:00:00 2001 From: peterhinch Date: Fri, 4 Nov 2022 11:53:57 +0000 Subject: [PATCH 174/305] primitives/tests/event_test.py: Improve ESwitch test. --- v3/primitives/tests/event_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/v3/primitives/tests/event_test.py b/v3/primitives/tests/event_test.py index 8a2aca5..23989e3 100644 --- a/v3/primitives/tests/event_test.py +++ b/v3/primitives/tests/event_test.py @@ -152,7 +152,9 @@ async def stest(sw, verbose): tasks = [] for n, evt in enumerate(events): tasks.append(asyncio.create_task(monitor(evt, 1 << 3 * n, verbose))) - await pulse(1000) + asyncio.create_task(pulse(2000)) + await asyncio.sleep(1) + expect(val, 0x08) await asyncio.sleep(4) # Wait for any spurious events verbose and print("Switch close and open", hex(val)) expect(val, 0x09) From 893a374d44fc40ef55ea3573bd8c546de6fac9a7 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 9 Nov 2022 17:20:25 +0000 Subject: [PATCH 175/305] Tutorial: Add note re Queue not being thread safe. --- v3/docs/TUTORIAL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 12272b2..447f986 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1001,6 +1001,7 @@ async def queue_go(delay): asyncio.run(queue_go(4)) ``` +In common with CPython's `asyncio.Queue` this class is not thread safe. ###### [Contents](./TUTORIAL.md#contents) From 524ccae47c6cf2e76c1cb84d93159c6249534cbe Mon Sep 17 00:00:00 2001 From: peterhinch Date: Thu, 10 Nov 2022 08:21:02 +0000 Subject: [PATCH 176/305] Tutorial: correct errors in WaitAll and WaitAny examples. --- v3/docs/TUTORIAL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 447f986..7e3940b 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -715,7 +715,7 @@ from primitives import WaitAny evt1 = Event() evt2 = Event() # Launch tasks that might trigger these events -evt = await WaitAny((evt1, evt2)) +evt = await WaitAny((evt1, evt2)).wait() # One or other was triggered if evt is evt1: evt1.clear() @@ -730,7 +730,7 @@ until all passed `Event`s have been set: from primitives import WaitAll evt1 = Event() evt2 = Event() -wa = WaitAll((evt1, evt2)) # +wa = WaitAll((evt1, evt2)).wait() # Launch tasks that might trigger these events await wa # Both were triggered From 1a6b21969d15c434d0b229bfc0b76ccb6e71e12c Mon Sep 17 00:00:00 2001 From: peterhinch Date: Tue, 15 Nov 2022 13:43:06 +0000 Subject: [PATCH 177/305] Add ringbuf_queue primiive. --- v3/docs/EVENTS.md | 47 ++++++++++++++++ v3/docs/TUTORIAL.md | 15 ++++-- v3/primitives/__init__.py | 1 + v3/primitives/ringbuf_queue.py | 68 +++++++++++++++++++++++ v3/primitives/tests/asyntest.py | 96 ++++++++++++++++++++++++++++++--- 5 files changed, 214 insertions(+), 13 deletions(-) create mode 100644 v3/primitives/ringbuf_queue.py diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index df4e216..3e23b07 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -25,6 +25,7 @@ This document assumes familiarity with `uasyncio`. See [official docs](http://do 6.2 [EButton](./EVENTS.md#62-ebutton) Debounced pushbutton with double and long press events      6.2.1 [The suppress constructor argument](./EVENTS.md#621-the-suppress-constructor-argument)      6.2.2 [The sense constructor argument](./EVENTS.md#622-the-sense-constructor-argument) + 7. [Ringbuf queue](./EVENTS.md#7-ringbuf-queue) A MicroPython optimised queue primitive. [Appendix 1 Polling](./EVENTS.md#100-appendix-1-polling) # 1. An alternative to callbacks in uasyncio code @@ -478,6 +479,52 @@ determine whether the button is closed or open. ###### [Contents](./EVENTS.md#0-contents) +# 7. Ringbuf Queue + +The API of the `Queue` aims for CPython compatibility. This is at some cost to +efficiency. As the name suggests, the `RingbufQueue` class uses a pre-allocated +circular buffer which may be of any mutable type supporting the buffer protocol +e.g. `list`, `array` or `bytearray`. + +Attributes of `RingbufQueue`: + 1. It is of fixed size, `Queue` can grow to arbitrary size. + 2. It uses pre-allocated buffers of various types (`Queue` uses a `list`). + 3. It is an asynchronous iterator allowing retrieval with `async for`. + 4. It has an "overwrite oldest data" synchronous write mode. + +Constructor mandatory arg: + * `buf` Buffer for the queue, e.g. list `[0 for _ in range(20)]` or array. A + buffer of size `N` can hold a maximum of `N-1` items. + +Synchronous methods (immediate return): + * `qsize` No arg. Returns the number of items in the queue. + * `empty` No arg. Returns `True` if the queue is empty. + * `full` No arg. Returns `True` if the queue is full. + * `get_nowait` No arg. Returns an object from the queue. Raises an exception + if the queue is empty. + * `put_nowait` Arg: the object to put on the queue. Raises an exception if the + queue is full. If the calling code ignores the exception the oldest item in + the queue will be overwritten. In some applications this can be of use. + +Asynchronous methods: + * `put` Arg: the object to put on the queue. If the queue is full, it will + block until space is available. + +Retrieving items from the queue: + +The `RingbufQueue` is an asynchronous iterator. Results are retrieved using +`async for`: +```python +async def handle_queued_data(q): + async for obj in q: + await asyncio.sleep(0) # See below + # Process obj +``` +The `sleep` is necessary if you have multiple tasks waiting on the queue, +otherwise one task hogs all the data. + +###### [Contents](./EVENTS.md#0-contents) + # 100 Appendix 1 Polling The primitives or drivers referenced here do not use polling with the following diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 7e3940b..019b92f 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -947,8 +947,10 @@ is raised. ## 3.5 Queue -This is currently an unofficial implementation. Its API is as per CPython -asyncio. +This is currently an unofficial implementation. Its API is a subset of that of +CPython's `asyncio.Queue`. Like `asyncio.Queue` this class is not thread safe. +A queue class optimised for MicroPython is presented in +[Ringbuf queue](./EVENTS.md#7-ringbuf-queue). The `Queue` class provides a means of synchronising producer and consumer tasks: the producer puts data items onto the queue with the consumer removing @@ -1001,14 +1003,15 @@ async def queue_go(delay): asyncio.run(queue_go(4)) ``` -In common with CPython's `asyncio.Queue` this class is not thread safe. ###### [Contents](./TUTORIAL.md#contents) ## 3.6 ThreadSafeFlag This requires firmware V1.15 or later. -See also [Interfacing uasyncio to interrupts](./INTERRUPTS.md). +See also [Interfacing uasyncio to interrupts](./INTERRUPTS.md). Because of +[this issue](https://github.com/micropython/micropython/issues/7965) the +`ThreadSafeFlag` class does not work under the Unix build. This official class provides an efficient means of synchronising a task with a truly asynchronous event such as a hardware interrupt service routine or code @@ -1313,7 +1316,9 @@ finally: ## 3.9 Message -This requires firmware V1.15 or later. +This requires firmware V1.15 or later. Note that because of +[this issue](https://github.com/micropython/micropython/issues/7965) the +`Message` class does not work under the Unix build. This is an unofficial primitive with no counterpart in CPython asyncio. It uses [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) to provide an object similar diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index fa6b163..94c57fe 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -47,6 +47,7 @@ def _handle_exception(loop, context): "WaitAny": "events", "ESwitch": "events", "EButton": "events", + "RingbufQueue": "ringbuf_queue", } # Copied from uasyncio.__init__.py diff --git a/v3/primitives/ringbuf_queue.py b/v3/primitives/ringbuf_queue.py new file mode 100644 index 0000000..17c052d --- /dev/null +++ b/v3/primitives/ringbuf_queue.py @@ -0,0 +1,68 @@ +# ringbuf_queue.py Provides RingbufQueue class +# API differs from CPython +# Uses pre-allocated ring buffer: can use list or array +# Asynchronous iterator allowing consumer to use async for +# put_nowait QueueFull exception can be ignored allowing oldest data to be discarded. + +import uasyncio as asyncio + +# Exception raised by get_nowait(). +class QueueEmpty(Exception): + pass + +# Exception raised by put_nowait(). +class QueueFull(Exception): + pass + +class RingbufQueue: # MicroPython optimised + def __init__(self, buf): + self._q = buf + self._size = len(buf) + self._wi = 0 + self._ri = 0 + self._evput = asyncio.Event() # Triggered by put, tested by get + self._evget = asyncio.Event() # Triggered by get, tested by put + + def full(self): + return ((self._wi + 1) % self._size) == self._ri + + def empty(self): + return self._ri == self._wi + + def qsize(self): + return (self._wi - self._ri) % self._size + + def get_nowait(self): # Remove and return an item from the queue. + # Return an item if one is immediately available, else raise QueueEmpty. + if self.empty(): + raise QueueEmpty() + r = self._q[self._ri] + self._ri = (self._ri + 1) % self._size + return r + + def put_nowait(self, v): + self._q[self._wi] = v + self._evput.set() # Schedule any tasks waiting on get + self._evput.clear() + self._wi = (self._wi + 1) % self._size + if self._wi == self._ri: # Would indicate empty + self._ri = (self._ri + 1) % self._size # Discard a message + raise QueueFull # Caller can ignore if overwrites are OK + + async def put(self, val): # Usage: await queue.put(item) + while self.full(): # Queue full + await self._evget.wait() # May be >1 task waiting on ._evget + # Task(s) waiting to get from queue, schedule first Task + self.put_nowait(val) + + def __aiter__(self): + return self + + async def __anext__(self): + while self.empty(): # Empty. May be more than one task waiting on ._evput + await self._evput.wait() + r = self._q[self._ri] + self._ri = (self._ri + 1) % self._size + self._evget.set() # Schedule all tasks waiting on ._evget + self._evget.clear() + return r diff --git a/v3/primitives/tests/asyntest.py b/v3/primitives/tests/asyntest.py index 9d07289..f8f0acd 100644 --- a/v3/primitives/tests/asyntest.py +++ b/v3/primitives/tests/asyntest.py @@ -13,11 +13,10 @@ import uasyncio as asyncio except ImportError: import asyncio +import sys +unix = "linux" in sys.implementation._machine -from primitives.message import Message -from primitives.barrier import Barrier -from primitives.semaphore import Semaphore, BoundedSemaphore -from primitives.condition import Condition +from primitives import Message, Barrier, Semaphore, BoundedSemaphore, Condition, Queue, RingbufQueue def print_tests(): st = '''Available functions: @@ -30,6 +29,7 @@ def print_tests(): test(6) Test BoundedSemaphore. test(7) Test the Condition class. test(8) Test the Queue class. +test(9) Test the RingbufQueue class. ''' print('\x1b[32m') print(st) @@ -83,6 +83,9 @@ async def ack_coro(delay): print("Time to die...") def ack_test(): + if unix: + print("Message class is incompatible with Unix build.") + return printexp('''message was set message_wait 1 got message with value 0 message_wait 2 got message with value 0 @@ -142,6 +145,9 @@ async def run_message_test(): print('Tasks complete') def msg_test(): + if unix: + print("Message class is incompatible with Unix build.") + return printexp('''Test Lock class Test Message class waiting for message @@ -389,8 +395,6 @@ def condition_test(): # ************ Queue test ************ -from primitives.queue import Queue - async def slow_process(): await asyncio.sleep(2) return 42 @@ -462,7 +466,7 @@ async def queue_go(): getter(q) ) print('Queue tests complete') - print("I've seen starships burn off the shoulder of Orion...") + print("I've seen attack ships burn off the shoulder of Orion...") print("Time to die...") def queue_test(): @@ -476,12 +480,86 @@ def queue_test(): Queue tests complete -I've seen starships burn off the shoulder of Orion... +I've seen attack ships burn off the shoulder of Orion... Time to die... ''', 20) asyncio.run(queue_go()) +# ************ RingbufQueue test ************ + +async def qread(q, lst, twr): + async for item in q: + lst.append(item) + await asyncio.sleep_ms(twr) + +async def read(q, t, twr=0): + lst = [] + try: + await asyncio.wait_for(qread(q, lst, twr), t) + except asyncio.TimeoutError: + pass + return lst + +async def put_list(q, lst, twp=0): + for item in lst: + await q.put(item) + await asyncio.sleep_ms(twp) + +async def rbq_go(): + q = RingbufQueue([0 for _ in range(10)]) # 10 elements + pl = [n for n in range(15)] + print("Read waits on slow write.") + asyncio.create_task(put_list(q, pl, 100)) + rl = await read(q, 2) + assert pl == rl + print('done') + print("Write waits on slow read.") + asyncio.create_task(put_list(q, pl)) + rl = await read(q, 2, 100) + assert pl == rl + print('done') + print("Testing full, empty and qsize methods.") + assert q.empty() + assert q.qsize() == 0 + assert not q.full() + await put_list(q, (1,2,3)) + assert not q.empty() + assert q.qsize() == 3 + assert not q.full() + print("Done") + print("Testing put_nowait and overruns.") + nfail = 0 + for x in range(4, 15): + try: + q.put_nowait(x) + except: + nfail += 1 + assert nfail == 5 + assert q.full() + rl = await read(q, 2) + assert rl == [6, 7, 8, 9, 10, 11, 12, 13, 14] + print("Tests complete.") + print("I've seen attack ships burn off the shoulder of Orion...") + print("Time to die...") + +def rbq_test(): + printexp('''Running (runtime = 6s): +Read waits on slow write. +done +Write waits on slow read. +done +Testing full, empty and qsize methods. +Done +Testing put_nowait and overruns. +Tests complete. +I've seen attack ships burn off the shoulder of Orion... +Time to die... + +''', 20) + asyncio.run(rbq_go()) + +# ************ ************ def test(n): try: if n == 1: @@ -500,6 +578,8 @@ def test(n): condition_test() # Test the Condition class. elif n == 8: queue_test() # Test the Queue class. + elif n == 9: + rbq_test() # Test the RingbufQueue class. except KeyboardInterrupt: print('Interrupted') finally: From c2f9259d04fa89fe33d95eb89efdc2afcdb092a3 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 16 Nov 2022 12:13:03 +0000 Subject: [PATCH 178/305] ringbuf_queue raises IndexError. Fix gather error in tutorial. --- v3/docs/EVENTS.md | 15 +++++++++++++-- v3/docs/TUTORIAL.md | 25 ++++++++++++++++++++----- v3/primitives/ringbuf_queue.py | 11 ++--------- v3/primitives/tests/asyntest.py | 2 +- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 3e23b07..9cf1eac 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -500,9 +500,9 @@ Synchronous methods (immediate return): * `qsize` No arg. Returns the number of items in the queue. * `empty` No arg. Returns `True` if the queue is empty. * `full` No arg. Returns `True` if the queue is full. - * `get_nowait` No arg. Returns an object from the queue. Raises an exception + * `get_nowait` No arg. Returns an object from the queue. Raises `IndexError` if the queue is empty. - * `put_nowait` Arg: the object to put on the queue. Raises an exception if the + * `put_nowait` Arg: the object to put on the queue. Raises `IndexError` if the queue is full. If the calling code ignores the exception the oldest item in the queue will be overwritten. In some applications this can be of use. @@ -523,6 +523,17 @@ async def handle_queued_data(q): The `sleep` is necessary if you have multiple tasks waiting on the queue, otherwise one task hogs all the data. +The following illustrates putting items onto a `RingbufQueue` where the queue is +not allowed to stall: where it becomes full, new items overwrite the oldest ones +in the queue: +```python +def add_item(q, data): +try: + q.put_nowait(data) +except IndexError: + pass +``` + ###### [Contents](./EVENTS.md#0-contents) # 100 Appendix 1 Polling diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 019b92f..779da7f 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -755,7 +755,7 @@ cancellation or timeout. It returns a list of the return values of each task. Its call signature is ```python -res = await asyncio.gather(*tasks, return_exceptions=True) +res = await asyncio.gather(*tasks, return_exceptions=False) ``` The keyword-only boolean arg `return_exceptions` determines the behaviour in the event of a cancellation or timeout of tasks. If `False`, the `gather` @@ -1095,10 +1095,25 @@ serviced the device, the ISR flags an asynchronous routine, typically processing received data. The fact that only one task may wait on a `ThreadSafeFlag` may be addressed by -having the task that waits on the `ThreadSafeFlag` set an `Event`. Multiple -tasks may wait on that `Event`. As an alternative to explicitly coding this, -the [Message class](./TUTORIAL.md#39-message) uses this approach to provide an -`Event`-like object which can be triggered from an ISR. +having a task that waits on the `ThreadSafeFlag` set an `Event` as in the +following: +```python +class ThreadSafeEvent(asyncio.Event): + def __init__(self): + super().__init__() + self._tsf = asyncio.ThreadSafeFlag() + asyncio.create_task(self._run()) + + async def _run(self): + while True: + await self._tsf.wait() + super().set() + + def set(self): + self._tsf.set() +``` +An instance may be set by a hard ISR or from another thread/core. It must +explicitly be cleared. Multiple tasks may wait on it. ###### [Contents](./TUTORIAL.md#contents) diff --git a/v3/primitives/ringbuf_queue.py b/v3/primitives/ringbuf_queue.py index 17c052d..4c4b62d 100644 --- a/v3/primitives/ringbuf_queue.py +++ b/v3/primitives/ringbuf_queue.py @@ -6,13 +6,6 @@ import uasyncio as asyncio -# Exception raised by get_nowait(). -class QueueEmpty(Exception): - pass - -# Exception raised by put_nowait(). -class QueueFull(Exception): - pass class RingbufQueue: # MicroPython optimised def __init__(self, buf): @@ -35,7 +28,7 @@ def qsize(self): def get_nowait(self): # Remove and return an item from the queue. # Return an item if one is immediately available, else raise QueueEmpty. if self.empty(): - raise QueueEmpty() + raise IndexError r = self._q[self._ri] self._ri = (self._ri + 1) % self._size return r @@ -47,7 +40,7 @@ def put_nowait(self, v): self._wi = (self._wi + 1) % self._size if self._wi == self._ri: # Would indicate empty self._ri = (self._ri + 1) % self._size # Discard a message - raise QueueFull # Caller can ignore if overwrites are OK + raise IndexError # Caller can ignore if overwrites are OK async def put(self, val): # Usage: await queue.put(item) while self.full(): # Queue full diff --git a/v3/primitives/tests/asyntest.py b/v3/primitives/tests/asyntest.py index f8f0acd..313efd5 100644 --- a/v3/primitives/tests/asyntest.py +++ b/v3/primitives/tests/asyntest.py @@ -533,7 +533,7 @@ async def rbq_go(): for x in range(4, 15): try: q.put_nowait(x) - except: + except IndexError: nfail += 1 assert nfail == 5 assert q.full() From 64ef6e5b56281c1d511b220acc314327974b4468 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Fri, 18 Nov 2022 14:25:44 +0000 Subject: [PATCH 179/305] Rewrite Message class. --- v3/docs/TUTORIAL.md | 56 ++++++++++---- v3/primitives/message.py | 63 +++++++++------ v3/primitives/tests/asyntest.py | 133 +++++++++++++++++++++++++------- 3 files changed, 185 insertions(+), 67 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 779da7f..c9441c6 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1008,15 +1008,14 @@ asyncio.run(queue_go(4)) ## 3.6 ThreadSafeFlag -This requires firmware V1.15 or later. See also [Interfacing uasyncio to interrupts](./INTERRUPTS.md). Because of [this issue](https://github.com/micropython/micropython/issues/7965) the `ThreadSafeFlag` class does not work under the Unix build. This official class provides an efficient means of synchronising a task with a truly asynchronous event such as a hardware interrupt service routine or code -running in another thread. It operates in a similar way to `Event` with the -following key differences: +running in another thread or on another core. It operates in a similar way to +`Event` with the following key differences: * It is thread safe: the `set` event may be called from asynchronous code. * It is self-clearing. * Only one task may wait on the flag. @@ -1094,26 +1093,39 @@ hardware device requires the use of an ISR for a μs level response. Having serviced the device, the ISR flags an asynchronous routine, typically processing received data. -The fact that only one task may wait on a `ThreadSafeFlag` may be addressed by -having a task that waits on the `ThreadSafeFlag` set an `Event` as in the -following: +The fact that only one task may wait on a `ThreadSafeFlag` may be addressed as +follows. ```python class ThreadSafeEvent(asyncio.Event): def __init__(self): super().__init__() + self._waiting_on_tsf = False self._tsf = asyncio.ThreadSafeFlag() - asyncio.create_task(self._run()) - - async def _run(self): - while True: - await self._tsf.wait() - super().set() def set(self): self._tsf.set() + + async def _waiter(self): # Runs if 1st task is cancelled + await self._tsf.wait() + super().set() + self._waiting_on_tsf = False + + async def wait(self): + if self._waiting_on_tsf == False: + self._waiting_on_tsf = True + await asyncio.sleep(0) # Ensure other tasks see updated flag + try: + await self._tsf.wait() + super().set() + self._waiting_on_tsf = False + except asyncio.CancelledError: + asyncio.create_task(self._waiter()) + raise # Pass cancellation to calling code + else: + await super().wait() ``` -An instance may be set by a hard ISR or from another thread/core. It must -explicitly be cleared. Multiple tasks may wait on it. +An instance may be set by a hard ISR or from another thread/core. As an `Event` +it can support multiple tasks and must explicitly be cleared. ###### [Contents](./TUTORIAL.md#contents) @@ -1331,9 +1343,8 @@ finally: ## 3.9 Message -This requires firmware V1.15 or later. Note that because of -[this issue](https://github.com/micropython/micropython/issues/7965) the -`Message` class does not work under the Unix build. +Because of [this issue](https://github.com/micropython/micropython/issues/7965) +the `Message` class does not work under the Unix build. This is an unofficial primitive with no counterpart in CPython asyncio. It uses [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) to provide an object similar @@ -1345,6 +1356,7 @@ It is similar to the `Event` class. It differs in that: * `.set()` is capable of being called from a hard or soft interrupt service routine. * It is an awaitable class. + * It can be used in an asynchronous iterator. * The logic of `.clear` differs: it must be called by at least one task which waits on the `Message`. @@ -1421,6 +1433,16 @@ async def main(): asyncio.run(main()) ``` +Receiving messages in an asynchronous iterator: +```python +msg = Message() +asyncio.create_task(send_data(msg)) +async for data in msg: + # process data + msg.clear() +``` +The `Message` class does not have a queue: if the instance is set, then set +again before it is accessed, the first data item will be lost. ## 3.10 Synchronising to hardware diff --git a/v3/primitives/message.py b/v3/primitives/message.py index ffd6d00..174061a 100644 --- a/v3/primitives/message.py +++ b/v3/primitives/message.py @@ -1,54 +1,73 @@ # message.py # Now uses ThreadSafeFlag for efficiency -# Copyright (c) 2018-2021 Peter Hinch +# Copyright (c) 2018-2022 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file # Usage: # from primitives.message import Message - -try: +# See https://github.com/micropython/micropython/issues/7965 for code below +import sys +ok = hasattr(sys.implementation, "_machine") # MicroPython +if ok: + ok = "linux" not in sys.implementation._machine +if ok: import uasyncio as asyncio -except ImportError: - import asyncio +else: + print("Message is MicroPython only, and not on Unix build.") + sys.exit(1) # A coro waiting on a message issues await message # A coro or hard/soft ISR raising the message issues.set(payload) # .clear() should be issued by at least one waiting task and before # next event. -class Message(asyncio.ThreadSafeFlag): - def __init__(self, _=0): # Arg: poll interval. Compatibility with old code. - self._evt = asyncio.Event() - self._data = None # Message - self._state = False # Ensure only one task waits on ThreadSafeFlag - self._is_set = False # For .is_set() +class Message(asyncio.Event): + def __init__(self): super().__init__() + self._waiting_on_tsf = False + self._tsf = asyncio.ThreadSafeFlag() + self._data = None # Message + self._is_set = False def clear(self): # At least one task must call clear when scheduled - self._state = False self._is_set = False + super().clear() def __iter__(self): yield from self.wait() return self._data + async def _waiter(self): # Runs if 1st task is cancelled + await self._tsf.wait() + super().set() + self._waiting_on_tsf = False + async def wait(self): - if self._state: # A task waits on ThreadSafeFlag - await self._evt.wait() # Wait on event - else: # First task to wait - self._state = True - # Ensure other tasks see updated ._state before they wait - await asyncio.sleep_ms(0) - await super().wait() # Wait on ThreadSafeFlag - self._evt.set() - self._evt.clear() + if self._waiting_on_tsf == False: + self._waiting_on_tsf = True + await asyncio.sleep(0) # Ensure other tasks see updated flag + try: + await self._tsf.wait() + super().set() + self._waiting_on_tsf = False + except asyncio.CancelledError: + asyncio.create_task(self._waiter()) + raise # Pass cancellation to calling code + else: + await super().wait() return self._data def set(self, data=None): # Can be called from a hard ISR self._data = data self._is_set = True - super().set() + self._tsf.set() + + def __aiter__(self): + return self + + async def __anext__(self): + return await self def is_set(self): return self._is_set diff --git a/v3/primitives/tests/asyntest.py b/v3/primitives/tests/asyntest.py index 313efd5..e376c67 100644 --- a/v3/primitives/tests/asyntest.py +++ b/v3/primitives/tests/asyntest.py @@ -1,7 +1,7 @@ # asyntest.py Test/demo of the 'micro' Event, Barrier and Semaphore classes # Test/demo of official asyncio library and official Lock class -# Copyright (c) 2017-2020 Peter Hinch +# Copyright (c) 2017-2022 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file # CPython 3.8 compatibility @@ -16,7 +16,11 @@ import sys unix = "linux" in sys.implementation._machine -from primitives import Message, Barrier, Semaphore, BoundedSemaphore, Condition, Queue, RingbufQueue +from primitives import Barrier, Semaphore, BoundedSemaphore, Condition, Queue, RingbufQueue +try: + from primitives import Message +except: + pass def print_tests(): st = '''Available functions: @@ -51,20 +55,22 @@ def printexp(exp, runtime=0): # Demo use of acknowledge message async def message_wait(message, ack_message, n): - await message - print('message_wait {} got message with value {}'.format(n, message.value())) - ack_message.set() + try: + await message + print(f'message_wait {n} got message: {message.value()}') + if ack_message is not None: + ack_message.set() + except asyncio.CancelledError: + print(f"message_wait {n} cancelled") -async def run_ack(): +async def run_ack(n): message = Message() ack1 = Message() ack2 = Message() - count = 0 - while True: - asyncio.create_task(message_wait(message, ack1, 1)) - asyncio.create_task(message_wait(message, ack2, 2)) + for count in range(n): + t0 = asyncio.create_task(message_wait(message, ack1, 1)) + t1 = asyncio.create_task(message_wait(message, ack2, 2)) message.set(count) - count += 1 print('message was set') await ack1 ack1.clear() @@ -75,10 +81,54 @@ async def run_ack(): message.clear() print('Cleared message') await asyncio.sleep(1) + t0.cancel() + t1.cancel() + +async def msg_send(msg, items): + for item in items: + await asyncio.sleep_ms(400) + msg.set(item) + +async def msg_recv(msg): # Receive using asynchronous iterator + async for data in msg: + print("Got", data) + msg.clear() + +async def ack_coro(): + print("Test multiple tasks waiting on a message.") + await run_ack(3) + print() + print("Test asynchronous iterator.") + msg = Message() + asyncio.create_task(msg_send(msg, (1, 2, 3))) + try: + await asyncio.wait_for(msg_recv(msg), 3) + except asyncio.TimeoutError: + pass + await asyncio.sleep(1) + print() + print("Test cancellation of first waiting task.") + t1 = asyncio.create_task(message_wait(msg, None, 1)) + t2 = asyncio.create_task(message_wait(msg, None, 2)) + await asyncio.sleep(1) + t1.cancel() + await asyncio.sleep(1) + print("Setting message") + msg.set("Test message") + await asyncio.sleep(1) # Tasks have ended or been cancelled + msg.clear() + print() + print("Test cancellation of second waiting task.") + t1 = asyncio.create_task(message_wait(msg, None, 1)) + t2 = asyncio.create_task(message_wait(msg, None, 2)) + await asyncio.sleep(1) + t2.cancel() + await asyncio.sleep(1) + print("Setting message") + msg.set("Test message") + await asyncio.sleep(1) + msg.clear() -async def ack_coro(delay): - print('Started ack coro with delay', delay) - await asyncio.sleep(delay) print("I've seen attack ships burn on the shoulder of Orion...") print("Time to die...") @@ -86,28 +136,45 @@ def ack_test(): if unix: print("Message class is incompatible with Unix build.") return - printexp('''message was set -message_wait 1 got message with value 0 -message_wait 2 got message with value 0 + printexp('''Running (runtime = 12s): +Test multiple tasks waiting on a message. +message was set +message_wait 1 got message: 0 +message_wait 2 got message: 0 Cleared ack1 Cleared ack2 Cleared message message was set -message_wait 1 got message with value 1 -message_wait 2 got message with value 1 - -... text omitted ... - -message_wait 1 got message with value 5 -message_wait 2 got message with value 5 +message_wait 1 got message: 1 +message_wait 2 got message: 1 +Cleared ack1 +Cleared ack2 +Cleared message +message was set +message_wait 1 got message: 2 +message_wait 2 got message: 2 Cleared ack1 Cleared ack2 Cleared message + +Test asynchronous iterator. +Got 1 +Got 2 +Got 3 + +Test cancellation of first waiting task. +message_wait 1 cancelled +Setting message +message_wait 2 got message: Test message + +Test cancellation of second waiting task. +message_wait 2 cancelled +Setting message +message_wait 1 got message: Test message I've seen attack ships burn on the shoulder of Orion... Time to die... -''', 10) - asyncio.create_task(run_ack()) - asyncio.run(ack_coro(6)) +''', 12) + asyncio.run(ack_coro()) # ************ Test Lock and Message classes ************ @@ -539,6 +606,15 @@ async def rbq_go(): assert q.full() rl = await read(q, 2) assert rl == [6, 7, 8, 9, 10, 11, 12, 13, 14] + print("Testing get_nowait.") + await q.put(1) + assert q.get_nowait() == 1 + err = 0 + try: + q.get_nowait() + except IndexError: + err = 1 + assert err == 1 print("Tests complete.") print("I've seen attack ships burn off the shoulder of Orion...") print("Time to die...") @@ -552,11 +628,12 @@ def rbq_test(): Testing full, empty and qsize methods. Done Testing put_nowait and overruns. +Testing get_nowait. Tests complete. I've seen attack ships burn off the shoulder of Orion... Time to die... -''', 20) +''', 6) asyncio.run(rbq_go()) # ************ ************ From 500906b1511f400646f473b0cfd19bb86d052184 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sun, 20 Nov 2022 11:08:02 +0000 Subject: [PATCH 180/305] Fix RingbufQueue bug. Add ThreadSafeQueue. --- v3/docs/EVENTS.md | 79 +++++++++++++++++++++++++++++++ v3/primitives/__init__.py | 1 + v3/primitives/ringbuf_queue.py | 6 +++ v3/primitives/threadsafe_queue.py | 69 +++++++++++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 v3/primitives/threadsafe_queue.py diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 9cf1eac..5a190b0 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -534,6 +534,85 @@ except IndexError: pass ``` +# 8. Threadsafe Queue + +This queue is designed to interface between one or more `uasyncio` tasks and a +single thread running in a different context. This can be an interrupt service +routine (ISR) or code running in a different thread or on a different core. + +Any Python object may be placed on a `ThreadSafeQueue`. If bi-directional +communication is required between the two contexts, two `ThreadSafeQueue` +instances are required. + +Attributes of `ThreadSafeQueue`: + 1. It is of fixed size defined on instantiation. + 2. It uses pre-allocated buffers of various types (`Queue` uses a `list`). + 3. It is an asynchronous iterator allowing retrieval with `async for`. + 4. It provides synchronous "put" and "get" methods. If the queue becomes full + (put) or empty (get), behaviour is user definable. The method either blocks or + raises an `IndexError`. + +Constructor mandatory arg: + * `buf` Buffer for the queue, e.g. list `[0 for _ in range(20)]` or array. A + buffer of size `N` can hold a maximum of `N-1` items. + +Synchronous methods (immediate return): + * `qsize` No arg. Returns the number of items in the queue. + * `empty` No arg. Returns `True` if the queue is empty. + * `full` No arg. Returns `True` if the queue is full. + * `get_sync` Arg `block=False`. Returns an object from the queue. Raises + `IndexError` if the queue is empty, unless `block==True` in which case the + method blocks until the `uasyncio` tasks put an item on the queue. + * `put_sync` Args: the object to put on the queue, `block=False`. Raises + `IndexError` if the queue is full unless `block==True` in which case the + method blocks until the `uasyncio` tasks remove an item from the queue. + +Asynchronous methods: + * `put` Arg: the object to put on the queue. If the queue is full, it will + block until space is available. + +In use as a data consumer the `uasyncio` code will use `async for` to retrieve +items from the queue. If it is a data provider it will use `put` to place +objects on the queue. + +Data consumer: +```python +async def handle_queued_data(q): + async for obj in q: + await asyncio.sleep(0) # See below + # Process obj +``` +The `sleep` is necessary if you have multiple tasks waiting on the queue, +otherwise one task hogs all the data. + +Data provider: +```python +async def feed_queue(q): + while True: + data = await data_source() + await q.put(data) +``` +The alternate thread will use synchronous methods. + +Data provider (throw if full): +```python +while True: + data = data_source() + try: + q.put_sync(data) + except IndexError: + # Queue is full +``` +Data consumer (block while empty): +```python +while True: + data = q.get(block=True) # May take a while if the uasyncio side is slow + process(data) # Do something with it +``` +Note that where the alternate thread is an ISR it is very bad practice to allow +blocking. The application should be designed in such a way that the full/empty +case does not occur. + ###### [Contents](./EVENTS.md#0-contents) # 100 Appendix 1 Polling diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index 94c57fe..d8a339e 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -48,6 +48,7 @@ def _handle_exception(loop, context): "ESwitch": "events", "EButton": "events", "RingbufQueue": "ringbuf_queue", + "ThreadSafeQueue": "threadsafe_queue", } # Copied from uasyncio.__init__.py diff --git a/v3/primitives/ringbuf_queue.py b/v3/primitives/ringbuf_queue.py index 4c4b62d..be44c2a 100644 --- a/v3/primitives/ringbuf_queue.py +++ b/v3/primitives/ringbuf_queue.py @@ -1,4 +1,8 @@ # ringbuf_queue.py Provides RingbufQueue class + +# Copyright (c) 2022 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + # API differs from CPython # Uses pre-allocated ring buffer: can use list or array # Asynchronous iterator allowing consumer to use async for @@ -31,6 +35,8 @@ def get_nowait(self): # Remove and return an item from the queue. raise IndexError r = self._q[self._ri] self._ri = (self._ri + 1) % self._size + self._evget.set() # Schedule all tasks waiting on ._evget + self._evget.clear() return r def put_nowait(self, v): diff --git a/v3/primitives/threadsafe_queue.py b/v3/primitives/threadsafe_queue.py new file mode 100644 index 0000000..2fc1d88 --- /dev/null +++ b/v3/primitives/threadsafe_queue.py @@ -0,0 +1,69 @@ +# threadsafe_queue.py Provides ThreadsafeQueue class + +# Copyright (c) 2022 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +# Uses pre-allocated ring buffer: can use list or array +# Asynchronous iterator allowing consumer to use async for +# put_nowait QueueFull exception can be ignored allowing oldest data to be discarded. + +import uasyncio as asyncio + + +class ThreadSafeQueue: # MicroPython optimised + def __init__(self, buf): + self._q = buf + self._size = len(buf) + self._wi = 0 + self._ri = 0 + self._evput = asyncio.ThreadSafeFlag() # Triggered by put, tested by get + self._evget = asyncio.ThreadSafeFlag() # Triggered by get, tested by put + + def full(self): + return ((self._wi + 1) % self._size) == self._ri + + def empty(self): + return self._ri == self._wi + + def qsize(self): + return (self._wi - self._ri) % self._size + + def get_sync(self, block=False): # Remove and return an item from the queue. + # Return an item if one is immediately available, else raise QueueEmpty. + if block: + while self.empty(): + pass + else: + if self.empty(): + raise IndexError + r = self._q[self._ri] + self._ri = (self._ri + 1) % self._size + self._evget.set() + return r + + def put_sync(self, v, block=False): + self._q[self._wi] = v + self._evput.set() # Schedule any tasks waiting on get + if block: + while ((self._wi + 1) % self._size) == self._ri: + pass # can't bump ._wi until an item is removed + elif ((self._wi + 1) % self._size) == self._ri: + raise IndexError + self._wi = (self._wi + 1) % self._size + + async def put(self, val): # Usage: await queue.put(item) + while self.full(): # Queue full + await self._evget.wait() # May be >1 task waiting on ._evget + # Task(s) waiting to get from queue, schedule first Task + self.put_sync(val) + + def __aiter__(self): + return self + + async def __anext__(self): + while self.empty(): # Empty. May be more than one task waiting on ._evput + await self._evput.wait() + r = self._q[self._ri] + self._ri = (self._ri + 1) % self._size + self._evget.set() # Schedule all tasks waiting on ._evget + return r From 58d5a161617201b885487cd31b65fdedf3699ca8 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sun, 20 Nov 2022 12:35:52 +0000 Subject: [PATCH 181/305] Code improvements to ThreadSafeQueue. Fix docs and code comments. --- v3/docs/EVENTS.md | 15 +++++++-------- v3/primitives/threadsafe_queue.py | 28 +++++++++++----------------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 5a190b0..ad16f56 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -536,9 +536,9 @@ except IndexError: # 8. Threadsafe Queue -This queue is designed to interface between one or more `uasyncio` tasks and a -single thread running in a different context. This can be an interrupt service -routine (ISR) or code running in a different thread or on a different core. +This queue is designed to interface between one `uasyncio` task and a single +thread running in a different context. This can be an interrupt service routine +(ISR), code running in a different thread or code on a different core. Any Python object may be placed on a `ThreadSafeQueue`. If bi-directional communication is required between the two contexts, two `ThreadSafeQueue` @@ -556,7 +556,7 @@ Constructor mandatory arg: * `buf` Buffer for the queue, e.g. list `[0 for _ in range(20)]` or array. A buffer of size `N` can hold a maximum of `N-1` items. -Synchronous methods (immediate return): +Synchronous methods. * `qsize` No arg. Returns the number of items in the queue. * `empty` No arg. Returns `True` if the queue is empty. * `full` No arg. Returns `True` if the queue is full. @@ -567,6 +567,9 @@ Synchronous methods (immediate return): `IndexError` if the queue is full unless `block==True` in which case the method blocks until the `uasyncio` tasks remove an item from the queue. +The blocking methods should not be used in the `uasyncio` context, because by +blocking they will lock up the scheduler. + Asynchronous methods: * `put` Arg: the object to put on the queue. If the queue is full, it will block until space is available. @@ -579,12 +582,8 @@ Data consumer: ```python async def handle_queued_data(q): async for obj in q: - await asyncio.sleep(0) # See below # Process obj ``` -The `sleep` is necessary if you have multiple tasks waiting on the queue, -otherwise one task hogs all the data. - Data provider: ```python async def feed_queue(q): diff --git a/v3/primitives/threadsafe_queue.py b/v3/primitives/threadsafe_queue.py index 2fc1d88..b97c657 100644 --- a/v3/primitives/threadsafe_queue.py +++ b/v3/primitives/threadsafe_queue.py @@ -5,7 +5,6 @@ # Uses pre-allocated ring buffer: can use list or array # Asynchronous iterator allowing consumer to use async for -# put_nowait QueueFull exception can be ignored allowing oldest data to be discarded. import uasyncio as asyncio @@ -29,13 +28,10 @@ def qsize(self): return (self._wi - self._ri) % self._size def get_sync(self, block=False): # Remove and return an item from the queue. - # Return an item if one is immediately available, else raise QueueEmpty. - if block: - while self.empty(): - pass - else: - if self.empty(): - raise IndexError + if not block and self.empty(): + raise IndexError # Not allowed to block + while self.empty(): # Block until an item appears + pass r = self._q[self._ri] self._ri = (self._ri + 1) % self._size self._evget.set() @@ -43,27 +39,25 @@ def get_sync(self, block=False): # Remove and return an item from the queue. def put_sync(self, v, block=False): self._q[self._wi] = v - self._evput.set() # Schedule any tasks waiting on get - if block: - while ((self._wi + 1) % self._size) == self._ri: - pass # can't bump ._wi until an item is removed - elif ((self._wi + 1) % self._size) == self._ri: + self._evput.set() # Schedule task waiting on get + if not block and self.full(): raise IndexError + while self.full(): + pass # can't bump ._wi until an item is removed self._wi = (self._wi + 1) % self._size async def put(self, val): # Usage: await queue.put(item) while self.full(): # Queue full - await self._evget.wait() # May be >1 task waiting on ._evget - # Task(s) waiting to get from queue, schedule first Task + await self._evget.wait() self.put_sync(val) def __aiter__(self): return self async def __anext__(self): - while self.empty(): # Empty. May be more than one task waiting on ._evput + while self.empty(): await self._evput.wait() r = self._q[self._ri] self._ri = (self._ri + 1) % self._size - self._evget.set() # Schedule all tasks waiting on ._evget + self._evget.set() # Schedule task waiting on ._evget return r From 4549dc4964dee697e29df536c8fe039acb331e62 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sun, 20 Nov 2022 17:24:32 +0000 Subject: [PATCH 182/305] Create threading directory and THREADING.md --- v3/docs/EVENTS.md | 78 -------- v3/docs/THREADING.md | 181 ++++++++++++++++++ v3/primitives/__init__.py | 1 - v3/threadsafe/__init__.py | 26 +++ v3/threadsafe/threadsafe_event.py | 36 ++++ .../threadsafe_queue.py | 0 6 files changed, 243 insertions(+), 79 deletions(-) create mode 100644 v3/docs/THREADING.md create mode 100644 v3/threadsafe/__init__.py create mode 100644 v3/threadsafe/threadsafe_event.py rename v3/{primitives => threadsafe}/threadsafe_queue.py (100%) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index ad16f56..9cf1eac 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -534,84 +534,6 @@ except IndexError: pass ``` -# 8. Threadsafe Queue - -This queue is designed to interface between one `uasyncio` task and a single -thread running in a different context. This can be an interrupt service routine -(ISR), code running in a different thread or code on a different core. - -Any Python object may be placed on a `ThreadSafeQueue`. If bi-directional -communication is required between the two contexts, two `ThreadSafeQueue` -instances are required. - -Attributes of `ThreadSafeQueue`: - 1. It is of fixed size defined on instantiation. - 2. It uses pre-allocated buffers of various types (`Queue` uses a `list`). - 3. It is an asynchronous iterator allowing retrieval with `async for`. - 4. It provides synchronous "put" and "get" methods. If the queue becomes full - (put) or empty (get), behaviour is user definable. The method either blocks or - raises an `IndexError`. - -Constructor mandatory arg: - * `buf` Buffer for the queue, e.g. list `[0 for _ in range(20)]` or array. A - buffer of size `N` can hold a maximum of `N-1` items. - -Synchronous methods. - * `qsize` No arg. Returns the number of items in the queue. - * `empty` No arg. Returns `True` if the queue is empty. - * `full` No arg. Returns `True` if the queue is full. - * `get_sync` Arg `block=False`. Returns an object from the queue. Raises - `IndexError` if the queue is empty, unless `block==True` in which case the - method blocks until the `uasyncio` tasks put an item on the queue. - * `put_sync` Args: the object to put on the queue, `block=False`. Raises - `IndexError` if the queue is full unless `block==True` in which case the - method blocks until the `uasyncio` tasks remove an item from the queue. - -The blocking methods should not be used in the `uasyncio` context, because by -blocking they will lock up the scheduler. - -Asynchronous methods: - * `put` Arg: the object to put on the queue. If the queue is full, it will - block until space is available. - -In use as a data consumer the `uasyncio` code will use `async for` to retrieve -items from the queue. If it is a data provider it will use `put` to place -objects on the queue. - -Data consumer: -```python -async def handle_queued_data(q): - async for obj in q: - # Process obj -``` -Data provider: -```python -async def feed_queue(q): - while True: - data = await data_source() - await q.put(data) -``` -The alternate thread will use synchronous methods. - -Data provider (throw if full): -```python -while True: - data = data_source() - try: - q.put_sync(data) - except IndexError: - # Queue is full -``` -Data consumer (block while empty): -```python -while True: - data = q.get(block=True) # May take a while if the uasyncio side is slow - process(data) # Do something with it -``` -Note that where the alternate thread is an ISR it is very bad practice to allow -blocking. The application should be designed in such a way that the full/empty -case does not occur. - ###### [Contents](./EVENTS.md#0-contents) # 100 Appendix 1 Polling diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md new file mode 100644 index 0000000..ce5d190 --- /dev/null +++ b/v3/docs/THREADING.md @@ -0,0 +1,181 @@ +# Thread safe classes + +These provide an interface between `uasyncio` tasks and code running in a +different context. Supported contexts are: + 1. An interrupt service routine (ISR). + 2. Another thread running on the same core. + 3. Code running on a different core (currently only supported on RP2). + +The first two cases are relatively straightforward because both contexts share +a common bytecode interpreter and GIL. There is a guarantee that even a hard +MicroPython (MP) ISR will not interrupt execution of a line of Python code. + +This is not the case where the threads run on different cores, where there is +no synchronisation between the streams of machine code. If the two threads +concurrently modify a shared Python object, there is no guarantee that +corruption will not occur. + +# 2. Threadsafe Event + +The `ThreadsafeFlag` has a limitation in that only a single task can wait on +it. The `ThreadSafeEvent` overcomes this. It is subclassed from `Event` and +presents the same interface. The `set` method may be called from an ISR or from +code running on another core. Any number of tasks may wait on it. + +The following Pyboard-specific code demos its use in a hard ISR: +```python +import uasyncio as asyncio +from threadsafe import ThreadSafeEvent +from pyb import Timer + +async def waiter(n, evt): + try: + await evt.wait() + print(f"Waiter {n} got event") + except asyncio.CancelledError: + print(f"Waiter {n} cancelled") + +async def can(task): + await asyncio.sleep_ms(100) + task.cancel() + +async def main(): + evt = ThreadSafeEvent() + tim = Timer(4, freq=1, callback=lambda t: evt.set()) + nt = 0 + while True: + tasks = [asyncio.create_task(waiter(n + 1, evt)) for n in range(4)] + asyncio.create_task(can(tasks[nt])) + await asyncio.gather(*tasks, return_exceptions=True) + evt.clear() + print("Cleared event") + nt = (nt + 1) % 4 + +asyncio.run(main()) +``` + +# 3. Threadsafe Queue + +This queue is designed to interface between one `uasyncio` task and a single +thread running in a different context. This can be an interrupt service routine +(ISR), code running in a different thread or code on a different core. + +Any Python object may be placed on a `ThreadSafeQueue`. If bi-directional +communication is required between the two contexts, two `ThreadSafeQueue` +instances are required. + +Attributes of `ThreadSafeQueue`: + 1. It is of fixed size defined on instantiation. + 2. It uses pre-allocated buffers of various types (`Queue` uses a `list`). + 3. It is an asynchronous iterator allowing retrieval with `async for`. + 4. It provides synchronous "put" and "get" methods. If the queue becomes full + (put) or empty (get), behaviour is user definable. The method either blocks or + raises an `IndexError`. + +Constructor mandatory arg: + * `buf` Buffer for the queue, e.g. list `[0 for _ in range(20)]` or array. A + buffer of size `N` can hold a maximum of `N-1` items. + +Synchronous methods. + * `qsize` No arg. Returns the number of items in the queue. + * `empty` No arg. Returns `True` if the queue is empty. + * `full` No arg. Returns `True` if the queue is full. + * `get_sync` Arg `block=False`. Returns an object from the queue. Raises + `IndexError` if the queue is empty, unless `block==True` in which case the + method blocks until the `uasyncio` tasks put an item on the queue. + * `put_sync` Args: the object to put on the queue, `block=False`. Raises + `IndexError` if the queue is full unless `block==True` in which case the + method blocks until the `uasyncio` tasks remove an item from the queue. + +See the note below re blocking methods. + +Asynchronous methods: + * `put` Arg: the object to put on the queue. If the queue is full, it will + block until space is available. + +In use as a data consumer the `uasyncio` code will use `async for` to retrieve +items from the queue. If it is a data provider it will use `put` to place +objects on the queue. + +Data consumer: +```python +async def handle_queued_data(q): + async for obj in q: + # Process obj +``` +Data provider: +```python +async def feed_queue(q): + while True: + data = await data_source() + await q.put(data) +``` +The alternate thread will use synchronous methods. + +Data provider (throw if full): +```python +while True: + data = data_source() + try: + q.put_sync(data) + except IndexError: + # Queue is full +``` +Data consumer (block while empty): +```python +while True: + data = q.get(block=True) # May take a while if the uasyncio side is slow + process(data) # Do something with it +``` + +## 3.1 Blocking + +The synchronous `get_sync` and `put_sync` methods have blocking modes invoked +by passing `block=True`. Blocking modes are intended to be used in a multi +threaded context. They should not be invoked in a `uasyncio` task, because +blocking locks up the scheduler. Nor should they be used in an ISR where +blocking code can have unpredictable consequences. + +These methods, called with `blocking=False`, produce an immediate return. To +avoid an `IndexError` the user should check for full or empty status before +calling. + +## 3.2 A complete example + +This demonstrates an echo server running on core 2. The `sender` task sends +consecutive integers to the server, which echoes them back on a second queue. +```python +import uasyncio as asyncio +from threadsafe import ThreadSafeQueue +import _thread +from time import sleep_ms + +def core_2(getq, putq): # Run on core 2 + buf = [] + while True: + while getq.qsize(): # Ensure no exception when queue is empty + buf.append(getq.get_sync()) + for x in buf: + putq.put_sync(x, block=True) # Wait if queue fills. + buf.clear() + sleep_ms(30) + +async def sender(to_core2): + x = 0 + while True: + await to_core2.put(x := x + 1) + +async def main(): + to_core2 = ThreadSafeQueue([0 for _ in range(10)]) + from_core2 = ThreadSafeQueue([0 for _ in range(10)]) + _thread.start_new_thread(core_2, (to_core2, from_core2)) + asyncio.create_task(sender(to_core2)) + n = 0 + async for x in from_core2: + if not x % 1000: + print(f"Received {x} queue items.") + n += 1 + assert x == n + +asyncio.run(main()) +``` diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index d8a339e..94c57fe 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -48,7 +48,6 @@ def _handle_exception(loop, context): "ESwitch": "events", "EButton": "events", "RingbufQueue": "ringbuf_queue", - "ThreadSafeQueue": "threadsafe_queue", } # Copied from uasyncio.__init__.py diff --git a/v3/threadsafe/__init__.py b/v3/threadsafe/__init__.py new file mode 100644 index 0000000..596c526 --- /dev/null +++ b/v3/threadsafe/__init__.py @@ -0,0 +1,26 @@ +# __init__.py Common functions for uasyncio threadsafe primitives + +# Copyright (c) 2022 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +_attrs = { + "ThreadSafeEvent": "threadsafe_event", + "ThreadSafeQueue": "threadsafe_queue", +} + +# Copied from uasyncio.__init__.py +# Lazy loader, effectively does: +# global attr +# from .mod import attr +def __getattr__(attr): + mod = _attrs.get(attr, None) + if mod is None: + raise AttributeError(attr) + value = getattr(__import__(mod, None, None, True, 1), attr) + globals()[attr] = value + return value diff --git a/v3/threadsafe/threadsafe_event.py b/v3/threadsafe/threadsafe_event.py new file mode 100644 index 0000000..5667253 --- /dev/null +++ b/v3/threadsafe/threadsafe_event.py @@ -0,0 +1,36 @@ +# threadsafe_queue.py Provides ThreadsafeQueue class + +# Copyright (c) 2022 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio + + +class ThreadSafeEvent(asyncio.Event): + def __init__(self): + super().__init__() + self._waiting_on_tsf = False + self._tsf = asyncio.ThreadSafeFlag() + + def set(self): + self._tsf.set() + + async def _waiter(self): + await self._tsf.wait() + super().set() + self._waiting_on_tsf = False + + async def wait(self): + if self._waiting_on_tsf == False: + self._waiting_on_tsf = True + await asyncio.sleep_ms(0) + try: + await self._tsf.wait() + super().set() + self._waiting_on_tsf = False + except asyncio.CancelledError: + asyncio.create_task(self._waiter()) + raise + else: + await super().wait() + diff --git a/v3/primitives/threadsafe_queue.py b/v3/threadsafe/threadsafe_queue.py similarity index 100% rename from v3/primitives/threadsafe_queue.py rename to v3/threadsafe/threadsafe_queue.py From 732c8889cc34430aed2ea0b07c7add18e3e7f3dd Mon Sep 17 00:00:00 2001 From: peterhinch Date: Thu, 24 Nov 2022 17:43:07 +0000 Subject: [PATCH 183/305] Threadsafe queue has asynchronous get method. --- v3/docs/THREADING.md | 104 ++++++++++++++++++++++++++---- v3/threadsafe/threadsafe_queue.py | 3 + 2 files changed, 93 insertions(+), 14 deletions(-) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index ce5d190..059cb40 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -1,7 +1,9 @@ -# Thread safe classes +# Linking uasyncio and other contexts -These provide an interface between `uasyncio` tasks and code running in a -different context. Supported contexts are: +# 1. Introduction + +This document identifies issues arising when `uasyncio` applications interface +code running in a different context. Supported contexts are: 1. An interrupt service routine (ISR). 2. Another thread running on the same core. 3. Code running on a different core (currently only supported on RP2). @@ -12,8 +14,50 @@ MicroPython (MP) ISR will not interrupt execution of a line of Python code. This is not the case where the threads run on different cores, where there is no synchronisation between the streams of machine code. If the two threads -concurrently modify a shared Python object, there is no guarantee that -corruption will not occur. +concurrently modify a shared Python object it is possible that corruption will +occur. Reading an object while it is being written can also produce an +unpredictable outcome. + +A key practical point is that coding errors can be hard to identify: the +consequences can be extremely rare bugs or crashes. + +There are two fundamental problems: data sharing and synchronisation. + +# 2. Data sharing + +The simplest case is a shared pool of data. It is possible to share an `int` or +`bool` because at machine code level writing an `int` is "atomic": it cannot be +interrupted. Anything more complex must be protected to ensure that concurrent +access cannot take place. The consequences even of reading an object while it +is being written can be unpredictable. One approach is to use locking: + +```python +lock = _thread.allocate_lock() +values = { "X": 0, "Y": 0, "Z": 0} +def producer(): + while True: + lock.acquire() + values["X"] = sensor_read(0) + values["Y"] = sensor_read(1) + values["Z"] = sensor_read(2) + lock.release() + time.sleep_ms(100) + +_thread.start_new_thread(producer, ()) + +async def consumer(): + while True: + lock.acquire() + await process(values) # Do something with the data + lock.release() +``` +This will work even for the multi core case. However the consumer might hold +the lock for some time: it will take time for the scheduler to execute the +`process()` call, and the call itself will take time to run. This would be +problematic if the producer were an ISR. + +In cases such as this a `ThreadSafeQueue` is more appropriate as it decouples +producer and consumer code. # 2. Threadsafe Event @@ -65,8 +109,9 @@ communication is required between the two contexts, two `ThreadSafeQueue` instances are required. Attributes of `ThreadSafeQueue`: - 1. It is of fixed size defined on instantiation. - 2. It uses pre-allocated buffers of various types (`Queue` uses a `list`). + 1. It is of fixed capacity defined on instantiation. + 2. It uses a pre-allocated buffer of user selectable type (`Queue` uses a + dynaically allocated `list`). 3. It is an asynchronous iterator allowing retrieval with `async for`. 4. It provides synchronous "put" and "get" methods. If the queue becomes full (put) or empty (get), behaviour is user definable. The method either blocks or @@ -92,6 +137,10 @@ See the note below re blocking methods. Asynchronous methods: * `put` Arg: the object to put on the queue. If the queue is full, it will block until space is available. + * `get` No arg. Returns an object from the queue. If the queue is empty, it + will block until an object is put on the queue. Normal retrieval is with + `async for` but this method provides an alternative. + In use as a data consumer the `uasyncio` code will use `async for` to retrieve items from the queue. If it is a data provider it will use `put` to place @@ -130,17 +179,44 @@ while True: ## 3.1 Blocking -The synchronous `get_sync` and `put_sync` methods have blocking modes invoked -by passing `block=True`. Blocking modes are intended to be used in a multi -threaded context. They should not be invoked in a `uasyncio` task, because -blocking locks up the scheduler. Nor should they be used in an ISR where -blocking code can have unpredictable consequences. - These methods, called with `blocking=False`, produce an immediate return. To avoid an `IndexError` the user should check for full or empty status before calling. -## 3.2 A complete example +The synchronous `get_sync` and `put_sync` methods have blocking modes invoked +by passing `block=True`. Blocking modes are primarily intended for use in the +non-`uasyncio ` context. If invoked in a `uasyncio` task they must not be +allowed to block because it would lock up the scheduler. Nor should they be +allowed to block in an ISR where blocking can have unpredictable consequences. + +## 3.2 Object ownership + +Any Python object can be placed on a queue, but the user should be aware that +once the producer puts an object on the queue it loses ownership of the object +until the consumer has finished using it. In this sample the producer reads X, +Y and Z values from a sensor, puts them in a list or array and places the +object on a queue: +```python +def get_coordinates(q): + while True: + lst = [axis(0), axis(1), axis(2)] # Read sensors and put into list + putq.put_sync(lst, block=True) +``` +This is valid because a new list is created each time. The following will not +work: +```python +def get_coordinates(q): + a = array.array("I", (0,0,0)) + while True: + a[0], a[1], a[2] = [axis(0), axis(1), axis(2)] + putq.put_sync(lst, block=True) +``` +The problem here is that the array is modified after being put on the queue. If +the queue is capable of holding 10 objects, 10 array instances are required. Re +using objects requires the producer to be notified that the consumer has +finished with the item. + +## 3.3 A complete example This demonstrates an echo server running on core 2. The `sender` task sends consecutive integers to the server, which echoes them back on a second queue. diff --git a/v3/threadsafe/threadsafe_queue.py b/v3/threadsafe/threadsafe_queue.py index b97c657..0bec8d2 100644 --- a/v3/threadsafe/threadsafe_queue.py +++ b/v3/threadsafe/threadsafe_queue.py @@ -55,6 +55,9 @@ def __aiter__(self): return self async def __anext__(self): + return await self.get() + + async def get(self): while self.empty(): await self._evput.wait() r = self._q[self._ri] From 825d38cf61d5d61ac001a8961668fe71e6b05432 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Fri, 25 Nov 2022 08:36:16 +0000 Subject: [PATCH 184/305] TUTORIAL: Fix MillisecTimer demo. --- v3/docs/TUTORIAL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index c9441c6..21e4308 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2267,8 +2267,8 @@ class MyIO(io.IOBase): return ret ``` -The following is a complete awaitable delay class: - +The following is a complete awaitable delay class. Please note that it does not +run on the Unix port (under investigation). ```python import uasyncio as asyncio import utime @@ -2290,7 +2290,7 @@ class MillisecTimer(io.IOBase): return self def read(self, _): - pass + return "a" def ioctl(self, req, arg): ret = MP_STREAM_ERROR From f95705d95465bebce8dbc7a316c6985879f65c18 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Fri, 25 Nov 2022 10:04:14 +0000 Subject: [PATCH 185/305] TUTORIAL: Section 6.3 add Unix disclaimer. --- v3/docs/TUTORIAL.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 21e4308..ee1b254 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2126,7 +2126,13 @@ asyncio.run(run()) ## 6.3 Using the stream mechanism -This can be illustrated using a Pyboard UART. The following code sample +This section applies to platforms other than the Unix build. The latter handles +stream I/O in a different way described +[here](https://github.com/micropython/micropython/issues/7965#issuecomment-960259481). +Code samples may not run under the Unix build until it is made more compatible +with other platforms. + +The stream mechanism can be illustrated using a Pyboard UART. This code sample demonstrates concurrent I/O on one UART. To run, link Pyboard pins X1 and X2 (UART Txd and Rxd). @@ -2267,8 +2273,7 @@ class MyIO(io.IOBase): return ret ``` -The following is a complete awaitable delay class. Please note that it does not -run on the Unix port (under investigation). +The following is a complete awaitable delay class. ```python import uasyncio as asyncio import utime From b82109651051ef31b7627b215bb74f0387fc8974 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Mon, 28 Nov 2022 17:59:11 +0000 Subject: [PATCH 186/305] THREADING.md: Improve section 1. --- v3/docs/INTERRUPTS.md | 11 ++- v3/docs/THREADING.md | 193 +++++++++++++++++++++++++++++------------- 2 files changed, 143 insertions(+), 61 deletions(-) diff --git a/v3/docs/INTERRUPTS.md b/v3/docs/INTERRUPTS.md index 05d7acd..ef96fd5 100644 --- a/v3/docs/INTERRUPTS.md +++ b/v3/docs/INTERRUPTS.md @@ -185,9 +185,16 @@ async def process_data(): # Process the data here before waiting for the next interrupt ``` +## 3.4 Thread Safe Classes + +Other classes capable of being used to interface an ISR with `uasyncio` are +discussed [here](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/THREADING.md), +notably the `ThreadSafeQueue`. + # 4. Conclusion -The key take-away is that `ThreadSafeFlag` is the only `uasyncio` construct -which can safely be used in an ISR context. +The key take-away is that `ThreadSafeFlag` is the only official `uasyncio` +construct which can safely be used in an ISR context. Unofficial "thread +safe" classes may also be used. ###### [Main tutorial](./TUTORIAL.md#contents) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index 059cb40..c7b8d68 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -8,18 +8,81 @@ code running in a different context. Supported contexts are: 2. Another thread running on the same core. 3. Code running on a different core (currently only supported on RP2). -The first two cases are relatively straightforward because both contexts share -a common bytecode interpreter and GIL. There is a guarantee that even a hard -MicroPython (MP) ISR will not interrupt execution of a line of Python code. - -This is not the case where the threads run on different cores, where there is -no synchronisation between the streams of machine code. If the two threads -concurrently modify a shared Python object it is possible that corruption will -occur. Reading an object while it is being written can also produce an -unpredictable outcome. - -A key practical point is that coding errors can be hard to identify: the -consequences can be extremely rare bugs or crashes. +Note that hard ISR's require careful coding to avoid RAM allocation. See +[the official docs](http://docs.micropython.org/en/latest/reference/isr_rules.html). +The allocation issue is orthogonal to the concurrency issues discussed in this +document. Concurrency problems apply equally to hard and soft ISR's. Code +samples assume a soft ISR or a function launched by `micropython.schedule`. +[This doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/INTERRUPTS.md) +provides specific guidance on interfacing `uasyncio` with ISR's. + +The rest of this section compares the characteristics of the three contexts. +Consider this function which updates a global dictionary `d` from a hardware +device. The dictionary is shared with a `uasyncio` task. +```python +def update_dict(): + d["x"] = read_data(0) + d["y"] = read_data(1) + d["z"] = read_data(2) +``` +This might be called in a soft ISR, in a thread running on the same core as +`uasyncio`, or in a thread running on a different core. Each of these contexts +has different characteristics, outlined below. In all these cases "thread safe" +constructs are needed to interface `uasyncio` tasks with code running in these +contexts. The official `ThreadSafeFlag`, or the classes documented here, may be +used in all of these cases. This function serves to illustrate concurrency +issues: it is not the most effcient way to transfer data. + +Beware that some apparently obvious ways to interface an ISR to `uasyncio` +introduce subtle bugs discussed in the doc referenced above. The only reliable +interface is via a thread safe class. + +## 1.1 Soft Interrupt Service Routines + + 1. The ISR and the main program share a common Python virtual machine (VM). + Consequently a line of code being executed when the interrupt occurs will run + to completion before the ISR runs. + 2. An ISR will run to completion before the main program regains control. This + means that if the ISR updates multiple items, when the main program resumes, + those items will be mutually consistent. The above code fragment will work + unchanged. + 3. The fact that ISR code runs to completion means that it must run fast to + avoid disrupting the main program or delaying other ISR's. ISR code should not + call blocking routines and should not wait on locks. Item 2. means that locks + are not usually necessary. + 4. If a burst of interrupts can occur faster than `uasyncio` can schedule the + handling task, data loss can occur. Consider using a `ThreadSafeQueue`. Note + that if this high rate is sustained something will break and the overall + design needs review. It may be necessary to discard some data items. + +## 1.2 Threaded code on one core + + 1. Both contexts share a common VM so Python code integrity is guaranteed. + 2. If one thread updates a data item there is no risk of the main program + reading a corrupt or partially updated item. If such code updates multiple + shared data items, note that `uasyncio` can regain control at any time. The + above code fragment may not have updated all the dictionary keys when + `uasyncio` regains control. If mutual consistency is important, a lock or + `ThreadSafeQueue` must be used. + 3. Code running on a thread other than that running `uasyncio` may block for + as long as necessary (an application of threading is to handle blocking calls + in a way that allows `uasyncio` to continue running). + +## 1.3 Threaded code on multiple cores + + 1. There is no common VM. The underlying machine code of each core runs + independently. + 2. In the code sample there is a risk of the `uasyncio` task reading the dict + at the same moment as it is being written. It may read a corrupt or partially + updated item; there may even be a crash. Using a lock or `ThreadSafeQueue` is + essential. + 3. Code running on a core other than that running `uasyncio` may block for + as long as necessary. + +A key practical point is that coding errors in synchronising threads can be +hard to locate: consequences can be extremely rare bugs or crashes. It is vital +to be careful in the way that communication between the contexts is achieved. This +doc aims to provide some guidelines and code to assist in this task. There are two fundamental problems: data sharing and synchronisation. @@ -54,51 +117,14 @@ async def consumer(): This will work even for the multi core case. However the consumer might hold the lock for some time: it will take time for the scheduler to execute the `process()` call, and the call itself will take time to run. This would be -problematic if the producer were an ISR. - -In cases such as this a `ThreadSafeQueue` is more appropriate as it decouples -producer and consumer code. +problematic if the producer were an ISR. In this case the absence of a lock +would not result in crashes because an ISR cannot interrupt a MicroPython +instruction. -# 2. Threadsafe Event +In cases where the duration of a lock is problematic a `ThreadSafeQueue` is +more appropriate as it decouples producer and consumer code. -The `ThreadsafeFlag` has a limitation in that only a single task can wait on -it. The `ThreadSafeEvent` overcomes this. It is subclassed from `Event` and -presents the same interface. The `set` method may be called from an ISR or from -code running on another core. Any number of tasks may wait on it. - -The following Pyboard-specific code demos its use in a hard ISR: -```python -import uasyncio as asyncio -from threadsafe import ThreadSafeEvent -from pyb import Timer - -async def waiter(n, evt): - try: - await evt.wait() - print(f"Waiter {n} got event") - except asyncio.CancelledError: - print(f"Waiter {n} cancelled") - -async def can(task): - await asyncio.sleep_ms(100) - task.cancel() - -async def main(): - evt = ThreadSafeEvent() - tim = Timer(4, freq=1, callback=lambda t: evt.set()) - nt = 0 - while True: - tasks = [asyncio.create_task(waiter(n + 1, evt)) for n in range(4)] - asyncio.create_task(can(tasks[nt])) - await asyncio.gather(*tasks, return_exceptions=True) - evt.clear() - print("Cleared event") - nt = (nt + 1) % 4 - -asyncio.run(main()) -``` - -# 3. Threadsafe Queue +## 2.1 ThreadSafeQueue This queue is designed to interface between one `uasyncio` task and a single thread running in a different context. This can be an interrupt service routine @@ -177,7 +203,7 @@ while True: process(data) # Do something with it ``` -## 3.1 Blocking +### 2.1.1 Blocking These methods, called with `blocking=False`, produce an immediate return. To avoid an `IndexError` the user should check for full or empty status before @@ -189,7 +215,7 @@ non-`uasyncio ` context. If invoked in a `uasyncio` task they must not be allowed to block because it would lock up the scheduler. Nor should they be allowed to block in an ISR where blocking can have unpredictable consequences. -## 3.2 Object ownership +### 2.1.2 Object ownership Any Python object can be placed on a queue, but the user should be aware that once the producer puts an object on the queue it loses ownership of the object @@ -214,9 +240,10 @@ def get_coordinates(q): The problem here is that the array is modified after being put on the queue. If the queue is capable of holding 10 objects, 10 array instances are required. Re using objects requires the producer to be notified that the consumer has -finished with the item. +finished with the item. In general it is simpler to create new objects and let +the MicroPython garbage collector delete them as per the first sample. -## 3.3 A complete example +### 2.1.3 A complete example This demonstrates an echo server running on core 2. The `sender` task sends consecutive integers to the server, which echoes them back on a second queue. @@ -255,3 +282,51 @@ async def main(): asyncio.run(main()) ``` +# 3. Synchronisation + +The principal means of synchronising `uasyncio` code with that running in +another context is the `ThreadsafeFlag`. This is discussed in the +[official docs](http://docs.micropython.org/en/latest/library/uasyncio.html#class-threadsafeflag) +and [tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#36-threadsafeflag). +In essence a single `uasyncio` task waits on a shared `ThreadSafeEvent`. Code +running in another context sets the flag. When the scheduler regains control +and other pending tasks have run, the waiting task resumes. + +## 3.1 Threadsafe Event + +The `ThreadsafeFlag` has a limitation in that only a single task can wait on +it. The `ThreadSafeEvent` overcomes this. It is subclassed from `Event` and +presents the same interface. The `set` method may be called from an ISR or from +code running on another core. Any number of tasks may wait on it. + +The following Pyboard-specific code demos its use in a hard ISR: +```python +import uasyncio as asyncio +from threadsafe import ThreadSafeEvent +from pyb import Timer + +async def waiter(n, evt): + try: + await evt.wait() + print(f"Waiter {n} got event") + except asyncio.CancelledError: + print(f"Waiter {n} cancelled") + +async def can(task): + await asyncio.sleep_ms(100) + task.cancel() + +async def main(): + evt = ThreadSafeEvent() + tim = Timer(4, freq=1, callback=lambda t: evt.set()) + nt = 0 + while True: + tasks = [asyncio.create_task(waiter(n + 1, evt)) for n in range(4)] + asyncio.create_task(can(tasks[nt])) + await asyncio.gather(*tasks, return_exceptions=True) + evt.clear() + print("Cleared event") + nt = (nt + 1) % 4 + +asyncio.run(main()) +``` From 4dfecdd0425cb5cff05d640fd6d8eaea1f652519 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Thu, 1 Dec 2022 13:43:24 +0000 Subject: [PATCH 187/305] Message class: Remove redundant code. --- v3/docs/THREADING.md | 146 +++++++++++++++++++++++++-------------- v3/docs/TUTORIAL.md | 3 + v3/primitives/message.py | 7 +- 3 files changed, 100 insertions(+), 56 deletions(-) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index c7b8d68..e5a81aa 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -1,24 +1,42 @@ # Linking uasyncio and other contexts +This document is primarily for those wishing to interface `uasyncio` code with +that running under the `_thread` module. It presents classes for that purpose +which may also find use for communicatiing between threads and in interrupt +service routine (ISR) applications. It provides an overview of the problems +implicit in pre-emptive multi tasking. + +It is not an introduction into ISR coding. For this see +[the official docs](http://docs.micropython.org/en/latest/reference/isr_rules.html) +and [this doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/INTERRUPTS.md) +which provides specific guidance on interfacing `uasyncio` with ISR's. + +# Contents + + 1. [Introduction](./THREADING.md#1-introduction) The various types of pre-emptive code. + 1.1 [Interrupt Service Routines](./THREADING.md#11-interrupt-service-routines) + 1.2 [Threaded code on one core](./THREADING.md#12-threaded-code-on-one-core) + 1.3 [Threaded code on multiple cores](./THREADING.md#13-threaded-code-on-multiple-cores) + 1.4 [Debugging](./THREADING.md#14-debugging) + 2. [Sharing data](./THREADING.md#2-sharing-data) + 2.1 [A pool](./THREADING.md#21-a-pool) Sharing a set of variables. + 2.2 [ThreadSafeQueue](./THREADING.md#22-threadsafequeue) +      2.2.1 [Blocking](./THREADING.md#221-blocking) +      2.2.3 [Object ownership](./THREADING.md#223-object-ownership) + 3. [Synchronisation](./THREADING.md#3-synchronisation) + 3.1 [Threadsafe Event](./THREADING.md#31-threadsafe-event) + # 1. Introduction -This document identifies issues arising when `uasyncio` applications interface -code running in a different context. Supported contexts are: - 1. An interrupt service routine (ISR). +Various issues arise when `uasyncio` applications interface with code running +in a different context. Supported contexts are: + 1. A hard or soft interrupt service routine (ISR). 2. Another thread running on the same core. 3. Code running on a different core (currently only supported on RP2). -Note that hard ISR's require careful coding to avoid RAM allocation. See -[the official docs](http://docs.micropython.org/en/latest/reference/isr_rules.html). -The allocation issue is orthogonal to the concurrency issues discussed in this -document. Concurrency problems apply equally to hard and soft ISR's. Code -samples assume a soft ISR or a function launched by `micropython.schedule`. -[This doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/INTERRUPTS.md) -provides specific guidance on interfacing `uasyncio` with ISR's. - -The rest of this section compares the characteristics of the three contexts. -Consider this function which updates a global dictionary `d` from a hardware -device. The dictionary is shared with a `uasyncio` task. +This section compares the characteristics of the three contexts. Consider this +function which updates a global dictionary `d` from a hardware device. The +dictionary is shared with a `uasyncio` task. ```python def update_dict(): d["x"] = read_data(0) @@ -30,40 +48,41 @@ This might be called in a soft ISR, in a thread running on the same core as has different characteristics, outlined below. In all these cases "thread safe" constructs are needed to interface `uasyncio` tasks with code running in these contexts. The official `ThreadSafeFlag`, or the classes documented here, may be -used in all of these cases. This function serves to illustrate concurrency -issues: it is not the most effcient way to transfer data. +used in all of these cases. This `update_dict` function serves to illustrate +concurrency issues: it is not the most effcient way to transfer data. Beware that some apparently obvious ways to interface an ISR to `uasyncio` introduce subtle bugs discussed in the doc referenced above. The only reliable -interface is via a thread safe class. +interface is via a thread safe class, usually `ThreadSafeFlag`. -## 1.1 Soft Interrupt Service Routines +## 1.1 Interrupt Service Routines 1. The ISR and the main program share a common Python virtual machine (VM). Consequently a line of code being executed when the interrupt occurs will run to completion before the ISR runs. 2. An ISR will run to completion before the main program regains control. This means that if the ISR updates multiple items, when the main program resumes, - those items will be mutually consistent. The above code fragment will work - unchanged. + those items will be mutually consistent. The above code fragment will provide + mutually consistent data. 3. The fact that ISR code runs to completion means that it must run fast to avoid disrupting the main program or delaying other ISR's. ISR code should not call blocking routines and should not wait on locks. Item 2. means that locks - are not usually necessary. + are seldom necessary. 4. If a burst of interrupts can occur faster than `uasyncio` can schedule the handling task, data loss can occur. Consider using a `ThreadSafeQueue`. Note - that if this high rate is sustained something will break and the overall - design needs review. It may be necessary to discard some data items. + that if this high rate is sustained something will break: the overall design + needs review. It may be necessary to discard some data items. ## 1.2 Threaded code on one core - 1. Both contexts share a common VM so Python code integrity is guaranteed. - 2. If one thread updates a data item there is no risk of the main program - reading a corrupt or partially updated item. If such code updates multiple - shared data items, note that `uasyncio` can regain control at any time. The - above code fragment may not have updated all the dictionary keys when - `uasyncio` regains control. If mutual consistency is important, a lock or - `ThreadSafeQueue` must be used. + 1. Behaviour depends on the port + [see](https://github.com/micropython/micropython/discussions/10135#discussioncomment-4275354). + At best, context switches can occur at bytecode boundaries. On ports where + contexts share no GIL they can occur at any time. + 2. Hence for shared data item more complex than a small int, a lock or + `ThreadSafeQueue` must be used. This ensures that the thread reading the data + cannot access a partially updated item (which might even result in a crash). + It also ensures mutual consistency between multiple data items. 3. Code running on a thread other than that running `uasyncio` may block for as long as necessary (an application of threading is to handle blocking calls in a way that allows `uasyncio` to continue running). @@ -79,21 +98,28 @@ interface is via a thread safe class. 3. Code running on a core other than that running `uasyncio` may block for as long as necessary. +## 1.4 Debugging + A key practical point is that coding errors in synchronising threads can be -hard to locate: consequences can be extremely rare bugs or crashes. It is vital -to be careful in the way that communication between the contexts is achieved. This -doc aims to provide some guidelines and code to assist in this task. +hard to locate: consequences can be extremely rare bugs or (in the case of +multi-core systems) crashes. It is vital to be careful in the way that +communication between the contexts is achieved. This doc aims to provide some +guidelines and code to assist in this task. There are two fundamental problems: data sharing and synchronisation. -# 2. Data sharing +###### [Contents](./THREADING.md#contents) + +# 2. Sharing data + +## 2.1 A pool The simplest case is a shared pool of data. It is possible to share an `int` or `bool` because at machine code level writing an `int` is "atomic": it cannot be -interrupted. Anything more complex must be protected to ensure that concurrent -access cannot take place. The consequences even of reading an object while it -is being written can be unpredictable. One approach is to use locking: - +interrupted. In the multi core case anything more complex must be protected to +ensure that concurrent access cannot take place. The consequences even of +reading an object while it is being written can be unpredictable. One approach +is to use locking: ```python lock = _thread.allocate_lock() values = { "X": 0, "Y": 0, "Z": 0} @@ -113,18 +139,30 @@ async def consumer(): lock.acquire() await process(values) # Do something with the data lock.release() + await asyncio.sleep_ms(0) # Ensure producer has time to grab the lock ``` -This will work even for the multi core case. However the consumer might hold -the lock for some time: it will take time for the scheduler to execute the -`process()` call, and the call itself will take time to run. This would be -problematic if the producer were an ISR. In this case the absence of a lock -would not result in crashes because an ISR cannot interrupt a MicroPython -instruction. +This is recommended where the producer runs in a different thread from +`uasyncio`. However the consumer might hold the lock for some time: it will +take time for the scheduler to execute the `process()` call, and the call +itself will take time to run. In cases where the duration of a lock is +problematic a `ThreadSafeQueue` is more appropriate as it decouples producer +and consumer code. + +As stated above, if the producer is an ISR no lock is needed or advised. +Producer code would follow this pattern: +```python +values = { "X": 0, "Y": 0, "Z": 0} +def producer(): + values["X"] = sensor_read(0) + values["Y"] = sensor_read(1) + values["Z"] = sensor_read(2) +``` +and the ISR would run to completion before `uasyncio` resumed, ensuring mutual +consistency of the dict values. -In cases where the duration of a lock is problematic a `ThreadSafeQueue` is -more appropriate as it decouples producer and consumer code. +###### [Contents](./THREADING.md#contents) -## 2.1 ThreadSafeQueue +## 2.2 ThreadSafeQueue This queue is designed to interface between one `uasyncio` task and a single thread running in a different context. This can be an interrupt service routine @@ -203,7 +241,9 @@ while True: process(data) # Do something with it ``` -### 2.1.1 Blocking +###### [Contents](./THREADING.md#contents) + +### 2.2.1 Blocking These methods, called with `blocking=False`, produce an immediate return. To avoid an `IndexError` the user should check for full or empty status before @@ -215,7 +255,9 @@ non-`uasyncio ` context. If invoked in a `uasyncio` task they must not be allowed to block because it would lock up the scheduler. Nor should they be allowed to block in an ISR where blocking can have unpredictable consequences. -### 2.1.2 Object ownership +###### [Contents](./THREADING.md#contents) + +### 2.2.2 Object ownership Any Python object can be placed on a queue, but the user should be aware that once the producer puts an object on the queue it loses ownership of the object @@ -243,7 +285,9 @@ using objects requires the producer to be notified that the consumer has finished with the item. In general it is simpler to create new objects and let the MicroPython garbage collector delete them as per the first sample. -### 2.1.3 A complete example +###### [Contents](./THREADING.md#contents) + +### 2.2.3 A complete example This demonstrates an echo server running on core 2. The `sender` task sends consecutive integers to the server, which echoes them back on a second queue. @@ -282,6 +326,8 @@ async def main(): asyncio.run(main()) ``` +###### [Contents](./THREADING.md#contents) + # 3. Synchronisation The principal means of synchronising `uasyncio` code with that running in diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index ee1b254..c0ae4a2 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2968,4 +2968,7 @@ The above comments refer to an ideal scheduler. Currently `uasyncio` is not in this category, with worst-case latency being > `N`ms. The conclusions remain valid. +This, along with other issues, is discussed in +[Interfacing uasyncio to interrupts](./INTERRUPTS.md). + ###### [Contents](./TUTORIAL.md#contents) diff --git a/v3/primitives/message.py b/v3/primitives/message.py index 174061a..a6202cb 100644 --- a/v3/primitives/message.py +++ b/v3/primitives/message.py @@ -28,10 +28,8 @@ def __init__(self): self._waiting_on_tsf = False self._tsf = asyncio.ThreadSafeFlag() self._data = None # Message - self._is_set = False def clear(self): # At least one task must call clear when scheduled - self._is_set = False super().clear() def __iter__(self): @@ -60,7 +58,7 @@ async def wait(self): def set(self, data=None): # Can be called from a hard ISR self._data = data - self._is_set = True + super().set() self._tsf.set() def __aiter__(self): @@ -69,8 +67,5 @@ def __aiter__(self): async def __anext__(self): return await self - def is_set(self): - return self._is_set - def value(self): return self._data From f7c44fa933bb1166f8abdba26b983255fa309e7f Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sat, 3 Dec 2022 18:15:40 +0000 Subject: [PATCH 188/305] Move Message class and docs to threadsafe. --- v3/docs/THREADING.md | 169 +++++++++++++++++++++++ v3/docs/TUTORIAL.md | 103 +------------- v3/primitives/__init__.py | 1 - v3/primitives/tests/asyntest.py | 2 +- v3/threadsafe/__init__.py | 1 + v3/{primitives => threadsafe}/message.py | 0 6 files changed, 178 insertions(+), 98 deletions(-) rename v3/{primitives => threadsafe}/message.py (100%) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index e5a81aa..a2282c4 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -11,6 +11,11 @@ It is not an introduction into ISR coding. For this see and [this doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/INTERRUPTS.md) which provides specific guidance on interfacing `uasyncio` with ISR's. +Because of [this issue](https://github.com/micropython/micropython/issues/7965) +the `ThreadSafeFlag` class does not work under the Unix build. The classes +presented here depend on this: none can be expected to work on Unix until this +is fixed. + # Contents 1. [Introduction](./THREADING.md#1-introduction) The various types of pre-emptive code. @@ -25,6 +30,8 @@ which provides specific guidance on interfacing `uasyncio` with ISR's.      2.2.3 [Object ownership](./THREADING.md#223-object-ownership) 3. [Synchronisation](./THREADING.md#3-synchronisation) 3.1 [Threadsafe Event](./THREADING.md#31-threadsafe-event) + 3.2 [Message](./THREADING.md#32-message) A threadsafe event with data payload. + 4. [Taming blocking functions](./THREADING.md#4-taming-blocking-functions) # 1. Introduction @@ -376,3 +383,165 @@ async def main(): asyncio.run(main()) ``` +## 3.2 Message + +The `Message` class uses [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) to +provide an object similar to `Event` with the following differences: + + * `.set()` has an optional data payload. + * `.set()` can be called from another thread, another core, or from an ISR. + * It is an awaitable class. + * Payloads may be retrieved in an asynchronous iterator. + * Multiple tasks can wait on a single `Message` instance. + +Constructor: + * No args. + +Synchronous methods: + * `set(data=None)` Trigger the `Message` with optional payload (may be any + Python object). + * `is_set()` Returns `True` if the `Message` is set, `False` if `.clear()` has + been issued. + * `clear()` Clears the triggered status. At least one task waiting on the + message should issue `clear()`. + * `value()` Return the payload. + +Asynchronous Method: + * `wait()` Pause until message is triggered. You can also `await` the message + as per the examples. + +The `.set()` method can accept an optional data value of any type. The task +waiting on the `Message` can retrieve it by means of `.value()` or by awaiting +the `Message` as below. A `Message` can provide a means of communication from +an interrupt handler and a task. The handler services the hardware and issues +`.set()` which causes the waiting task to resume (in relatively slow time). + +This illustrates basic usage: +```python +import uasyncio as asyncio +from threadsafe import Message + +async def waiter(msg): + print('Waiting for message') + res = await msg + print('waiter got', res) + msg.clear() + +async def main(): + msg = Message() + asyncio.create_task(waiter(msg)) + await asyncio.sleep(1) + msg.set('Hello') # Optional arg + await asyncio.sleep(1) + +asyncio.run(main()) +``` +The following example shows multiple tasks awaiting a `Message`. +```python +from threadsafe import Message +import uasyncio as asyncio + +async def bar(msg, n): + while True: + res = await msg + msg.clear() + print(n, res) + # Pause until other coros waiting on msg have run and before again + # awaiting a message. + await asyncio.sleep_ms(0) + +async def main(): + msg = Message() + for n in range(5): + asyncio.create_task(bar(msg, n)) + k = 0 + while True: + k += 1 + await asyncio.sleep_ms(1000) + msg.set('Hello {}'.format(k)) + +asyncio.run(main()) +``` +Receiving messages in an asynchronous iterator: +```python +import uasyncio as asyncio +from threadsafe import Message + +async def waiter(msg): + async for text in msg: + print(f"Waiter got {text}") + msg.clear() + +async def main(): + msg = Message() + task = asyncio.create_task(waiter(msg)) + for text in ("Hello", "This is a", "message", "goodbye"): + msg.set(text) + await asyncio.sleep(1) + task.cancel() + await asyncio.sleep(1) + print("Done") + +asyncio.run(main()) +``` +The `Message` class does not have a queue: if the instance is set, then set +again before it is accessed, the first data item will be lost. + +# 4. Taming blocking functions + +Blocking functions or methods have the potential of stalling the `uasyncio` +scheduler. Short of rewriting them to work properly the only way to tame them +is to run them in another thread. The following is a way to achieve this. +```python +async def unblock(func, *args, **kwargs): + def wrap(func, message, args, kwargs): + message.set(func(*args, **kwargs)) # Run the blocking function. + msg = Message() + _thread.start_new_thread(wrap, (func, msg, args, kwargs)) + return await msg +``` +Given a blocking function `blocking` taking two positional and two keyword args +it may be awaited in a `uasyncio` task with +```python + res = await unblock(blocking, 1, 2, c = 3, d = 4) +``` +The function runs "in the background" with other tasks running; only the +calling task is paused. Note how the args are passed. There is a "gotcha" which +is cancellation. It is not valid to cancel the `unblock` task because the +underlying thread will still be running. There is no general solution to this. +If the specific blocking function has a means of interrupting it or of forcing +a timeout then it may be possible to code a solution. + +The following is a complete example where blocking is demonstrated with +`time.sleep`. +```python +import uasyncio as asyncio +from threadsafe import Message +import _thread +from time import sleep + +def slow_add(a, b, *, c, d): # Blocking function. + sleep(5) + return a + b + c + d + +# Convert a blocking function to a nonblocking one using threading. +async def unblock(func, *args, **kwargs): + def wrap(func, message, args, kwargs): + message.set(func(*args, **kwargs)) # Run the blocking function. + msg = Message() + _thread.start_new_thread(wrap, (func, msg, args, kwargs)) + return await msg + +async def busywork(): # Prove uasyncio is running. + while True: + print("#", end="") + await asyncio.sleep_ms(200) + +async def main(): + bw = asyncio.create_task(busywork()) + res = await unblock(slow_add, 1, 2, c = 3, d = 4) + bw.cancel() + print(f"\nDone. Result = {res}") + +asyncio.run(main()) +``` diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index c0ae4a2..1111eb8 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1343,106 +1343,17 @@ finally: ## 3.9 Message -Because of [this issue](https://github.com/micropython/micropython/issues/7965) -the `Message` class does not work under the Unix build. +The `Message` class uses [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) to +provide an object similar to `Event` with the following differences: -This is an unofficial primitive with no counterpart in CPython asyncio. It uses -[ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) to provide an object similar -to `Event` but capable of being set in a hard ISR context. It extends -`ThreadSafeFlag` so that multiple tasks can wait on an ISR. - -It is similar to the `Event` class. It differs in that: * `.set()` has an optional data payload. - * `.set()` is capable of being called from a hard or soft interrupt service - routine. + * `.set()` can be called from another thread, another core, or from an ISR. * It is an awaitable class. - * It can be used in an asynchronous iterator. - * The logic of `.clear` differs: it must be called by at least one task which - waits on the `Message`. - -The `.set()` method can accept an optional data value of any type. The task -waiting on the `Message` can retrieve it by means of `.value()` or by awaiting -the `Message` as below. - -Like `Event`, `Message` provides a way for a task to pause until another flags it -to continue. A `Message` object is instantiated and made accessible to the task -using it: - -```python -import uasyncio as asyncio -from primitives import Message - -async def waiter(msg): - print('Waiting for message') - res = await msg - print('waiter got', res) - msg.clear() - -async def main(): - msg = Message() - asyncio.create_task(waiter(msg)) - await asyncio.sleep(1) - msg.set('Hello') # Optional arg - await asyncio.sleep(1) - -asyncio.run(main()) -``` -A `Message` can provide a means of communication between an interrupt handler -and a task. The handler services the hardware and issues `.set()` which causes -the waiting task to resume (in relatively slow time). - -Constructor: - * No args. + * Payloads may be retrieved in an asynchronous iterator. + * Multiple tasks can wait on a single `Message` instance. -Synchronous methods: - * `set(data=None)` Trigger the `Message` with optional payload (may be any - Python object). - * `is_set()` Returns `True` if the `Message` is set, `False` if `.clear()` has - been issued. - * `clear()` Clears the triggered status. At least one task waiting on the - message should issue `clear()`. - * `value()` Return the payload. - -Asynchronous Method: - * `wait()` Pause until message is triggered. You can also `await` the message - as per the examples. - -The following example shows multiple tasks awaiting a `Message`. -```python -from primitives import Message -import uasyncio as asyncio - -async def bar(msg, n): - while True: - res = await msg - msg.clear() - print(n, res) - # Pause until other coros waiting on msg have run and before again - # awaiting a message. - await asyncio.sleep_ms(0) - -async def main(): - msg = Message() - for n in range(5): - asyncio.create_task(bar(msg, n)) - k = 0 - while True: - k += 1 - await asyncio.sleep_ms(1000) - msg.set('Hello {}'.format(k)) - -asyncio.run(main()) -``` -Receiving messages in an asynchronous iterator: -```python -msg = Message() -asyncio.create_task(send_data(msg)) -async for data in msg: - # process data - msg.clear() -``` -The `Message` class does not have a queue: if the instance is set, then set -again before it is accessed, the first data item will be lost. +It may be found in the `threadsafe` directory and is documented +[here](./THREADING.md#32-message). ## 3.10 Synchronising to hardware diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index 94c57fe..1dab8ba 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -36,7 +36,6 @@ def _handle_exception(loop, context): "Condition": "condition", "Delay_ms": "delay_ms", "Encode": "encoder_async", - "Message": "message", "Pushbutton": "pushbutton", "ESP32Touch": "pushbutton", "Queue": "queue", diff --git a/v3/primitives/tests/asyntest.py b/v3/primitives/tests/asyntest.py index e376c67..606e1fd 100644 --- a/v3/primitives/tests/asyntest.py +++ b/v3/primitives/tests/asyntest.py @@ -18,7 +18,7 @@ from primitives import Barrier, Semaphore, BoundedSemaphore, Condition, Queue, RingbufQueue try: - from primitives import Message + from threadsafe import Message except: pass diff --git a/v3/threadsafe/__init__.py b/v3/threadsafe/__init__.py index 596c526..ae39d68 100644 --- a/v3/threadsafe/__init__.py +++ b/v3/threadsafe/__init__.py @@ -11,6 +11,7 @@ _attrs = { "ThreadSafeEvent": "threadsafe_event", "ThreadSafeQueue": "threadsafe_queue", + "Message": "message", } # Copied from uasyncio.__init__.py diff --git a/v3/primitives/message.py b/v3/threadsafe/message.py similarity index 100% rename from v3/primitives/message.py rename to v3/threadsafe/message.py From 5557622ac80e172e4a3376c30becd96bc99cd3f1 Mon Sep 17 00:00:00 2001 From: adminpete Date: Tue, 13 Dec 2022 17:20:05 +0000 Subject: [PATCH 189/305] Tutorial: Add section on threaded code. --- v3/docs/THREADING.md | 23 +++++++------ v3/docs/TUTORIAL.md | 80 +++++++++++++++++++------------------------- 2 files changed, 48 insertions(+), 55 deletions(-) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index a2282c4..e2f2aee 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -82,22 +82,23 @@ interface is via a thread safe class, usually `ThreadSafeFlag`. ## 1.2 Threaded code on one core - 1. Behaviour depends on the port - [see](https://github.com/micropython/micropython/discussions/10135#discussioncomment-4275354). - At best, context switches can occur at bytecode boundaries. On ports where - contexts share no GIL they can occur at any time. - 2. Hence for shared data item more complex than a small int, a lock or - `ThreadSafeQueue` must be used. This ensures that the thread reading the data - cannot access a partially updated item (which might even result in a crash). - It also ensures mutual consistency between multiple data items. + 1. On single core devices with a common GIL, Python instructions can be + considered "atomic": they are guaranteed to run to completion without being + pre-empted. + 2. Hence where a shared data item is updated by a single line of code a lock or + `ThreadSafeQueue` is not needed. In the above code sample, if the application + needs mutual consistency between the dictionary values, a lock must be used. 3. Code running on a thread other than that running `uasyncio` may block for as long as necessary (an application of threading is to handle blocking calls in a way that allows `uasyncio` to continue running). ## 1.3 Threaded code on multiple cores - 1. There is no common VM. The underlying machine code of each core runs - independently. +Currently this applies to RP2 and Unix ports, although as explained above the +thread safe classes offered here do not yet support Unix. + + 1. There is no common VM hence no common GIL. The underlying machine code of + each core runs independently. 2. In the code sample there is a risk of the `uasyncio` task reading the dict at the same moment as it is being written. It may read a corrupt or partially updated item; there may even be a crash. Using a lock or `ThreadSafeQueue` is @@ -105,6 +106,8 @@ interface is via a thread safe class, usually `ThreadSafeFlag`. 3. Code running on a core other than that running `uasyncio` may block for as long as necessary. +[See this reference from @jimmo](https://github.com/orgs/micropython/discussions/10135#discussioncomment-4309865). + ## 1.4 Debugging A key practical point is that coding errors in synchronising threads can be diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 1111eb8..a605621 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -84,8 +84,10 @@ including device drivers, debugging aids, and documentation. 8.4 [Scheduling in uasyncio](./TUTORIAL.md#84-scheduling-in-uasyncio) 8.5 [Why cooperative rather than pre-emptive?](./TUTORIAL.md#85-why-cooperative-rather-than-pre-emptive) 8.6 [Communication](./TUTORIAL.md#86-communication) -9. [Polling vs Interrupts](./TUTORIAL.md#9-polling-vs-interrupts) A common -source of confusion. + 9. [Polling vs Interrupts](./TUTORIAL.md#9-polling-vs-interrupts) A common + source of confusion. + 10. [Interfacing threaded code](./TUTORIAL.md#10-interfacing-threaded-code) Taming blocking functions. Multi core coding. + ###### [Main README](../README.md) @@ -947,18 +949,23 @@ is raised. ## 3.5 Queue -This is currently an unofficial implementation. Its API is a subset of that of -CPython's `asyncio.Queue`. Like `asyncio.Queue` this class is not thread safe. -A queue class optimised for MicroPython is presented in -[Ringbuf queue](./EVENTS.md#7-ringbuf-queue). +Queue objects provide a means of synchronising producer and consumer tasks: the +producer puts data items onto the queue with the consumer removing them. If the +queue becomes full, the producer task will block, likewise if the queue becomes +empty the consumer will block. Some queue implementations allow producer and +consumer to run in different contexts: for example where one runs in an +interrupt service routine or on a different thread or core from the `uasyncio` +application. Such a queue is termed "thread safe". -The `Queue` class provides a means of synchronising producer and consumer -tasks: the producer puts data items onto the queue with the consumer removing -them. If the queue becomes full, the producer task will block, likewise if -the queue becomes empty the consumer will block. +The `Queue` class is an unofficial implementation whose API is a subset of that +of CPython's `asyncio.Queue`. Like `asyncio.Queue` this class is not thread +safe. A queue class optimised for MicroPython is presented in +[Ringbuf queue](./EVENTS.md#7-ringbuf-queue). A thread safe version is +documented in [ThreadSafeQueue](./THREADING.md#22-threadsafequeue). -Constructor: Optional arg `maxsize=0`. If zero, the queue can grow without -limit subject to heap size. If >0 the queue's size will be constrained. +Constructor: +Optional arg `maxsize=0`. If zero, the queue can grow without limit subject to +heap size. If `maxsize>0` the queue's size will be constrained. Synchronous methods (immediate return): * `qsize` No arg. Returns the number of items in the queue. @@ -1093,39 +1100,8 @@ hardware device requires the use of an ISR for a μs level response. Having serviced the device, the ISR flags an asynchronous routine, typically processing received data. -The fact that only one task may wait on a `ThreadSafeFlag` may be addressed as -follows. -```python -class ThreadSafeEvent(asyncio.Event): - def __init__(self): - super().__init__() - self._waiting_on_tsf = False - self._tsf = asyncio.ThreadSafeFlag() - - def set(self): - self._tsf.set() - - async def _waiter(self): # Runs if 1st task is cancelled - await self._tsf.wait() - super().set() - self._waiting_on_tsf = False - - async def wait(self): - if self._waiting_on_tsf == False: - self._waiting_on_tsf = True - await asyncio.sleep(0) # Ensure other tasks see updated flag - try: - await self._tsf.wait() - super().set() - self._waiting_on_tsf = False - except asyncio.CancelledError: - asyncio.create_task(self._waiter()) - raise # Pass cancellation to calling code - else: - await super().wait() -``` -An instance may be set by a hard ISR or from another thread/core. As an `Event` -it can support multiple tasks and must explicitly be cleared. +See [Threadsafe Event](./THREADING.md#31-threadsafe-event) for a thread safe +class which allows multiple tasks to wait on it. ###### [Contents](./TUTORIAL.md#contents) @@ -2883,3 +2859,17 @@ This, along with other issues, is discussed in [Interfacing uasyncio to interrupts](./INTERRUPTS.md). ###### [Contents](./TUTORIAL.md#contents) + +# 10. Interfacing threaded code + +In the context of a `uasyncio` application, the `_thread` module has two main +uses: + 1. Defining code to run on another core (currently restricted to RP2). + 2. Handling blocking functions. The technique assigns the blocking function to + another thread. The `uasyncio` system continues to run, with a single task + paused pending the result of the blocking method. + +These techniques, and thread-safe classes to enable their use, are presented in +[this doc](./THREAD.md). + +###### [Contents](./TUTORIAL.md#contents) From ed8eaa79874f4f6aa07e119d9374e9e455406986 Mon Sep 17 00:00:00 2001 From: adminpete Date: Tue, 13 Dec 2022 17:30:10 +0000 Subject: [PATCH 190/305] Threading docs: Fix links. --- v3/docs/THREADING.md | 6 ++++++ v3/docs/TUTORIAL.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index e2f2aee..7b21b79 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -16,6 +16,9 @@ the `ThreadSafeFlag` class does not work under the Unix build. The classes presented here depend on this: none can be expected to work on Unix until this is fixed. +###### [Main README](../README.md) +###### [Tutorial](./TUTORIAL.md) + # Contents 1. [Introduction](./THREADING.md#1-introduction) The various types of pre-emptive code. @@ -490,6 +493,8 @@ asyncio.run(main()) The `Message` class does not have a queue: if the instance is set, then set again before it is accessed, the first data item will be lost. +###### [Contents](./THREADING.md#contents) + # 4. Taming blocking functions Blocking functions or methods have the potential of stalling the `uasyncio` @@ -548,3 +553,4 @@ async def main(): asyncio.run(main()) ``` +###### [Contents](./THREADING.md#contents) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index a605621..86460d3 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2870,6 +2870,6 @@ uses: paused pending the result of the blocking method. These techniques, and thread-safe classes to enable their use, are presented in -[this doc](./THREAD.md). +[this doc](./THREADING.md). ###### [Contents](./TUTORIAL.md#contents) From c3b61d3bf5f44cbaa50b73bd8b506253ac33d3a6 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 18 Jan 2023 16:09:50 +0000 Subject: [PATCH 191/305] THREADING.md: Initial changes post review. --- v3/docs/THREADING.md | 163 +++++++++++++++++++++++++++++++------------ 1 file changed, 117 insertions(+), 46 deletions(-) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index 7b21b79..cbe3495 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -2,7 +2,7 @@ This document is primarily for those wishing to interface `uasyncio` code with that running under the `_thread` module. It presents classes for that purpose -which may also find use for communicatiing between threads and in interrupt +which may also find use for communicating between threads and in interrupt service routine (ISR) applications. It provides an overview of the problems implicit in pre-emptive multi tasking. @@ -22,10 +22,11 @@ is fixed. # Contents 1. [Introduction](./THREADING.md#1-introduction) The various types of pre-emptive code. - 1.1 [Interrupt Service Routines](./THREADING.md#11-interrupt-service-routines) - 1.2 [Threaded code on one core](./THREADING.md#12-threaded-code-on-one-core) - 1.3 [Threaded code on multiple cores](./THREADING.md#13-threaded-code-on-multiple-cores) - 1.4 [Debugging](./THREADING.md#14-debugging) + 1.1 [Hard Interrupt Service Routines](./THREADING.md#11-hard-interrupt-service-routines) + 1.2 [Soft Interrupt Service Routines](./THREADING.md#12-soft-interrupt-service-routines) Also code scheduled by micropython.schedule() + 1.3 [Threaded code on one core](./THREADING.md#13-threaded-code-on-one-core) + 1.4 [Threaded code on multiple cores](./THREADING.md#14-threaded-code-on-multiple-cores) + 1.5 [Debugging](./THREADING.md#15-debugging) 2. [Sharing data](./THREADING.md#2-sharing-data) 2.1 [A pool](./THREADING.md#21-a-pool) Sharing a set of variables. 2.2 [ThreadSafeQueue](./THREADING.md#22-threadsafequeue) @@ -40,11 +41,19 @@ is fixed. Various issues arise when `uasyncio` applications interface with code running in a different context. Supported contexts are: - 1. A hard or soft interrupt service routine (ISR). - 2. Another thread running on the same core. - 3. Code running on a different core (currently only supported on RP2). - -This section compares the characteristics of the three contexts. Consider this + 1. A hard interrupt service routine (ISR). + 2. A soft ISR. This includes code scheduled by `micropython.schedule()`. + 3. Another thread running on the same core. + 4. Code running on a different core (currently only supported on RP2). + +In all these cases the contexts share a common VM (the virtual machine which +executes Python bytecode). This enables the contexts to share global state. In +case 4 there is no common GIL (the global interpreter lock). This lock protects +Python built-in objects enabling them to be considered atomic at the bytecode +level. (An "atomic" object is inherently thread safe: if thread changes it, +another concurrent thread performing a read is guaranteed to see valid data). + +This section compares the characteristics of the four contexts. Consider this function which updates a global dictionary `d` from a hardware device. The dictionary is shared with a `uasyncio` task. ```python @@ -65,53 +74,100 @@ Beware that some apparently obvious ways to interface an ISR to `uasyncio` introduce subtle bugs discussed in the doc referenced above. The only reliable interface is via a thread safe class, usually `ThreadSafeFlag`. -## 1.1 Interrupt Service Routines +## 1.1 Hard Interrupt Service Routines - 1. The ISR and the main program share a common Python virtual machine (VM). - Consequently a line of code being executed when the interrupt occurs will run - to completion before the ISR runs. + 1. The ISR and the main program share the Python GIL. This ensures that built + in Python objects (`list`, `dict` etc.) will not be corrupted if an ISR runs + while the object is being modified. This guarantee is quite limited: the code + will not crash, but there may be consistency problems. See consistency below. 2. An ISR will run to completion before the main program regains control. This means that if the ISR updates multiple items, when the main program resumes, those items will be mutually consistent. The above code fragment will provide mutually consistent data. 3. The fact that ISR code runs to completion means that it must run fast to avoid disrupting the main program or delaying other ISR's. ISR code should not - call blocking routines and should not wait on locks. Item 2. means that locks - are seldom necessary. + call blocking routines. It should not wait on locks because there is no way + for the interrupted code to release the lock. See locks below. 4. If a burst of interrupts can occur faster than `uasyncio` can schedule the handling task, data loss can occur. Consider using a `ThreadSafeQueue`. Note that if this high rate is sustained something will break: the overall design needs review. It may be necessary to discard some data items. -## 1.2 Threaded code on one core +#### locks + +there is a valid case where a hard ISR checks the status of a lock, aborting if +the lock is set. + +#### consistency + +Consider this code fragment: +```python +a = [0, 0, 0] +b = [0, 0, 0] +def hard_isr(): + a[0] = read_data(0) + b[0] = read_data(1) - 1. On single core devices with a common GIL, Python instructions can be - considered "atomic": they are guaranteed to run to completion without being - pre-empted. - 2. Hence where a shared data item is updated by a single line of code a lock or - `ThreadSafeQueue` is not needed. In the above code sample, if the application - needs mutual consistency between the dictionary values, a lock must be used. - 3. Code running on a thread other than that running `uasyncio` may block for +async def foo(): + while True: + await process(a + b) +``` +A hard ISR can occur during the execution of a bytecode. This means that the +combined list passed to `process()` might comprise old a + new b. + +## 1.2 Soft Interrupt Service Routines + +This also includes code scheduled by `micropython.schedule()`. + + 1. A soft ISR can only run at certain bytecode boundaries, not during + execution of a bytecode. It cannot interrupt garbage collection; this enables + soft ISR code to allocate. + 2. As per hard ISR's. + 3. A soft ISR should still be designed to complete quickly. While it won't + delay hard ISR's it nevertheless pre-empts the main program. In principle it + can wait on a lock, but only if the lock is released by a hard ISR or another + hard context (a thread or code on another core). + 4. As per hard ISR's. + +## 1.3 Threaded code on one core + + 1. The common GIL ensures that built-in Python objects (`list`, `dict` etc.) + will not be corrupted if a read on one thread occurs while the object's + contents are being updated. + 2. This protection does not extend to user defined data structures. The fact + that a dictionary won't be corrupted by concurrent access does not imply that + its contents will be mutually consistent. In the code sample in section 1, if + the application needs mutual consistency between the dictionary values, a lock + is needed to ensure that a read cannot be scheduled while an update is in + progress. + 3. The above means that, for example, calling `uasyncio.create_task` from a + thread is unsafe as it can scramble `uasyncio` data structures. + 4. Code running on a thread other than that running `uasyncio` may block for as long as necessary (an application of threading is to handle blocking calls in a way that allows `uasyncio` to continue running). -## 1.3 Threaded code on multiple cores +## 1.4 Threaded code on multiple cores Currently this applies to RP2 and Unix ports, although as explained above the thread safe classes offered here do not yet support Unix. - 1. There is no common VM hence no common GIL. The underlying machine code of - each core runs independently. + 1. There is no common GIL. This means that under some conditions Python built + in objects can be corrupted. 2. In the code sample there is a risk of the `uasyncio` task reading the dict - at the same moment as it is being written. It may read a corrupt or partially - updated item; there may even be a crash. Using a lock or `ThreadSafeQueue` is - essential. - 3. Code running on a core other than that running `uasyncio` may block for + at the same moment as it is being written. Updating a dictionary data entry is + atomic: there is no risk of corrupt data being read. In the code sample a lock + is only required if mutual consistency of the three values is essential. + 3. In the absence of a GIL some operations on built-in objects are not thread + safe. For example adding or deleting items in a `dict`. This extends to global + variables which are implemented as a `dict`. + 4. The observations in 1.3 on user defined data structures and `uasyncio` + interfacing apply. + 5. Code running on a core other than that running `uasyncio` may block for as long as necessary. [See this reference from @jimmo](https://github.com/orgs/micropython/discussions/10135#discussioncomment-4309865). -## 1.4 Debugging +## 1.5 Debugging A key practical point is that coding errors in synchronising threads can be hard to locate: consequences can be extremely rare bugs or (in the case of @@ -129,10 +185,13 @@ There are two fundamental problems: data sharing and synchronisation. The simplest case is a shared pool of data. It is possible to share an `int` or `bool` because at machine code level writing an `int` is "atomic": it cannot be -interrupted. In the multi core case anything more complex must be protected to -ensure that concurrent access cannot take place. The consequences even of -reading an object while it is being written can be unpredictable. One approach -is to use locking: +interrupted. A shared global `dict` might be replaced in its entirety by one +process and read by another. This is safe because the shared variable is a +pointer, and replacing a pointer is atomic. Problems arise when multiple fields +are updated by one process and read by another, as the read might occur while +the write operation is in progress. + +One approach is to use locking: ```python lock = _thread.allocate_lock() values = { "X": 0, "Y": 0, "Z": 0} @@ -154,14 +213,24 @@ async def consumer(): lock.release() await asyncio.sleep_ms(0) # Ensure producer has time to grab the lock ``` -This is recommended where the producer runs in a different thread from -`uasyncio`. However the consumer might hold the lock for some time: it will -take time for the scheduler to execute the `process()` call, and the call -itself will take time to run. In cases where the duration of a lock is -problematic a `ThreadSafeQueue` is more appropriate as it decouples producer -and consumer code. - -As stated above, if the producer is an ISR no lock is needed or advised. +Condsider also this code: +```python +def consumer(): + send(d["x"].height()) # d is a global dict + send(d["x"].width()) # d["x"] is an instance of a class +``` +In this instance if the producer, running in a different context, changes +`d["x"]` between the two `send()` calls, different objects will be accessed. A +lock should be used. + +Locking is recommended where the producer runs in a different thread from +`uasyncio`. However the consumer might hold the lock for some time: in the +first sample it will take time for the scheduler to execute the `process()` +call, and the call itself will take time to run. In cases where the duration +of a lock is problematic a `ThreadSafeQueue` is more appropriate than a locked +pool as it decouples producer and consumer code. + +As stated above, if the producer is an ISR a lock is normally unusable. Producer code would follow this pattern: ```python values = { "X": 0, "Y": 0, "Z": 0} @@ -170,8 +239,10 @@ def producer(): values["Y"] = sensor_read(1) values["Z"] = sensor_read(2) ``` -and the ISR would run to completion before `uasyncio` resumed, ensuring mutual -consistency of the dict values. +and the ISR would run to completion before `uasyncio` resumed. The ISR could +run while the `uasyncio` task was reading the values: to ensure mutual +consistency of the dict values the consumer should disable interrupts while +the read is in progress. ###### [Contents](./THREADING.md#contents) From a378fec716edbe84d5edaf068eca9db97fecbdd1 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Thu, 19 Jan 2023 10:32:39 +0000 Subject: [PATCH 192/305] THREADING.md: Add information re hard ISR hazards. --- v3/docs/THREADING.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index cbe3495..80efbe7 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -78,8 +78,13 @@ interface is via a thread safe class, usually `ThreadSafeFlag`. 1. The ISR and the main program share the Python GIL. This ensures that built in Python objects (`list`, `dict` etc.) will not be corrupted if an ISR runs - while the object is being modified. This guarantee is quite limited: the code - will not crash, but there may be consistency problems. See consistency below. + while the object's contents are being modified. This guarantee is limited: the + code will not crash, but there may be consistency problems. See consistency + below. Further, failure can occur if the object's _structure_ is modified, for + example by the main program adding or deleting a dictionary entry. Note that + globals are implemented as a `dict`. Globals should be declared before an ISR + starts to run. Alternatively interrupts should be disabled while adding or + deleting a global. 2. An ISR will run to completion before the main program regains control. This means that if the ISR updates multiple items, when the main program resumes, those items will be mutually consistent. The above code fragment will provide @@ -95,7 +100,7 @@ interface is via a thread safe class, usually `ThreadSafeFlag`. #### locks -there is a valid case where a hard ISR checks the status of a lock, aborting if +There is a valid case where a hard ISR checks the status of a lock, aborting if the lock is set. #### consistency @@ -133,7 +138,7 @@ This also includes code scheduled by `micropython.schedule()`. 1. The common GIL ensures that built-in Python objects (`list`, `dict` etc.) will not be corrupted if a read on one thread occurs while the object's - contents are being updated. + contents or the object's structure are being updated. 2. This protection does not extend to user defined data structures. The fact that a dictionary won't be corrupted by concurrent access does not imply that its contents will be mutually consistent. In the code sample in section 1, if @@ -159,7 +164,9 @@ thread safe classes offered here do not yet support Unix. is only required if mutual consistency of the three values is essential. 3. In the absence of a GIL some operations on built-in objects are not thread safe. For example adding or deleting items in a `dict`. This extends to global - variables which are implemented as a `dict`. + variables which are implemented as a `dict`. Creating a new global on one core + while another core reads a different global could fail in the event that the + write operation triggered a re-hash. A lock should be used in such cases. 4. The observations in 1.3 on user defined data structures and `uasyncio` interfacing apply. 5. Code running on a core other than that running `uasyncio` may block for From 87b6cdf6667894beaec710d9d43ca36dc2a6dd6a Mon Sep 17 00:00:00 2001 From: peterhinch Date: Fri, 20 Jan 2023 14:38:22 +0000 Subject: [PATCH 193/305] THREADING.md: Further updata. --- v3/docs/DRIVERS.md | 14 ++-- v3/docs/THREADING.md | 166 +++++++++++++++++++++++++++++++++---------- 2 files changed, 136 insertions(+), 44 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 4009826..559dad2 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -72,7 +72,8 @@ The `primitives.switch` module provides the `Switch` class. This supports debouncing a normally open switch connected between a pin and ground. Can run callbacks or schedule coros on contact closure and/or opening. As an alternative to a callback based interface, bound `Event` objects may be -triggered on switch state changes. +triggered on switch state changes. To use an `Event` based interface +exclusively see the simpler [ESwitch class](./EVENTS.md#61-eswitch). In the following text the term `callable` implies a Python `callable`: namely a function, bound method, coroutine or bound coroutine. The term implies that any @@ -140,7 +141,8 @@ instance. A bound contact closure `Event` is created by passing `None` to This is discussed further in [Event based interface](./DRIVERS.md#8-event-based-interface) which includes a -code example. This API is recommended for new projects. +code example. This API and the simpler [EButton class](./EVENTS.md#62-ebutton) +is recommended for new projects. ###### [Contents](./DRIVERS.md#1-contents) @@ -156,13 +158,15 @@ double-click appears as four voltage changes. The asynchronous `Pushbutton` class provides the logic required to handle these user interactions by monitoring these events over time. -Instances of this class can run a `callable` on on press, release, double-click -or long press events. +Instances of this class can run a `callable` on press, release, double-click or +long press events. As an alternative to callbacks bound `Event` instances may be created which are triggered by press, release, double-click or long press events. This mode of operation is more flexible than the use of callbacks and is covered in -[Event based interface](./DRIVERS.md#8-event-based-interface). +[Event based interface](./DRIVERS.md#8-event-based-interface). To use an +`Event` based interface exclusively see the simpler +[EButton class](./EVENTS.md#62-ebutton). ## 4.1 Pushbutton class diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index 80efbe7..81664ed 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -26,7 +26,8 @@ is fixed. 1.2 [Soft Interrupt Service Routines](./THREADING.md#12-soft-interrupt-service-routines) Also code scheduled by micropython.schedule() 1.3 [Threaded code on one core](./THREADING.md#13-threaded-code-on-one-core) 1.4 [Threaded code on multiple cores](./THREADING.md#14-threaded-code-on-multiple-cores) - 1.5 [Debugging](./THREADING.md#15-debugging) + 1.5 [Globals](./THREADING.md#15-globals) + 1.6 [Debugging](./THREADING.md#16-debugging) 2. [Sharing data](./THREADING.md#2-sharing-data) 2.1 [A pool](./THREADING.md#21-a-pool) Sharing a set of variables. 2.2 [ThreadSafeQueue](./THREADING.md#22-threadsafequeue) @@ -36,6 +37,7 @@ is fixed. 3.1 [Threadsafe Event](./THREADING.md#31-threadsafe-event) 3.2 [Message](./THREADING.md#32-message) A threadsafe event with data payload. 4. [Taming blocking functions](./THREADING.md#4-taming-blocking-functions) + 5. [Glossary](./THREADING.md#5-glossary) Terminology of realtime coding. # 1. Introduction @@ -47,48 +49,47 @@ in a different context. Supported contexts are: 4. Code running on a different core (currently only supported on RP2). In all these cases the contexts share a common VM (the virtual machine which -executes Python bytecode). This enables the contexts to share global state. In -case 4 there is no common GIL (the global interpreter lock). This lock protects -Python built-in objects enabling them to be considered atomic at the bytecode -level. (An "atomic" object is inherently thread safe: if thread changes it, -another concurrent thread performing a read is guaranteed to see valid data). +executes Python bytecode). This enables the contexts to share global state. The +contexts differ in their use of the GIL [see glossary](./THREADING.md#5-glossary). This section compares the characteristics of the four contexts. Consider this function which updates a global dictionary `d` from a hardware device. The -dictionary is shared with a `uasyncio` task. +dictionary is shared with a `uasyncio` task. (The function serves to illustrate +concurrency issues: it is not the most effcient way to transfer data.) ```python def update_dict(): d["x"] = read_data(0) d["y"] = read_data(1) d["z"] = read_data(2) ``` -This might be called in a soft ISR, in a thread running on the same core as -`uasyncio`, or in a thread running on a different core. Each of these contexts -has different characteristics, outlined below. In all these cases "thread safe" -constructs are needed to interface `uasyncio` tasks with code running in these -contexts. The official `ThreadSafeFlag`, or the classes documented here, may be -used in all of these cases. This `update_dict` function serves to illustrate -concurrency issues: it is not the most effcient way to transfer data. +This might be called in a hard or soft ISR, in a thread running on the same +core as `uasyncio`, or in a thread running on a different core. Each of these +contexts has different characteristics, outlined below. In all these cases +"thread safe" constructs are needed to interface `uasyncio` tasks with code +running in these contexts. The official `ThreadSafeFlag`, or the classes +documented here, may be used. Beware that some apparently obvious ways to interface an ISR to `uasyncio` -introduce subtle bugs discussed in the doc referenced above. The only reliable -interface is via a thread safe class, usually `ThreadSafeFlag`. +introduce subtle bugs discussed in +[this doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/INTERRUPTS.md) +referenced above. The only reliable interface is via a thread safe class, +usually `ThreadSafeFlag`. ## 1.1 Hard Interrupt Service Routines - 1. The ISR and the main program share the Python GIL. This ensures that built - in Python objects (`list`, `dict` etc.) will not be corrupted if an ISR runs - while the object's contents are being modified. This guarantee is limited: the - code will not crash, but there may be consistency problems. See consistency - below. Further, failure can occur if the object's _structure_ is modified, for - example by the main program adding or deleting a dictionary entry. Note that - globals are implemented as a `dict`. Globals should be declared before an ISR - starts to run. Alternatively interrupts should be disabled while adding or - deleting a global. + 1. The ISR sees the GIL state of the main program: if the latter has locked + the GIL, the ISR will still run. This renders the GIL, as seen by the ISR, + ineffective. Built in Python objects (`list`, `dict` etc.) will not be + corrupted if an ISR runs while the object's contents are being modified as + these updates are atomic. This guarantee is limited: the code will not crash, + but there may be consistency problems. See **consistency** below. The lack of GIL + functionality means that failure can occur if the object's _structure_ is + modified, for example by the main program adding or deleting a dictionary + entry. This results in issues for [globals](./THREADING.md#15-globals). 2. An ISR will run to completion before the main program regains control. This means that if the ISR updates multiple items, when the main program resumes, those items will be mutually consistent. The above code fragment will provide - mutually consistent data. + mutually consistent data (but see **consistency** below). 3. The fact that ISR code runs to completion means that it must run fast to avoid disrupting the main program or delaying other ISR's. ISR code should not call blocking routines. It should not wait on locks because there is no way @@ -118,11 +119,22 @@ async def foo(): await process(a + b) ``` A hard ISR can occur during the execution of a bytecode. This means that the -combined list passed to `process()` might comprise old a + new b. +combined list passed to `process()` might comprise old a + new b. Even though +the ISR produces consistent data, the fact that it can preempt the main code +at any time means that to read consistent data interrupts must be disabled: +```python +async def foo(): + while True: + state = machine.disable_irq() + d = a + b # Disable for as short a time as possible + machine.enable_irq(state) + await process(d) +``` ## 1.2 Soft Interrupt Service Routines -This also includes code scheduled by `micropython.schedule()`. +This also includes code scheduled by `micropython.schedule()` which is assumed +to have been called from a hard ISR. 1. A soft ISR can only run at certain bytecode boundaries, not during execution of a bytecode. It cannot interrupt garbage collection; this enables @@ -146,7 +158,8 @@ This also includes code scheduled by `micropython.schedule()`. is needed to ensure that a read cannot be scheduled while an update is in progress. 3. The above means that, for example, calling `uasyncio.create_task` from a - thread is unsafe as it can scramble `uasyncio` data structures. + thread is unsafe as it can destroy the mutual consistency of `uasyncio` data + structures. 4. Code running on a thread other than that running `uasyncio` may block for as long as necessary (an application of threading is to handle blocking calls in a way that allows `uasyncio` to continue running). @@ -164,17 +177,48 @@ thread safe classes offered here do not yet support Unix. is only required if mutual consistency of the three values is essential. 3. In the absence of a GIL some operations on built-in objects are not thread safe. For example adding or deleting items in a `dict`. This extends to global - variables which are implemented as a `dict`. Creating a new global on one core - while another core reads a different global could fail in the event that the - write operation triggered a re-hash. A lock should be used in such cases. - 4. The observations in 1.3 on user defined data structures and `uasyncio` + variables which are implemented as a `dict`. See [Globals](./THREADING.md#15-globals). + 4. The observations in 1.3 re user defined data structures and `uasyncio` interfacing apply. 5. Code running on a core other than that running `uasyncio` may block for as long as necessary. [See this reference from @jimmo](https://github.com/orgs/micropython/discussions/10135#discussioncomment-4309865). -## 1.5 Debugging +## 1.5 Globals + +Globals are implemented as a `dict`. Adding or deleting an entry is unsafe in +the main program if there is a context which accesses global data and does not +use the GIL. This means hard ISR's and code running on another core. Given that +shared global data is widely used, the following guidelines should be followed. + +All globals should be declared in the main program before an ISR starts to run, +and before code on another core is started. It is valid to insert placeholder +data, as updates to `dict` data are atomic. In the example below, a pointer to +the `None` object is replaced by a pointer to a class instance: a pointer +update is atomic so can occur while globals are accessed by code in other +contexts. +```python +display_driver = None +# Start code on other core +# It's now valid to do +display_driver = DisplayDriverClass(args) +``` +The hazard with globals can occur in other ways. Importing a module while other +contexts are accessing globals can be problematic as that module might create +global objects. The following would present a hazard if `foo` were run for the +first time while globals were being accessed: +```python +def foo(): + global bar + bar = 42 +``` +Once again the hazard is avoided by, in global scope, populating `bar` prior +with a placeholder before allowing other contexts to run. + +If globals must be created and destroyed dynaically, a lock must be used. + +## 1.6 Debugging A key practical point is that coding errors in synchronising threads can be hard to locate: consequences can be extremely rare bugs or (in the case of @@ -198,7 +242,8 @@ pointer, and replacing a pointer is atomic. Problems arise when multiple fields are updated by one process and read by another, as the read might occur while the write operation is in progress. -One approach is to use locking: +One approach is to use locking. This example solves data sharing, but does not +address synchronisation: ```python lock = _thread.allocate_lock() values = { "X": 0, "Y": 0, "Z": 0} @@ -246,10 +291,10 @@ def producer(): values["Y"] = sensor_read(1) values["Z"] = sensor_read(2) ``` -and the ISR would run to completion before `uasyncio` resumed. The ISR could -run while the `uasyncio` task was reading the values: to ensure mutual -consistency of the dict values the consumer should disable interrupts while -the read is in progress. +and the ISR would run to completion before `uasyncio` resumed. However the ISR +might run while the `uasyncio` task was reading the values: to ensure mutual +consistency of the dict values the consumer should disable interrupts while the +read is in progress. ###### [Contents](./THREADING.md#contents) @@ -632,3 +677,46 @@ async def main(): asyncio.run(main()) ``` ###### [Contents](./THREADING.md#contents) + +# 5. Glossary + +### ISR + +An Interrupt Service Routine: code that runs in response to an interrupt. Hard +ISR's offer very low latency but require careful coding - see +[official docs](http://docs.micropython.org/en/latest/reference/isr_rules.html). + +### Context + +In MicroPython terms a `context` may be viewed as a stream of bytecodes. A +`uasyncio` program comprises a single context: execution is passed between +tasks and the scheduler as a single stream of code. By contrast code in an ISR +can preempt the main stream to run its own stream. This is also true of threads +which can preempt each other at arbitrary times, and code on another core +which runs independently albeit under the same VM. + +### GIL + +MicroPython has a Global Interpreter Lock. The purpose of this is to ensure +that multi-threaded programs cannot cause corruption in the event that two +contexts simultaneously modify an instance of a Python built-in class. It does +not protect user defined objects. + +### micropython.schedule + +The relevance of this is that it is normally called in a hard ISR. In this +case the scheduled code runs in a different context to the main program. See +[official docs](http://docs.micropython.org/en/latest/library/micropython.html#micropython.schedule). + +### VM + +In MicroPython terms a VM is the Virtual Machine that executes bytecode. Code +running in different contexts share a common VM which enables the contexts to +share global objects. + +### Atomic + +An operation is described as "atomic" if it can be guaranteed to proceed to +completion without being preempted. Writing an integer is atomic at the machine +code level. Updating a dictionary value is atomic at bytecode level. Adding or +deleting a dictionary key is not. From 076f0195dc5896a5f95ec1f110d885e94481664a Mon Sep 17 00:00:00 2001 From: peterhinch Date: Fri, 20 Jan 2023 14:42:01 +0000 Subject: [PATCH 194/305] THREADING.md: Further update. --- v3/docs/DRIVERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 559dad2..48b1395 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -141,7 +141,7 @@ instance. A bound contact closure `Event` is created by passing `None` to This is discussed further in [Event based interface](./DRIVERS.md#8-event-based-interface) which includes a -code example. This API and the simpler [EButton class](./EVENTS.md#62-ebutton) +code example. This API and the simpler [ESwitch class](./EVENTS.md#61-eswitch) is recommended for new projects. ###### [Contents](./DRIVERS.md#1-contents) From c0dcacac9457f3124c700089a92df2a2d97dd48b Mon Sep 17 00:00:00 2001 From: peterhinch Date: Tue, 24 Jan 2023 17:07:44 +0000 Subject: [PATCH 195/305] THREADING.md: Correct error re imports. --- v3/docs/THREADING.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index 81664ed..b00f996 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -204,10 +204,8 @@ display_driver = None # It's now valid to do display_driver = DisplayDriverClass(args) ``` -The hazard with globals can occur in other ways. Importing a module while other -contexts are accessing globals can be problematic as that module might create -global objects. The following would present a hazard if `foo` were run for the -first time while globals were being accessed: +The hazard with globals can occur in other ways. The following would present a +hazard if `foo` were run for the first time while globals were being accessed: ```python def foo(): global bar @@ -216,7 +214,7 @@ def foo(): Once again the hazard is avoided by, in global scope, populating `bar` prior with a placeholder before allowing other contexts to run. -If globals must be created and destroyed dynaically, a lock must be used. +If globals must be created and destroyed dynamically, a lock must be used. ## 1.6 Debugging From 2d7881f73f050c11d4308c8d92a4f184f5dea170 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sun, 29 Jan 2023 17:06:05 +0000 Subject: [PATCH 196/305] delay_ms.py: Fix for issue 98. --- v3/primitives/delay_ms.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py index bfed02d..d651412 100644 --- a/v3/primitives/delay_ms.py +++ b/v3/primitives/delay_ms.py @@ -75,6 +75,7 @@ def callback(self, func=None, args=()): self._args = args def deinit(self): - self.stop() - self._mtask.cancel() - self._mtask = None + if self._mtask is not None: # https://github.com/peterhinch/micropython-async/issues/98 + self.stop() + self._mtask.cancel() + self._mtask = None From 121663836fa3c88676f256ba9a06b651aff85627 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Fri, 3 Feb 2023 15:53:07 +0000 Subject: [PATCH 197/305] EVENTS.md: Fix bug in wait_any example. --- v3/docs/EVENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 9cf1eac..0819716 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -165,7 +165,7 @@ the instance's `.event()` method. ```python from primitives import WaitAny async def foo(elo1, elo2) - evt = WaitAny((elo1, elo2)).wait() + evt = await WaitAny((elo1, elo2)).wait() if evt is elo1: # Handle elo1 ``` From 3d40a66fcc3542beb8bddc9c3dd8bdec35652ef9 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 8 Feb 2023 10:20:18 +0000 Subject: [PATCH 198/305] README.md: Reference THREADING.md. --- v3/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/v3/README.md b/v3/README.md index 38d5b44..d83ab5d 100644 --- a/v3/README.md +++ b/v3/README.md @@ -22,6 +22,10 @@ is a guide to interfacing interrupts to `uasyncio`. applications and device drivers which largely does away with callbacks. Assumes some knowledge of `uasyncio`. +[Threading](./docs/THREADING.md) is a guide to the use of multi-threaded and +multi-core programming. Code is offered to enable a `uasyncio` application to +deal with blocking functions. + ## 1.2 Debugging tools [aiorepl](https://github.com/micropython/micropython-lib/tree/master/micropython/aiorepl) From ab6cbb6e41aad0c012a8b21a0f5dbd13f633a1a7 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Fri, 17 Feb 2023 15:32:25 +0000 Subject: [PATCH 199/305] Minor doc changes. --- v3/README.md | 24 ++++++++++++++++++------ v3/docs/TUTORIAL.md | 16 +++++++--------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/v3/README.md b/v3/README.md index d83ab5d..f42711d 100644 --- a/v3/README.md +++ b/v3/README.md @@ -19,8 +19,8 @@ and incremental encoders. is a guide to interfacing interrupts to `uasyncio`. [Event-based programming](./docs/EVENTS.md) is a guide to a way of writing -applications and device drivers which largely does away with callbacks. Assumes -some knowledge of `uasyncio`. +applications and device drivers which largely does away with callbacks. The doc +assumes some knowledge of `uasyncio`. [Threading](./docs/THREADING.md) is a guide to the use of multi-threaded and multi-core programming. Code is offered to enable a `uasyncio` application to @@ -51,12 +51,24 @@ Documented in the [tutorial](./docs/TUTORIAL.md). Documented in the [tutorial](./docs/TUTORIAL.md). Comprises: * Implementations of unsupported CPython primitives including `barrier`, `queue` and others. - * An additional primitive `Message`. * A software retriggerable monostable timer class `Delay_ms`, similar to a watchdog. * Two primitives enabling waiting on groups of `Event` instances. -### 1.3.3 Asynchronous device drivers +### 1.3.3 Threadsafe primitives + +[This doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/THREADING.md) +describes issues linking `uasyncio` code with code running on other cores or in +other threads. The `threadsafe` directory provides: + + * A threadsafe primitive `Message`. + * `ThreadSafeQueue` + * `ThreadSafeEvent` Extends `ThreadsafeFlag`. + +The doc also provides code to enable `uasyncio` to handle blocking functions +using threading. + +### 1.3.4 Asynchronous device drivers These are documented [here](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/DRIVERS.md): @@ -64,13 +76,13 @@ These are documented * Drivers for ADC's * Drivers for incremental encoders. -### 1.3.4 A scheduler +### 1.3.5 A scheduler This [lightweight scheduler](./docs/SCHEDULE.md) enables tasks to be scheduled at future times. These can be assigned in a flexible way: a task might run at 4.10am on Monday and Friday if there's no "r" in the month. -### 1.3.5 Asynchronous interfaces +### 1.3.6 Asynchronous interfaces These device drivers are intended as examples of asynchronous code which are useful in their own right: diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 86460d3..926b268 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -11,7 +11,7 @@ including device drivers, debugging aids, and documentation. # Contents 0. [Introduction](./TUTORIAL.md#0-introduction) - 0.1 [Installing uasyncio](./TUTORIAL.md#01-installing-uasyncio) + 0.1 [Installing uasyncio](./TUTORIAL.md#01-installing-uasyncio) Also the optional extensions. 1. [Cooperative scheduling](./TUTORIAL.md#1-cooperative-scheduling) 1.1 [Modules](./TUTORIAL.md#11-modules)      1.1.1 [Primitives](./TUTORIAL.md#111-primitives) @@ -118,14 +118,12 @@ CPython V3.8 and above. ## 0.1 Installing uasyncio -Firmware builds after V1.13 incorporate `uasyncio`. The version may be checked -by issuing at the REPL: -```python -import uasyncio -print(uasyncio.__version__) -``` -Version 3 will print a version number. Older versions will throw an exception: -installing updated firmware is highly recommended. +Firmware builds after V1.13 incorporate `uasyncio`. Check the firmware version +number reported on boot and upgrade if necessary. + +This repository has optional unofficial primitives and extensions. To install +these the repo should be cloned to a PC. The directories `primitives` and +`threadsafe` (with contents) should be copied to the hardware plaform. ###### [Main README](../README.md) From a22b1a330b5b923438fefc073233998c4d4b871a Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 22 Feb 2023 18:11:36 +0000 Subject: [PATCH 200/305] THREADING.md: Add section on stream devices. --- v3/docs/THREADING.md | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index b00f996..5aa58e1 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -36,8 +36,9 @@ is fixed. 3. [Synchronisation](./THREADING.md#3-synchronisation) 3.1 [Threadsafe Event](./THREADING.md#31-threadsafe-event) 3.2 [Message](./THREADING.md#32-message) A threadsafe event with data payload. - 4. [Taming blocking functions](./THREADING.md#4-taming-blocking-functions) - 5. [Glossary](./THREADING.md#5-glossary) Terminology of realtime coding. + 4. [Taming blocking functions](./THREADING.md#4-taming-blocking-functions) Enabling uasyncio to handle blocking code. + 5. [Sharing a stream device](./THREADING.md#4-sharing-a-stream-device) + 6. [Glossary](./THREADING.md#5-glossary) Terminology of realtime coding. # 1. Introduction @@ -676,7 +677,33 @@ asyncio.run(main()) ``` ###### [Contents](./THREADING.md#contents) -# 5. Glossary +# 5. Sharing a stream device + +Typical stream devices are a UART or a socket. These are typically employed to +exchange multi-byte messages between applications running on different systems. + +When sharing a stream device between concurrent functions, similar issues arise +whether the functions are `uasyncio` tasks or code with hard concurrency. In +the case of transmission of multi-character messages a lock must be used to +ensure that transmitted characters cannot become interleaved. + +In theory a lock can also be used for reception, but in practice it is rarely +feasible. Synchronising multiple receiving tasks is hard. This is because the +receiving processes seldom have precise control over the timing of the +(remote) transmitting device. It is therefore hard to determine when to +initiate each receiving process. If there is a requirement to handle +communication errors, the difficulties multiply. + +The usual approach is to design the message format to enable the intended +receiving process to be determined from the message contents. The application +has a single receiving task. This parses incoming messages and routes them to +the appropriate destination. Routing may be done by the data sharing mechanisms +discussed above. Error handling may be done by the receiving process or passed +on to the message destination. + +###### [Contents](./THREADING.md#contents) + +# 6. Glossary ### ISR From 2ef4a6a0c9f2ca814670196e3d0216c626db479b Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 22 Feb 2023 18:14:12 +0000 Subject: [PATCH 201/305] THREADING.md: Add section on stream devices. --- v3/docs/THREADING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index 5aa58e1..4e6c168 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -37,8 +37,8 @@ is fixed. 3.1 [Threadsafe Event](./THREADING.md#31-threadsafe-event) 3.2 [Message](./THREADING.md#32-message) A threadsafe event with data payload. 4. [Taming blocking functions](./THREADING.md#4-taming-blocking-functions) Enabling uasyncio to handle blocking code. - 5. [Sharing a stream device](./THREADING.md#4-sharing-a-stream-device) - 6. [Glossary](./THREADING.md#5-glossary) Terminology of realtime coding. + 5. [Sharing a stream device](./THREADING.md#5-sharing-a-stream-device) + 6. [Glossary](./THREADING.md#6-glossary) Terminology of realtime coding. # 1. Introduction From 6f1bf52f2e50fd90053926fceda9063d59ff50f4 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sat, 1 Apr 2023 17:33:05 +0100 Subject: [PATCH 202/305] sched: Rewrite crontest.py. Add simulate.py. Docs to follow. --- v3/as_drivers/sched/cron.py | 4 +- v3/as_drivers/sched/crontest.py | 148 ++++++++++++++++++++------------ v3/as_drivers/sched/simulate.py | 40 +++++++++ 3 files changed, 133 insertions(+), 59 deletions(-) create mode 100644 v3/as_drivers/sched/simulate.py diff --git a/v3/as_drivers/sched/cron.py b/v3/as_drivers/sched/cron.py index e0ddeae..b958a92 100644 --- a/v3/as_drivers/sched/cron.py +++ b/v3/as_drivers/sched/cron.py @@ -1,6 +1,6 @@ # cron.py -# Copyright (c) 2020 Peter Hinch +# Copyright (c) 2020-2023 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file from time import mktime, localtime @@ -93,7 +93,7 @@ def inner(tnow): md += toff % 7 # mktime handles md > 31 but month may increment tev = mktime((yr, mo, md, h, m, s, wd, 0)) cur_mo = mo - _, mo = localtime(tev)[:2] # get month + mo = localtime(tev)[1] # get month if mo != cur_mo: toff = do_arg(month, mo) # Get next valid month mo += toff # Offset is relative to new, incremented month diff --git a/v3/as_drivers/sched/crontest.py b/v3/as_drivers/sched/crontest.py index 7638614..ea91b2c 100644 --- a/v3/as_drivers/sched/crontest.py +++ b/v3/as_drivers/sched/crontest.py @@ -1,72 +1,106 @@ -# crontest.py +# crontest.py Now works under Unix build -# Copyright (c) 2020 Peter Hinch +# Copyright (c) 2020-2023 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -from time import time, ticks_diff, ticks_us, localtime +from time import time, ticks_diff, ticks_us, localtime, mktime from sched.cron import cron import sys maxruntime = 0 fail = 0 -def result(t, msg): - global fail - if t != next(iexp): - print('FAIL', msg, t) - fail += 1 - return - print('PASS', msg, t) -def test(*, secs=0, mins=0, hrs=3, mday=None, month=None, wday=None, tsource=None): - global maxruntime - ts = int(time() if tsource is None else tsource) # int() for Unix build +# Args: +# ts Time of run in secs since epoch +# exp Expected absolute end time (yr, mo, md, h, m, s) +# msg Message describing test +# kwargs are args for cron +def test(ts, exp, msg, *, secs=0, mins=0, hrs=3, mday=None, month=None, wday=None): + global maxruntime, fail + texp = mktime(exp + (0, 0)) # Expected absolute end time + yr, mo, md, h, m, s, wd = localtime(texp)[:7] + print(f"Test: {msg}") + print(f"Expected endtime: {h:02d}:{m:02d}:{s:02d} on {md:02d}/{mo:02d}/{yr:02d}") + cg = cron(secs=secs, mins=mins, hrs=hrs, mday=mday, month=month, wday=wday) start = ticks_us() - t = cg(ts) # Time relative to ts + t = cg(ts) # Wait duration returned by cron (secs) delta = ticks_diff(ticks_us(), start) maxruntime = max(maxruntime, delta) - print('Runtime = {}μs'.format(delta)) - tev = t + ts # Absolute time of 1st event - yr, mo, md, h, m, s, wd = localtime(tev)[:7] - print('{:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}'.format(h, m, s, md, mo, yr)) - return t # Relative time - -now = 1596074400 if sys.platform == 'linux' else 649393200 # 3am Thursday (day 3) 30 July 2020 -iexp = iter([79500, 79500, 86700, 10680, 13564800, 17712000, - 12781800, 11217915, 5443200, 21600, 17193600, - 18403200, 5353140, 13392000, 18662400]) -# Expect 01:05:00 on 31/07/2020 -result(test(wday=4, hrs=(1,2), mins=5, tsource=now), 'wday and time both cause 1 day increment.') -# 01:05:00 on 31/07/2020 -result(test(hrs=(1,2), mins=5, tsource=now), 'time causes 1 day increment.') -# 03:05:00 on 31/07/2020 -result(test(wday=4, mins=5, tsource=now), 'wday causes 1 day increment.') -# 05:58:00 on 30/07/2020 -result(test(hrs=(5, 23), mins=58, tsource=now), 'time increment no day change.') -# 03:00:00 on 03/01/2021 -result(test(month=1, wday=6, tsource=now), 'month and year rollover, 1st Sunday') -# 03:00:00 on 20/02/2021 -result(test(month=2, mday=20, tsource=now), 'month and year rollover, mday->20 Feb') -# 01:30:00 on 25/12/2020 -result(test(month=12, mday=25, hrs=1, mins=30, tsource=now), 'Forward to Christmas day, hrs backwards') -# 23:05:15 on 06/12/2020 -result(test(month=12, wday=6, hrs=23, mins=5, secs=15, tsource=now), '1st Sunday in Dec 2020') -# 03:00:00 on 01/10/2020 -result(test(month=10, tsource=now), 'Current time on 1st Oct 2020') -# 09:00:00 on 30/07/2020 -result(test(month=7, hrs=9, tsource=now), 'Explicitly specify current month') -# 03:00:00 on 14/02/2021 -result(test(month=2, mday=8, wday=6, tsource=now), 'Second Sunday in February 2021') -# 03:00:00 on 28/02/2021 -result(test(month=2, mday=22, wday=6, tsource=now), 'Fourth Sunday in February 2021') # last day of month -# 01:59:00 on 01/10/2020 -result(test(month=(7, 10), hrs=1, mins=59, tsource=now + 24*3600), 'Time causes month rollover to next legal month') -# 03:00:00 on 01/01/2021 -result(test(month=(7, 1), mday=1, tsource=now), 'mday causes month rollover to next year') -# 03:00:00 on 03/03/2021 -result(test(month=(7, 3), wday=(2, 6), tsource=now), 'wday causes month rollover to next year') -print('Max runtime {}μs'.format(maxruntime)) + yr, mo, md, h, m, s, wd = localtime(t + ts)[:7] # Get absolute time from cron + print(f"Endtime from cron: {h:02d}:{m:02d}:{s:02d} on {md:02d}/{mo:02d}/{yr:02d}") + if t == texp - ts: + print(f"PASS") + else: + print(f"FAIL [{t}]") + fail += 1 + print(f"Runtime = {delta}us\n") + + +now = mktime((2020, 7, 30, 3, 0, 0, 0, 0)) # 3am Thursday (day 3) 30 July 2020 + +exp = (2020, 7, 31, 1, 5, 0) # Expect 01:05:00 on 31/07/2020 +msg = "wday and time both cause 1 day increment." +test(now, exp, msg, wday=4, hrs=(1, 2), mins=5) + +exp = (2020, 7, 31, 1, 5, 0) # 01:05:00 on 31/07/2020 +msg = "time causes 1 day increment." +test(now, exp, msg, hrs=(1, 2), mins=5) + +exp = (2020, 7, 31, 3, 5, 0) # 03:05:00 on 31/07/2020 +msg = "wday causes 1 day increment." +test(now, exp, msg, wday=4, mins=5) + +exp = (2020, 7, 30, 5, 58, 0) # 05:58:00 on 30/07/2020 +msg = "time increment no day change." +test(now, exp, msg, hrs=(5, 23), mins=58) + +exp = (2021, 1, 3, 3, 0, 0) # 03:00:00 on 03/01/2021 +msg = "month and year rollover, 1st Sunday" +test(now, exp, msg, month=1, wday=6) + +exp = (2021, 2, 20, 3, 0, 0) # 03:00:00 on 20/02/2021 +msg = "month and year rollover, mday->20 Feb" +test(now, exp, msg, month=2, mday=20) + +exp = (2020, 12, 25, 1, 30, 0) # 01:30:00 on 25/12/2020 +msg = "Forward to Xmas day, hrs backwards" +test(now, exp, msg, month=12, mday=25, hrs=1, mins=30) + +exp = (2020, 12, 6, 23, 5, 15) # 23:05:15 on 06/12/2020 +msg = "1st Sunday in Dec 2020" +test(now, exp, msg, month=12, wday=6, hrs=23, mins=5, secs=15) + +exp = (2020, 10, 1, 3, 0, 0) # 03:00:00 on 01/10/2020 +msg = "Current time on 1st Oct 2020" +test(now, exp, msg, month=10) + +exp = (2020, 7, 30, 9, 0, 0) # 09:00:00 on 30/07/2020 +msg = "Explicitly specify current month" +test(now, exp, msg, month=7, hrs=9) + +exp = (2021, 2, 14, 3, 0, 0) # 03:00:00 on 14/02/2021 +msg = "Second Sunday in February 2021" +test(now, exp, msg, month=2, mday=8, wday=6) + +exp = (2021, 2, 28, 3, 0, 0) # 03:00:00 on 28/02/2021 +msg = "Fourth Sunday in February 2021" +test(now, exp, msg, month=2, mday=22, wday=6) # month end + +exp = (2020, 10, 1, 1, 59, 0) # 01:59:00 on 01/10/2020 +msg = "Time causes month rollover to next legal month" +test(now + 24 * 3600, exp, msg, month=(7, 10), hrs=1, mins=59) + +exp = (2021, 1, 1, 3, 0, 0) # 03:00:00 on 01/01/2021 +msg = "mday causes month rollover to next year" +test(now, exp, msg, month=(7, 1), mday=1) + +exp = (2021, 3, 3, 3, 0, 0) # 03:00:00 on 03/03/2021 +msg = "wday causes month rollover to next year" +test(now, exp, msg, month=(7, 3), wday=(2, 6)) + +print(f"Max runtime {maxruntime}us") if fail: - print(fail, 'FAILURES OCCURRED') + print(fail, "FAILURES OCCURRED") else: - print('ALL TESTS PASSED') + print("ALL TESTS PASSED") diff --git a/v3/as_drivers/sched/simulate.py b/v3/as_drivers/sched/simulate.py new file mode 100644 index 0000000..5bf4fb5 --- /dev/null +++ b/v3/as_drivers/sched/simulate.py @@ -0,0 +1,40 @@ +# simulate.py Adapt this to simulate scheduled sequences + +from time import localtime, mktime +from sched.cron import cron + +days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") +tim = 0 # Global time in secs + +def print_time(msg=""): + yr, mo, md, h, m, s, wd = localtime(tim)[:7] + print(f"{msg} {h:02d}:{m:02d}:{s:02d} on {days[wd]} {md:02d}/{mo:02d}/{yr:02d}") + +def wait(cr): # Simulate waiting on a cron instance + global tim + tim += 2 # Must always wait >=2s before calling cron again + dt = cr(tim) + hrs, m_s = divmod(dt + 2, 3600) # For neat display add back the 2 secs + mins, secs = divmod(m_s, 60) + print(f"Wait {hrs}hrs {mins}mins {secs}s") + tim += dt + print_time("Time now:") + +def set_time(y, month, mday, hrs, mins, secs): + global tim + tim = mktime((y, month, mday, hrs, mins, secs, 0, 0)) + print_time("Start at:") + +# Adapt the following to emulate the proposed application. Cron args +# secs=0, mins=0, hrs=3, mday=None, month=None, wday=None + +def sim(*args): + set_time(*args) + cs = cron(hrs = 0, mins = 59) + wait(cs) + cn = cron(wday=(0, 5), hrs=(1, 10), mins = range(0, 60, 15)) + for _ in range(10): + wait(cn) + print("Run payload.\n") + +sim(2023, 3, 29, 15, 20, 0) # Start time: year, month, mday, hrs, mins, secs From a9a5e3a62a4ead9b3024682a4e962a48ec2cac2a Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sun, 2 Apr 2023 12:55:21 +0100 Subject: [PATCH 203/305] Schedule: draft SCEDULE.md. --- v3/docs/SCHEDULE.md | 80 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index 448f0f0..950559d 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -10,11 +10,13 @@      4.2.2 [Time causing month rollover](./SCHEDULE.md#422-time-causing-month-rollover) 4.3 [Limitations](./SCHEDULE.md#43-limitations) 4.4 [The Unix build](./SCHEDULE.md#44-the-unix-build) + 4.5 [Initialisation](./SCHEDULE.md#45-initialisation)__ 5. [The cron object](./SCHEDULE.md#5-the-cron-object) For hackers and synchronous coders 5.1 [The time to an event](./SCHEDULE.md#51-the-time-to-an-event) 5.2 [How it works](./SCHEDULE.md#52-how-it-works) 6. [Hardware timing limitations](./SCHEDULE.md#6-hardware-timing-limitations) - 7. [Use in synchronous code](./SCHEDULE.md#7-use-in-synchronous-code) If you really must + 7. [Use in synchronous code](./SCHEDULE.md#7-use-in-synchronous-code) If you really must. + 8. [The simulate script](./SCHEDULE.md#8-the-simulate-script) Rapidly test sequences. ##### [Tutorial](./TUTORIAL.md#contents) ##### [Main V3 README](../README.md) @@ -37,7 +39,7 @@ adapt the example code. It can be used in synchronous code and an example is provided. It is cross-platform and has been tested on Pyboard, Pyboard D, ESP8266, ESP32 -and the Unix build (the latter is subject to a minor local time issue). +and the Unix build. # 2. Overview @@ -59,8 +61,8 @@ instances must explicitly be created. # 3. Installation Copy the `sched` directory and contents to the target's filesystem. It requires -`uasyncio` V3 which is included in daily firmware builds. It will be in release -builds after V1.12. +`uasyncio` V3 which is included in daily firmware builds and in release builds +after V1.12. To install to an SD card using [rshell](https://github.com/dhylands/rshell) move to the parent directory of `sched` and issue: @@ -76,11 +78,12 @@ The following files are installed in the `sched` directory. 4. `asynctest.py` Demo of asynchronous scheduling. 5. `synctest.py` Synchronous scheduling demo. For `uasyncio` phobics only. 6. `crontest.py` A test for `cron.py` code. - 7. `__init__.py` Empty file for Python package. + 7. `simulate.py` A simple script which may be adapted to prove that a `cron` + instance will behave as expected. See [The simulate script](./SCHEDULE.md#8-the-simulate-script). + 8. `__init__.py` Empty file for Python package. The `crontest` script is only of interest to those wishing to adapt `cron.py`. -To run error-free a bare metal target should be used for the reason discussed -[here](./SCHEDULE.md#46-the-unix-build). +It will run on any MicroPython target. # 4. The schedule function @@ -158,7 +161,8 @@ The args may be of the following types. 3. An object supporting the Python iterator protocol and iterating over integers. For example `hrs=(3, 17)` will cause events to occur at 3am and 5pm, `wday=range(0, 5)` specifies weekdays. Tuples, lists, ranges or sets may be - passed. + passed. If using this feature please see + [Initialisation](./SCHEDULE.md#45-initialisation). Legal integer values are listed above. Basic validation is done as soon as `schedule` is run. @@ -236,18 +240,34 @@ Asynchronous use requires `uasyncio` V3, so ensure this is installed on the Linux target. The synchronous and asynchronous demos run under the Unix build. The module is -usable on Linux provided the daylight saving time (DST) constraints below are -met. - -A consequence of DST is that there are impossible times when clocks go forward +usable on Linux provided the daylight saving time (DST) constraints are met. A +consequence of DST is that there are impossible times when clocks go forward and duplicates when they go back. Scheduling those times will fail. A solution is to avoid scheduling the times in your region where this occurs (01.00.00 to 02.00.00 in March and October here). -The `crontest.py` test program produces failures under Unix. These result from -the fact that the Unix `localtime` function handles daylight saving time. The -purpose of `crontest.py` is to check `cron` code. It should be run on bare -metal targets. +## 4.5 Initialisation + +Where a time specifier is an iterator (e.g. `mins=range(0, 60, 15)`) and there +are additional constraints (e.g. `hrs=3`) it may be necessary to delay the +start. The problem is specific to scheduling a sequence at a future time, and +there is a simple solution. + +A `cron` object searches forwards from the current time. Assume the above case. +If the code start at 7:05 it picks the first later minute in the `range`, +i.e. `mins=15`, then picks the hour. This means that the first trigger occurs +at 3:15. Subsequent behaviour will be correct, but the first trigger would be +expected at 3:00. The solution is to delay start until the minutes value is in +the range`45 < mins <= 59`. The `hours` value is immaterial but a reasonable +general solution is to delay until just before the first expected callback: + +```python +async def run(): + asyncio.create_task(schedule(payload, args, hrs=3, mins=range(0, 60, 15))) + +async def delay_start(): + asyncio.create_task(schedule(run, hrs=2, mins=55, times=1)) +``` ##### [Top](./SCHEDULE.md#0-contents) @@ -300,7 +320,7 @@ example). # 6. Hardware timing limitations -The code has been tested on Pyboard 1.x, Pyboard D, ESP32 and ESP8266. All +The code has been tested on Pyboard 1.x, Pyboard D, RP2, ESP32 and ESP8266. All except ESP8266 have good timing performance. Pyboards can be calibrated to timepiece precision using a cheap DS3231 and [this utility](https://github.com/peterhinch/micropython-samples/tree/master/DS3231). @@ -377,4 +397,30 @@ available to the application including cancellation of scheduled tasks. The above code is incompatible with `uasyncio` because of the blocking calls to `time.sleep()`. +If scheduling a sequence to run at a future time please see +[Initialisation](./SCHEDULE.md#45-initialisation). + +##### [Top](./SCHEDULE.md#0-contents) + +# 8. The simulate script + +This enables the behaviour of sets of args to `schedule` to be rapidly checked. +The `sim` function should be adapted to reflect the application specifics. The +default is: +```python +def sim(*args): + set_time(*args) + cs = cron(hrs = 0, mins = 59) + wait(cs) + cn = cron(wday=(0, 5), hrs=(1, 10), mins = range(0, 60, 15)) + for _ in range(10): + wait(cn) + print("Run payload.\n") + +sim(2023, 3, 29, 15, 20, 0) # Start time: year, month, mday, hrs, mins, secs +``` +The `wait` function returns immediately, but prints the length of the delay and +the value of system time when the delay ends. In this instance the start of a +sequence is delayed to ensure that the first trigger occurs at 01:00. + ##### [Top](./SCHEDULE.md#0-contents) From 24ec5da29f681022954364754d90e83d2be2d218 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Mon, 3 Apr 2023 19:15:51 +0100 Subject: [PATCH 204/305] Schedule: schedule.py fix iss. 100. --- v3/as_drivers/sched/cron.py | 9 ++++- v3/as_drivers/sched/sched.py | 31 +++++++++++---- v3/docs/SCHEDULE.md | 75 +++++++++++++++++++++--------------- 3 files changed, 75 insertions(+), 40 deletions(-) diff --git a/v3/as_drivers/sched/cron.py b/v3/as_drivers/sched/cron.py index b958a92..0d853b1 100644 --- a/v3/as_drivers/sched/cron.py +++ b/v3/as_drivers/sched/cron.py @@ -3,6 +3,11 @@ # Copyright (c) 2020-2023 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file +# A cron is instantiated with sequence specifier args. An instance accepts an integer time +# value (in secs since epoch) and returns the number of seconds to wait for a matching time. +# It holds no state. +# See docs for restrictions and limitations. + from time import mktime, localtime # Validation _valid = ((0, 59, 'secs'), (0, 59, 'mins'), (0, 23, 'hrs'), @@ -28,8 +33,8 @@ def do_arg(a, cv): # Arg, current value raise ValueError('Invalid None value for secs') if not isinstance(secs, int) and len(secs) > 1: # It's an iterable ss = sorted(secs) - if min((a[1] - a[0] for a in zip(ss, ss[1:]))) < 2: - raise ValueError("Can't have consecutive seconds.", last, x) + if min((a[1] - a[0] for a in zip(ss, ss[1:]))) < 10: + raise ValueError("Seconds values must be >= 10s apart.") args = (secs, mins, hrs, mday, month, wday) # Validation for all args valid = iter(_valid) vestr = 'Argument {} out of range' diff --git a/v3/as_drivers/sched/sched.py b/v3/as_drivers/sched/sched.py index 2993d5b..64ae773 100644 --- a/v3/as_drivers/sched/sched.py +++ b/v3/as_drivers/sched/sched.py @@ -1,21 +1,36 @@ # sched.py -# Copyright (c) 2020 Peter Hinch +# Copyright (c) 2020-2023 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file import uasyncio as asyncio from sched.primitives import launch -from time import time +from time import time, mktime, localtime from sched.cron import cron + +# uasyncio can't handle long delays so split into 1000s (1e6 ms) segments +_MAXT = const(1000) +# Wait prior to a sequence start +_PAUSE = const(2) + async def schedule(func, *args, times=None, **kwargs): - fcron = cron(**kwargs) - maxt = 1000 # uasyncio can't handle arbitrarily long delays + async def long_sleep(t): # Sleep with no bounds. Immediate return if t < 0. + while t > 0: + await asyncio.sleep(min(t, _MAXT)) + t -= _MAXT + + tim = mktime(localtime()[:3] + (0, 0, 0, 0, 0)) # Midnight last night + now = round(time()) # round() is for Unix + fcron = cron(**kwargs) # Cron instance for search. + while tim < now: # Find first event in sequence + # Defensive. fcron should never return 0, but if it did the loop would never quit + tim += max(fcron(tim), 1) + await long_sleep(tim - now - _PAUSE) # Time to wait (can be < 0) + while times is None or times > 0: - tw = fcron(int(time())) # Time to wait (s) - while tw > 0: # While there is still time to wait - await asyncio.sleep(min(tw, maxt)) - tw -= maxt + tw = fcron(round(time())) # Time to wait (s) + await long_sleep(tw) res = launch(func, args) if times is not None: times -= 1 diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index 950559d..77d87b7 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -10,14 +10,18 @@      4.2.2 [Time causing month rollover](./SCHEDULE.md#422-time-causing-month-rollover) 4.3 [Limitations](./SCHEDULE.md#43-limitations) 4.4 [The Unix build](./SCHEDULE.md#44-the-unix-build) - 4.5 [Initialisation](./SCHEDULE.md#45-initialisation)__ 5. [The cron object](./SCHEDULE.md#5-the-cron-object) For hackers and synchronous coders 5.1 [The time to an event](./SCHEDULE.md#51-the-time-to-an-event) 5.2 [How it works](./SCHEDULE.md#52-how-it-works) 6. [Hardware timing limitations](./SCHEDULE.md#6-hardware-timing-limitations) 7. [Use in synchronous code](./SCHEDULE.md#7-use-in-synchronous-code) If you really must. + 7.1 [Initialisation](./SCHEDULE.md#71-initialisation)__ 8. [The simulate script](./SCHEDULE.md#8-the-simulate-script) Rapidly test sequences. +Release note: +3rd April 2023 Fix issue #100. Where an iterable is passed to `secs`, triggers +must now be at least 10s apart (formerly 2s). + ##### [Tutorial](./TUTORIAL.md#contents) ##### [Main V3 README](../README.md) @@ -161,16 +165,20 @@ The args may be of the following types. 3. An object supporting the Python iterator protocol and iterating over integers. For example `hrs=(3, 17)` will cause events to occur at 3am and 5pm, `wday=range(0, 5)` specifies weekdays. Tuples, lists, ranges or sets may be - passed. If using this feature please see - [Initialisation](./SCHEDULE.md#45-initialisation). + passed. Legal integer values are listed above. Basic validation is done as soon as `schedule` is run. Note the implications of the `None` wildcard. Setting `mins=None` will schedule the event to occur on every minute (equivalent to `*` in a Unix cron table). -Setting `secs=None` or consecutive seconds values will cause a `ValueError` - -events must be at least two seconds apart. +Setting `secs=None` will cause a `ValueError`. + +Passing an iterable to `secs` is not recommended: this library is intended for +scheduling relatively long duration events. For rapid sequencing, schedule a +coroutine which awaits `uasyncio` `sleep` or `sleep_ms` routines. If an +iterable is passed, triggers must be at least ten seconds apart or a +`ValueError` will result. Default values schedule an event every day at 03.00.00. @@ -246,29 +254,6 @@ and duplicates when they go back. Scheduling those times will fail. A solution is to avoid scheduling the times in your region where this occurs (01.00.00 to 02.00.00 in March and October here). -## 4.5 Initialisation - -Where a time specifier is an iterator (e.g. `mins=range(0, 60, 15)`) and there -are additional constraints (e.g. `hrs=3`) it may be necessary to delay the -start. The problem is specific to scheduling a sequence at a future time, and -there is a simple solution. - -A `cron` object searches forwards from the current time. Assume the above case. -If the code start at 7:05 it picks the first later minute in the `range`, -i.e. `mins=15`, then picks the hour. This means that the first trigger occurs -at 3:15. Subsequent behaviour will be correct, but the first trigger would be -expected at 3:00. The solution is to delay start until the minutes value is in -the range`45 < mins <= 59`. The `hours` value is immaterial but a reasonable -general solution is to delay until just before the first expected callback: - -```python -async def run(): - asyncio.create_task(schedule(payload, args, hrs=3, mins=range(0, 60, 15))) - -async def delay_start(): - asyncio.create_task(schedule(run, hrs=2, mins=55, times=1)) -``` - ##### [Top](./SCHEDULE.md#0-contents) # 5. The cron object @@ -397,8 +382,38 @@ available to the application including cancellation of scheduled tasks. The above code is incompatible with `uasyncio` because of the blocking calls to `time.sleep()`. -If scheduling a sequence to run at a future time please see -[Initialisation](./SCHEDULE.md#45-initialisation). +## 7.1 Initialisation + +Where a time specifier is an iterator (e.g. `mins=range(0, 60, 15)`) and there +are additional constraints (e.g. `hrs=3`) it may be necessary to delay the +start. The problem is specific to scheduling a sequence at a future time, and +there is a simple solution (which the asynchronous version implements +transparently). + +A `cron` object searches forwards from the current time. Assume the above case. +If the code start at 7:05 it picks the first later minute in the `range`, +i.e. `mins=15`, then picks the hour. This means that the first trigger occurs +at 3:15. Subsequent behaviour will be correct, but the first trigger would be +expected at 3:00. The solution is to delay start until the minutes value is in +the range`45 < mins <= 59`. The general solution is to delay until just before +the first expected callback: + +```python +def wait_for(**kwargs): + tim = mktime(localtime()[:3] + (0, 0, 0, 0, 0)) # Midnight last night + now = round(time()) + scron = cron(**kwargs) # Cron instance for search. + while tim < now: # Find first event in sequence + tim += scron(tim) + 2 + twait = tim - now - 600 + if twait > 0: + sleep(twait) + tcron = cron(**kwargs) + while True: + now = round(time()) + tw = tcron(now) + sleep(tw + 2) +``` ##### [Top](./SCHEDULE.md#0-contents) From 44de299988e9def57956265f4519ab5f2814d38f Mon Sep 17 00:00:00 2001 From: peterhinch Date: Tue, 4 Apr 2023 15:52:09 +0100 Subject: [PATCH 205/305] sched.py: Add Event-based option. --- v3/as_drivers/sched/sched.py | 5 ++++- v3/docs/SCHEDULE.md | 42 +++++++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/v3/as_drivers/sched/sched.py b/v3/as_drivers/sched/sched.py index 64ae773..0fbaadd 100644 --- a/v3/as_drivers/sched/sched.py +++ b/v3/as_drivers/sched/sched.py @@ -31,7 +31,10 @@ async def long_sleep(t): # Sleep with no bounds. Immediate return if t < 0. while times is None or times > 0: tw = fcron(round(time())) # Time to wait (s) await long_sleep(tw) - res = launch(func, args) + if isinstance(func, asyncio.Event): + func.set() + else: + res = launch(func, args) if times is not None: times -= 1 await asyncio.sleep_ms(1200) # ensure we're into next second diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index 77d87b7..9d61c55 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -96,7 +96,8 @@ be specified to run forever, once only or a fixed number of times. `schedule` is an asynchronous function. Positional args: - 1. `func` The callable (callback or coroutine) to run. + 1. `func` The callable (callback or coroutine) to run. Alternatively an + `Event` may be passed (see below). 2. Any further positional args are passed to the callable. Keyword-only args. Args 1..6 are @@ -154,6 +155,30 @@ try: finally: _ = asyncio.new_event_loop() ``` +The event-based interface can be simpler than using callables: +```python +import uasyncio as asyncio +from sched.sched import schedule +from time import localtime + +async def main(): + print("Asynchronous test running...") + evt = asyncio.Event() + asyncio.create_task(schedule(evt, hrs=10, mins=range(0, 60, 4))) + while True: + await evt.wait() # Multiple tasks may wait on an Event + evt.clear() # It must be cleared. + yr, mo, md, h, m, s, wd = localtime()[:7] + print(f"Event {h:02d}:{m:02d}:{s:02d} on {md:02d}/{mo:02d}/{yr}") + +try: + asyncio.run(main()) +finally: + _ = asyncio.new_event_loop() +``` +See [tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#32-event). +Also [this doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/EVENTS.md) +for a discussion of event-based programming. ##### [Top](./SCHEDULE.md#0-contents) @@ -262,9 +287,10 @@ This is the core of the scheduler. Users of `uasyncio` do not need to concern themseleves with it. It is documented for those wishing to modify the code and for those wanting to perform scheduling in synchronous code. -It is a closure whose creation accepts a time specification for future events. -Each subsequent call is passed the current time and returns the number of -seconds to wait for the next event to occur. +It is a closure whose creation accepts a time specification for future +triggers. When called it is passed a time value in seconds since the epoch. It +returns the number of seconds to wait for the next trigger to occur. It stores +no state. It takes the following keyword-only args. A flexible set of data types are accepted namely [time specifiers](./SCHEDULE.md#41-time-specifiers). Valid @@ -404,14 +430,14 @@ def wait_for(**kwargs): now = round(time()) scron = cron(**kwargs) # Cron instance for search. while tim < now: # Find first event in sequence - tim += scron(tim) + 2 - twait = tim - now - 600 + # Defensive. scron should never return 0, but if it did the loop would never quit + tim += max(scron(tim), 1) + twait = tim - now - 2 # Wait until 2 secs before first trigger if twait > 0: sleep(twait) - tcron = cron(**kwargs) while True: now = round(time()) - tw = tcron(now) + tw = scron(now) sleep(tw + 2) ``` From 0270af52929b8b516490347ca220f5e832aafedf Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 5 Apr 2023 11:17:19 +0100 Subject: [PATCH 206/305] pushbutton.py: Fix iss 101. --- v3/primitives/pushbutton.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index dff541f..9a072ae 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -1,6 +1,6 @@ # pushbutton.py -# Copyright (c) 2018-2022 Peter Hinch +# Copyright (c) 2018-2023 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file import uasyncio as asyncio @@ -48,7 +48,9 @@ def _check(self, state): if state: # Button pressed: launch pressed func if self._tf: launch(self._tf, self._ta) - if self._ld: # There's a long func: start long press delay + # If there's a long func: start long press delay if no double click running + # (case where a short click is rapidly followed by a long one, iss 101). + if self._ld and not (self._df and self._dd()): self._ld.trigger(Pushbutton.long_press_ms) if self._df: if self._dd(): # Second click: timer running From 883ce965c01613101cd658d0c3b36739af6c53a0 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 5 Apr 2023 17:39:47 +0100 Subject: [PATCH 207/305] events.EButton: fix iss 101. Queues: allow int list size arg. --- v3/docs/EVENTS.md | 6 ++++-- v3/docs/THREADING.md | 6 ++++-- v3/primitives/events.py | 2 +- v3/primitives/ringbuf_queue.py | 4 ++-- v3/primitives/tests/event_test.py | 12 +++++++++++- v3/threadsafe/threadsafe_queue.py | 2 +- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 0819716..b8c967c 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -493,8 +493,10 @@ Attributes of `RingbufQueue`: 4. It has an "overwrite oldest data" synchronous write mode. Constructor mandatory arg: - * `buf` Buffer for the queue, e.g. list `[0 for _ in range(20)]` or array. A - buffer of size `N` can hold a maximum of `N-1` items. + * `buf` Buffer for the queue, e.g. list, bytearray or array. If an integer is + passed, a list of this size is created. A buffer of size `N` can hold a + maximum of `N-1` items. Note that, where items on the queue are suitably + limited, bytearrays or arrays are more efficient than lists. Synchronous methods (immediate return): * `qsize` No arg. Returns the number of items in the queue. diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index 4e6c168..89003a0 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -317,8 +317,10 @@ Attributes of `ThreadSafeQueue`: raises an `IndexError`. Constructor mandatory arg: - * `buf` Buffer for the queue, e.g. list `[0 for _ in range(20)]` or array. A - buffer of size `N` can hold a maximum of `N-1` items. + * `buf` Buffer for the queue, e.g. list, bytearray or array. If an integer is + passed, a list of this size is created. A buffer of size `N` can hold a + maximum of `N-1` items. Note that, where items on the queue are suitably + limited, bytearrays or arrays are more efficient than lists. Synchronous methods. * `qsize` No arg. Returns the number of items in the queue. diff --git a/v3/primitives/events.py b/v3/primitives/events.py index 4e5dd7e..71b957b 100644 --- a/v3/primitives/events.py +++ b/v3/primitives/events.py @@ -123,7 +123,7 @@ async def _poll(self, dt): # Poll the button def _pf(self): # Button press if not self._supp: self.press.set() # User event - if not self._ltim(): # Don't retrigger long timer if already running + if not (self._ltim() or self._dtim()): # Don't retrigger long timer if already running self._ltim.trigger() if self._dtim(): # Press occurred while _dtim is running self.double.set() # User event diff --git a/v3/primitives/ringbuf_queue.py b/v3/primitives/ringbuf_queue.py index be44c2a..2b57a2b 100644 --- a/v3/primitives/ringbuf_queue.py +++ b/v3/primitives/ringbuf_queue.py @@ -1,6 +1,6 @@ # ringbuf_queue.py Provides RingbufQueue class -# Copyright (c) 2022 Peter Hinch +# Copyright (c) 2022-2023 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file # API differs from CPython @@ -13,7 +13,7 @@ class RingbufQueue: # MicroPython optimised def __init__(self, buf): - self._q = buf + self._q = [0 for _ in range(buf)] if isinstance(buf, int) else buf self._size = len(buf) self._wi = 0 self._ri = 0 diff --git a/v3/primitives/tests/event_test.py b/v3/primitives/tests/event_test.py index 23989e3..8b2c227 100644 --- a/v3/primitives/tests/event_test.py +++ b/v3/primitives/tests/event_test.py @@ -108,7 +108,7 @@ def expect(v, e): if v == e: print("Pass") else: - print(f"Fail: expected {e} got {v}") + print(f"Fail: expected 0x{e:04x} got 0x{v:04x}") fail = True async def btest(btn, verbose, supp): @@ -142,6 +142,16 @@ async def btest(btn, verbose, supp): verbose and print("Double press", hex(val)) exp = 0x48 if supp else 0x52 expect(val, exp) + val = 0 + await asyncio.sleep(1) + print("Start double press, 2nd press long, test") + await pulse() + await asyncio.sleep_ms(100) + await pulse(2000) + await asyncio.sleep(4) + verbose and print("Double press", hex(val)) + exp = 0x48 if supp else 0x52 + expect(val, exp) for task in tasks: task.cancel() diff --git a/v3/threadsafe/threadsafe_queue.py b/v3/threadsafe/threadsafe_queue.py index 0bec8d2..86917a2 100644 --- a/v3/threadsafe/threadsafe_queue.py +++ b/v3/threadsafe/threadsafe_queue.py @@ -11,7 +11,7 @@ class ThreadSafeQueue: # MicroPython optimised def __init__(self, buf): - self._q = buf + self._q = [0 for _ in range(buf)] if isinstance(buf, int) else buf self._size = len(buf) self._wi = 0 self._ri = 0 From 0339d72d445f6daa46fd2fce1a3c3764797a77b7 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Thu, 6 Apr 2023 11:44:35 +0100 Subject: [PATCH 208/305] events.Ebutton: Simplify code. Improve test script. --- v3/primitives/events.py | 12 ++++++------ v3/primitives/tests/event_test.py | 14 +++++++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/v3/primitives/events.py b/v3/primitives/events.py index 71b957b..bce928c 100644 --- a/v3/primitives/events.py +++ b/v3/primitives/events.py @@ -123,13 +123,12 @@ async def _poll(self, dt): # Poll the button def _pf(self): # Button press if not self._supp: self.press.set() # User event - if not (self._ltim() or self._dtim()): # Don't retrigger long timer if already running - self._ltim.trigger() if self._dtim(): # Press occurred while _dtim is running self.double.set() # User event self._dtim.stop() # _dtim's Event is only used if suppress - else: + else: # Single press or 1st of a double pair. self._dtim.trigger() + self._ltim.trigger() # Trigger long timer on 1st press of a double pair def _rf(self): # Button release self._ltim.stop() @@ -142,11 +141,12 @@ async def _ltf(self): # Long timeout self._ltim.clear() # Clear the event self.long.set() # User event - async def _dtf(self): # Double timeout (runs if suppress is set) + # Runs if suppress set. Delay response to single press until sure it is a single short pulse. + async def _dtf(self): while True: - await self._dtim.wait() + await self._dtim.wait() # Double click has timed out self._dtim.clear() # Clear the event - if not self._ltim(): # Button was released + if not self._ltim(): # Button was released: not a long press. self.press.set() # User events self.release.set() diff --git a/v3/primitives/tests/event_test.py b/v3/primitives/tests/event_test.py index 8b2c227..bb13020 100644 --- a/v3/primitives/tests/event_test.py +++ b/v3/primitives/tests/event_test.py @@ -116,7 +116,7 @@ async def btest(btn, verbose, supp): val = 0 events = btn.press, btn.release, btn.double, btn.long tasks = [] - for n, evt in enumerate(events): + for n, evt in enumerate(events): # Each event has a 3-bit event counter tasks.append(asyncio.create_task(monitor(evt, 1 << 3 * n, verbose))) await asyncio.sleep(1) print("Start short press test") @@ -124,6 +124,7 @@ async def btest(btn, verbose, supp): await asyncio.sleep(1) verbose and print("Test of short press", hex(val)) expect(val, 0x09) + val = 0 await asyncio.sleep(1) print("Start long press test") @@ -132,6 +133,7 @@ async def btest(btn, verbose, supp): verbose and print("Long press", hex(val)) exp = 0x208 if supp else 0x209 expect(val, exp) + val = 0 await asyncio.sleep(1) print("Start double press test") @@ -142,6 +144,7 @@ async def btest(btn, verbose, supp): verbose and print("Double press", hex(val)) exp = 0x48 if supp else 0x52 expect(val, exp) + val = 0 await asyncio.sleep(1) print("Start double press, 2nd press long, test") @@ -163,9 +166,11 @@ async def stest(sw, verbose): for n, evt in enumerate(events): tasks.append(asyncio.create_task(monitor(evt, 1 << 3 * n, verbose))) asyncio.create_task(pulse(2000)) + print("Switch closure") await asyncio.sleep(1) expect(val, 0x08) await asyncio.sleep(4) # Wait for any spurious events + print("Switch open") verbose and print("Switch close and open", hex(val)) expect(val, 0x09) for task in tasks: @@ -177,12 +182,15 @@ async def switch_test(pol, verbose): pin = Pin('Y1', Pin.IN) pout = Pin('Y2', Pin.OUT, value=pol) print("Testing EButton.") - print("suppress == False") + print("Testing with suppress == False") btn = EButton(pin) await btest(btn, verbose, False) - print("suppress == True") + print() + print("Testing with suppress == True") + btn.deinit() btn = EButton(pin, suppress=True) await btest(btn, verbose, True) + print() print("Testing ESwitch") sw = ESwitch(pin, pol) await stest(sw, verbose) From 51103b1dc7d6629d2b8221b143ef3401c2021a33 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Mon, 10 Apr 2023 09:58:51 +0100 Subject: [PATCH 209/305] as_rwgps.py: Fix string encoding bug #102 --- v3/as_drivers/as_GPS/as_rwGPS.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/as_drivers/as_GPS/as_rwGPS.py b/v3/as_drivers/as_GPS/as_rwGPS.py index 3fb4b8b..d93ec71 100644 --- a/v3/as_drivers/as_GPS/as_rwGPS.py +++ b/v3/as_drivers/as_GPS/as_rwGPS.py @@ -69,19 +69,19 @@ async def baudrate(self, value=9600): if value not in (4800,9600,14400,19200,38400,57600,115200): raise ValueError('Invalid baudrate {:d}.'.format(value)) - sentence = bytearray('$PMTK251,{:d}*00\r\n'.format(value)) + sentence = bytearray('$PMTK251,{:d}*00\r\n'.format(value).encode()) await self._send(sentence) async def update_interval(self, ms=1000): if ms < 100 or ms > 10000: raise ValueError('Invalid update interval {:d}ms.'.format(ms)) - sentence = bytearray('$PMTK220,{:d}*00\r\n'.format(ms)) + sentence = bytearray('$PMTK220,{:d}*00\r\n'.format(ms).encode()) await self._send(sentence) self._update_ms = ms # Save for timing driver async def enable(self, *, gll=0, rmc=1, vtg=1, gga=1, gsa=1, gsv=5, chan=0): fstr = '$PMTK314,{:d},{:d},{:d},{:d},{:d},{:d},0,0,0,0,0,0,0,0,0,0,0,0,{:d}*00\r\n' - sentence = bytearray(fstr.format(gll, rmc, vtg, gga, gsa, gsv, chan)) + sentence = bytearray(fstr.format(gll, rmc, vtg, gga, gsa, gsv, chan).encode()) await self._send(sentence) async def command(self, cmd): From e7bd487fc1a8549db8216f559a05feb1b7a6fe43 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Mon, 10 Apr 2023 16:47:48 +0100 Subject: [PATCH 210/305] GPS.md: Add RP2 code samples. --- v3/docs/GPS.md | 89 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 18 deletions(-) diff --git a/v3/docs/GPS.md b/v3/docs/GPS.md index 5e1bb84..ab84636 100644 --- a/v3/docs/GPS.md +++ b/v3/docs/GPS.md @@ -4,8 +4,10 @@ This repository offers a suite of asynchronous device drivers for GPS devices which communicate with the host via a UART. GPS [NMEA-0183] sentence parsing is based on this excellent library [micropyGPS]. -The code in this V3 repo has been ported to uasyncio V3. Some modules can run -under CPython: doing so will require Python V3.8 or later. +The code requires uasyncio V3. Some modules can run under CPython: doing so +will require Python V3.8 or later. + +The main modules have been tested on Pyboards and RP2 (Pico and Pico W). ###### [Tutorial](./TUTORIAL.md#contents) ###### [Main V3 README](../README.md) @@ -68,18 +70,21 @@ to access data such as position, altitude, course, speed, time and date. These notes are for the Adafruit Ultimate GPS Breakout. It may be run from 3.3V or 5V. If running the Pyboard from USB, GPS Vin may be wired to Pyboard V+. If the Pyboard is run from a voltage >5V the Pyboard 3V3 pin should be used. +Testing on Pico and Pico W used the 3.3V output to power the GPS module. + +| GPS | Pyboard | RP2 | Optional | +|:----|:-----------|:----|:--------:| +| Vin | V+ or 3V3 | 3V3 | | +| Gnd | Gnd | Gnd | | +| PPS | X3 | 2 | Y | +| Tx | X2 (U4 rx) | 1 | | +| Rx | X1 (U4 tx) | 0 | Y | + +Pyboard connections are based on UART 4 as used in the test programs; any UART +may be used. RP2 connections assume UART 0. -| GPS | Pyboard | Optional | -|:---:|:----------:|:--------:| -| Vin | V+ or 3V3 | | -| Gnd | Gnd | | -| PPS | X3 | Y | -| Tx | X2 (U4 rx) | | -| Rx | X1 (U4 tx) | Y | - -This is based on UART 4 as used in the test programs; any UART may be used. The -UART Tx-GPS Rx connection is only necessary if using the read/write driver. The -PPS connection is required only if using the timing driver `as_tGPS.py`. Any +The UART Tx-GPS Rx connection is only necessary if using the read/write driver. +The PPS connection is required only if using the timing driver `as_tGPS.py`. Any pin may be used. On the Pyboard D the 3.3V output is switched. Enable it with the following @@ -113,6 +118,7 @@ In the example below a UART is instantiated and an `AS_GPS` instance created. A callback is specified which will run each time a valid fix is acquired. The test runs for 60 seconds once data has been received. +Pyboard: ```python import uasyncio as asyncio import as_drivers.as_GPS as as_GPS @@ -124,6 +130,25 @@ uart = UART(4, 9600) sreader = asyncio.StreamReader(uart) # Create a StreamReader gps = as_GPS.AS_GPS(sreader, fix_cb=callback) # Instantiate GPS +async def test(): + print('waiting for GPS data') + await gps.data_received(position=True, altitude=True) + await asyncio.sleep(60) # Run for one minute + +asyncio.run(test()) +``` +RP2: +```python +import uasyncio as asyncio +import as_drivers.as_GPS as as_GPS +from machine import UART, Pin +def callback(gps, *_): # Runs for each valid fix + print(gps.latitude(), gps.longitude(), gps.altitude) + +uart = UART(0, 9600, tx=Pin(0), rx=Pin(1), timeout=5000, timeout_char=5000) +sreader = asyncio.StreamReader(uart) # Create a StreamReader +gps = as_GPS.AS_GPS(sreader, fix_cb=callback) # Instantiate GPS + async def test(): print('waiting for GPS data') await gps.data_received(position=True, altitude=True) @@ -358,7 +383,7 @@ The following are counts since instantiation. * `utc` (property) [hrs: int, mins: int, secs: int] UTC time e.g. [23, 3, 58]. Note the integer seconds value. The MTK3339 chip provides a float - buts its value is always an integer. To achieve accurate subsecond timing see + but its value is always an integer. To achieve accurate subsecond timing see [section 6](./GPS.md#6-using-gps-for-accurate-timing). * `local_time` (property) [hrs: int, mins: int, secs: int] Local time. * `date` (property) [day: int, month: int, year: int] e.g. [23, 3, 18] @@ -447,9 +472,13 @@ This reduces to 2s the interval at which the GPS sends messages: ```python import uasyncio as asyncio from as_drivers.as_GPS.as_rwGPS import GPS -from machine import UART - -uart = UART(4, 9600) +# Pyboard +#from machine import UART +#uart = UART(4, 9600) +# RP2 +from machine import UART, Pin +uart = UART(0, 9600, tx=Pin(0), rx=Pin(1), timeout=5000, timeout_char=5000) +# sreader = asyncio.StreamReader(uart) # Create a StreamReader swriter = asyncio.StreamWriter(uart, {}) gps = GPS(sreader, swriter) # Instantiate GPS @@ -633,6 +662,7 @@ test.usec() ## 6.2 Usage example +Pyboard: ```python import uasyncio as asyncio import pyb @@ -657,6 +687,30 @@ async def test(): t = gps_tim.get_t_split() print(fstr.format(gps_tim.get_ms(), t[0], t[1], t[2], t[3])) +asyncio.run(test()) +``` +RP2 (note set_rtc function is Pyboard specific) +```python +import uasyncio as asyncio +from machine import UART, Pin +import as_drivers.as_GPS.as_tGPS as as_tGPS + +async def test(): + fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}' + uart = UART(0, 9600, tx=Pin(0), rx=Pin(1), rxbuf=200, timeout=5000, timeout_char=5000) + sreader = asyncio.StreamReader(uart) + pps_pin = Pin(2, Pin.IN) + gps_tim = as_tGPS.GPS_Timer(sreader, pps_pin, local_offset=1, + fix_cb=lambda *_: print("fix"), + pps_cb=lambda *_: print("pps")) + print('Waiting for signal.') + await gps_tim.ready() # Wait for GPS to get a signal + while True: + await asyncio.sleep(1) + # In a precision app, get the time list without allocation: + t = gps_tim.get_t_split() + print(fstr.format(gps_tim.get_ms(), t[0], t[1], t[2], t[3])) + asyncio.run(test()) ``` @@ -964,7 +1018,6 @@ These tests allow NMEA parsing to be verified in the absence of GPS hardware: * `astests_pyb.py` Test with synthetic data on UART. GPS hardware replaced by a loopback on UART 4. Requires a Pyboard. -# 11 References [MicroPython]:https://micropython.org/ [frozen module]:https://learn.adafruit.com/micropython-basics-loading-modules/frozen-modules From 6b0a3a88c54dab73fbedacede779040014c9bc22 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 12 Apr 2023 13:33:40 +0100 Subject: [PATCH 211/305] GPS.md: add TOC. --- v3/docs/GPS.md | 76 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/v3/docs/GPS.md b/v3/docs/GPS.md index ab84636..6d43ffd 100644 --- a/v3/docs/GPS.md +++ b/v3/docs/GPS.md @@ -1,4 +1,4 @@ -# 1 as_GPS +# An asynchronous GPS receiver This repository offers a suite of asynchronous device drivers for GPS devices which communicate with the host via a UART. GPS [NMEA-0183] sentence parsing is @@ -7,11 +7,71 @@ based on this excellent library [micropyGPS]. The code requires uasyncio V3. Some modules can run under CPython: doing so will require Python V3.8 or later. -The main modules have been tested on Pyboards and RP2 (Pico and Pico W). +The main modules have been tested on Pyboards and RP2 (Pico and Pico W). Since +the interface is a standard UART it is expected that the modules will work on +other hosts. Some modules use GPS for precision timing: the accuracy of these +may be reduced on some platforms. ###### [Tutorial](./TUTORIAL.md#contents) ###### [Main V3 README](../README.md) +# 1. Contents + + 1. [Contents](./GPS.md#1-contents) + 1.1 [Driver characteristics](./GPS.md#11-driver-characteristics) + 1.2 [Comparison with micropyGPS](./GPS.md#12-comparison-with-micropygps) + 1.3 [Overview](./GPS.md#13-overview) + 2. [Installation](./GPS.md#2-installation) + 2.1 [Wiring])(./GPS.md#21-wiring) + 2.2 [Library installation](GPS.md#22-library-installation) + 2.3 [Dependency](./GPS.md#23-dependency) + 3. [Basic Usage](./GPS.md-basic-usage) + 3.1 [Demo programs](./GPS.md#31-demo-programs) + 4. [The AS_GPS Class read-only driver](./GPS.md#4-the-AS_GPS-class-read-only-driver) Base class: a general purpose driver. + 4.1 [Constructor](./GPS.md#41-constructor) +      4.1.1 [The fix callback](./GPS.md#411-the-fix-callback) Optional callback-based interface. + 4.2 [Public Methods](./GPS.md#42-public-methods) +      4.2.1 [Location](./GPS.md#412-location) +      4.2.2 [Course](./GPS.md#422-course) +      4.2.3 [Time and date](./GPS.md#423-time-and-date) + 4.3 [Public coroutines](./GPS.md#43-public-coroutines) +      4.3.1 [Data validity](./GPS.md#431-data-validity) +      4.3.2 [Satellite data](./GPS.md#432-satellite-data) + 4.4 [Public bound variables and properties](./GPS.md#44-public-bound-variables and properties) +      4.4.1 [Position and course](./GPS.md#441-position-and-course) +      4.4.2 [Statistics and status](./GPS.md#442-statistics-and-status) +      4.4.3 [Date and time](./GPS.md#443-date-and-time) +      4.4.4 [Satellite data](./GPS.md#444-satellite-data) + 4.5 [Subclass hooks](./GPS.md#45-subclass-hooks) + 4.6 [Public class variable](./GPS.md#46-public-class-variable) + 5. [The GPS class read-write driver](./GPS.md#5-the-gps-class-read-write-driver) Subclass supports changing GPS hardware modes. + 5.1 [Test script](./GPS.md#51-test-script) + 5.2 [Usage example](./GPS.md#52-usage-example) + 5.3 [The GPS class constructor](./GPS.md#53-the-gps-class-constructor) + 5.4 [Public coroutines](./GPS.md#54-public-coroutines) +      5.4.1 [Changing baudrate](./GPS.md#5-changing-baudrate) + 5.5 [Public bound variables](./GPS.md#55-public-bound-variables) + 5.6 [The parse method developer note](./GPS.md#56-the-parse-method-developer-note) + 6. [Using GPS for accurate timing](./GPS.md#6-using-gps-for-accurate-timing) + 6.1 [Test scripts](./GPS.md#61-test-scripts) + 6.2 [Usage example](./GPS.md#62-usage-example) + 6.3 [GPS_Timer and GPS_RWTimer classes](./GPS.md#63-gps_timer-and-gps_rwtimer-classes) + 6.4 [Public methods](./GPS.md#64-public-methods) + 6.5 [Public coroutines](./GPS.md#65-public-coroutines) + 6.6 [Absolute accuracy](./GPS.md#66-absolute-accuracy) + 6.7 [Demo program as_GPS_time.py](./GPS.md#67-demo-program-as_gps_time) + 7. [Supported sentences](./GPS.md#7-supported-sentences) + 8. [Developer notes](./GPS.md#8-developer-notes) For those wanting to modify the modules. + 8.1 [Subclassing](./GPS.md#81-subclassing) + 8.2 [Special test programs](./GPS.md#82-special-test-programs) + 9. [Notes on timing](./GPS.md#9-notes-on-timing) + 9.1 [Absolute accuracy](./GPS.md#91-absolute-accuracy) + 10. [Files](./GPS.md#10-files) List of files in the repo. + 10.1 [Basic files](./GPS.md#101-basic-files) + 10.2 [Files for read write operation](./GPS.md#102-files-for-read-write-operation) + 10.3 [Files for timing applications](./GPS.md#103-files-for-timing-applications) + 10.4 [Special test programs](./GPS.md#104-special-test-programs) + ## 1.1 Driver characteristics * Asynchronous: UART messaging is handled as a background task allowing the @@ -33,7 +93,7 @@ Testing was performed using a [Pyboard] with the Adafruit [Ultimate GPS Breakout] board. Most GPS devices will work with the read-only driver as they emit [NMEA-0183] sentences on startup. -## 1.2 Comparison with [micropyGPS] +## 1.2 Comparison with micropyGPS [NMEA-0183] sentence parsing is based on [micropyGPS] but with significant changes. @@ -245,6 +305,8 @@ gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG) ## 4.2 Public Methods +These are grouped below by the type of data returned. + ### 4.2.1 Location * `latitude` Optional arg `coord_format=as_GPS.DD`. Returns the most recent @@ -351,7 +413,7 @@ Note that if the GPS module does not support producing GSV sentences this coroutine will pause forever. It can also pause for arbitrary periods if satellite reception is blocked, such as in a building. -## 4.4 Public bound variables/properties +## 4.4 Public bound variables and properties These are updated whenever a sentence of the relevant type has been correctly received from the GPS unit. For crucial navigation data the `time_since_fix` @@ -359,7 +421,7 @@ method may be used to determine how current these values are. The sentence type which updates a value is shown in brackets e.g. (GGA). -### 4.4.1 Position/course +### 4.4.1 Position and course * `course` Track angle in degrees. (VTG). * `altitude` Metres above mean sea level. (GGA). @@ -616,7 +678,7 @@ measured in seconds) polls the value, returning it when it changes. 2 Internal antenna. 3 External antenna. -## 5.6 The parse method (developer note) +## 5.6 The parse method developer note The null `parse` method in the base class is overridden. It intercepts the single response to `VERSION` and `ENABLE` commands and updates the above bound @@ -821,7 +883,7 @@ accuracy and the `get_t_split` method should provide accuracy on the order of The reasoning behind this is discussed in [section 9](./GPS.md#9-notes-on-timing). -## 6.7 Test/demo program as_GPS_time.py +## 6.7 Demo program as_GPS_time Run by issuing ```python From e0bf028ada01e91ecc6597c8f300915eda2cd257 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 12 Apr 2023 13:38:04 +0100 Subject: [PATCH 212/305] GPS.md: add TOC. --- v3/docs/GPS.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/v3/docs/GPS.md b/v3/docs/GPS.md index 6d43ffd..b7ba4d1 100644 --- a/v3/docs/GPS.md +++ b/v3/docs/GPS.md @@ -121,7 +121,7 @@ the UART and parses them as they arrive. Valid sentences cause local bound variables to be updated. These can be accessed at any time with minimal latency to access data such as position, altitude, course, speed, time and date. -###### [Top](./GPS.md#1-as_gps) +##### [Contents](./GPS.md#1-contents) # 2 Installation @@ -170,7 +170,7 @@ Code samples will need adaptation for the serial port. The library requires `uasyncio` V3 on MicroPython and `asyncio` on CPython. -###### [Top](./GPS.md#1-as_gps) +##### [Contents](./GPS.md#1-contents) # 3 Basic Usage @@ -253,7 +253,7 @@ Data is logged to `/sd/log.kml` at 10s intervals. import as_drivers.as_gps.log_kml ``` -###### [Top](./GPS.md#1-as_gps) +##### [Contents](./GPS.md#1-contents) # 4. The AS_GPS Class read-only driver @@ -494,7 +494,7 @@ was received `reparse` would see basic checks on received sentences. If GPS is linked directly to the target (rather than via long cables) these checks are arguably not neccessary. -###### [Top](./GPS.md#1-as_gps) +##### [Contents](./GPS.md#1-contents) # 5. The GPS class read-write driver @@ -690,7 +690,7 @@ followed by any args specified in the constructor. Other `PMTK` messages are passed to the optional message callback as described [in section 5.3](GPS.md#53-gps-class-constructor). -###### [Top](./GPS.md#1-as_gps) +##### [Contents](./GPS.md#1-contents) # 6. Using GPS for accurate timing @@ -921,7 +921,7 @@ runs. * GPGSV * GLGSV -###### [Top](./GPS.md#1-as_gps) +##### [Contents](./GPS.md#1-contents) # 8 Developer notes @@ -966,7 +966,7 @@ or at the command line: $ micropython -m as_drivers.as_GPS.astests ``` -###### [Top](./GPS.md#1-as_gps) +##### [Contents](./GPS.md#1-contents) # 9. Notes on timing @@ -1039,7 +1039,7 @@ asynchronously to PPS: the interval will centre on 0.5s. The assumption is that inaccuracy in the `ticks_us` timer measured in μs is proportional to the duration over which it is measured. -###### [Top](./GPS.md#1-as_gps) +##### [Contents](./GPS.md#1-contents) # 10 Files @@ -1090,4 +1090,4 @@ These tests allow NMEA parsing to be verified in the absence of GPS hardware: [Ultimate GPS Breakout]:http://www.adafruit.com/product/746 [micropyGPS]:https://github.com/inmcm/micropyGPS.git -###### [Top](./GPS.md#1-as_gps) +##### [Contents](./GPS.md#1-contents) From bc90dbcbc126782ee7b4e36615ec5076f9ae3630 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 12 Apr 2023 13:57:50 +0100 Subject: [PATCH 213/305] GPS.md: add TOC. --- v3/as_drivers/as_GPS/as_GPS_time.py | 2 +- v3/as_drivers/as_GPS/as_rwGPS_time.py | 2 +- v3/docs/GPS.md | 19 +++++++++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/v3/as_drivers/as_GPS/as_GPS_time.py b/v3/as_drivers/as_GPS/as_GPS_time.py index b26a86b..943cfc0 100644 --- a/v3/as_drivers/as_GPS/as_GPS_time.py +++ b/v3/as_drivers/as_GPS/as_GPS_time.py @@ -11,7 +11,7 @@ import utime import math from .as_tGPS import GPS_Timer -from primitives.message import Message +from threadsafe.message import Message # Hardware assumptions. Change as required. PPS_PIN = pyb.Pin.board.X3 diff --git a/v3/as_drivers/as_GPS/as_rwGPS_time.py b/v3/as_drivers/as_GPS/as_rwGPS_time.py index b5fa239..cff1844 100644 --- a/v3/as_drivers/as_GPS/as_rwGPS_time.py +++ b/v3/as_drivers/as_GPS/as_rwGPS_time.py @@ -14,7 +14,7 @@ import uasyncio as asyncio from uasyncio import Event -from primitives.message import Message +from threadsafe.message import Message import pyb import utime import math diff --git a/v3/docs/GPS.md b/v3/docs/GPS.md index b7ba4d1..4fe6642 100644 --- a/v3/docs/GPS.md +++ b/v3/docs/GPS.md @@ -5,7 +5,7 @@ which communicate with the host via a UART. GPS [NMEA-0183] sentence parsing is based on this excellent library [micropyGPS]. The code requires uasyncio V3. Some modules can run under CPython: doing so -will require Python V3.8 or later. +requires Python V3.8 or later. The main modules have been tested on Pyboards and RP2 (Pico and Pico W). Since the interface is a standard UART it is expected that the modules will work on @@ -22,7 +22,7 @@ may be reduced on some platforms. 1.2 [Comparison with micropyGPS](./GPS.md#12-comparison-with-micropygps) 1.3 [Overview](./GPS.md#13-overview) 2. [Installation](./GPS.md#2-installation) - 2.1 [Wiring])(./GPS.md#21-wiring) + 2.1 [Wiring](./GPS.md#21-wiring) 2.2 [Library installation](GPS.md#22-library-installation) 2.3 [Dependency](./GPS.md#23-dependency) 3. [Basic Usage](./GPS.md-basic-usage) @@ -37,7 +37,7 @@ may be reduced on some platforms. 4.3 [Public coroutines](./GPS.md#43-public-coroutines)      4.3.1 [Data validity](./GPS.md#431-data-validity)      4.3.2 [Satellite data](./GPS.md#432-satellite-data) - 4.4 [Public bound variables and properties](./GPS.md#44-public-bound-variables and properties) + 4.4 [Public bound variables and properties](./GPS.md#44-public-bound-variables-and-properties)      4.4.1 [Position and course](./GPS.md#441-position-and-course)      4.4.2 [Statistics and status](./GPS.md#442-statistics-and-status)      4.4.3 [Date and time](./GPS.md#443-date-and-time) @@ -112,8 +112,6 @@ changes. * Hooks are provided for user-designed subclassing, for example to parse additional message types. -###### [Main V3 README](../README.md) - ## 1.3 Overview The `AS_GPS` object runs a coroutine which receives [NMEA-0183] sentences from @@ -160,7 +158,7 @@ time.sleep(1) The library is implemented as a Python package. To install copy the following directories and their contents to the target hardware: 1. `as_drivers/as_GPS` - 2. `primitives` + 2. `threadsafe` Required for timing applications only. On platforms with an underlying OS such as the Raspberry Pi ensure that the directories are on the Python path and that the Python version is 3.8 or later. @@ -368,6 +366,8 @@ These are grouped below by the type of data returned. `as_GPS.LONG` returns a string of form 'January 1st, 2014'. Note that this requires the file `as_GPS_utils.py`. +##### [Contents](./GPS.md#1-contents) + ## 4.3 Public coroutines ### 4.3.1 Data validity @@ -555,6 +555,7 @@ async def test(): asyncio.run(test()) ``` +##### [Contents](./GPS.md#1-contents) ## 5.3 GPS class Constructor @@ -823,6 +824,8 @@ Optional positional args: receives the `GPS_RWTimer` instance as the first arg, followed by any args in the tuple. +##### [Contents](./GPS.md#1-contents) + ## 6.4 Public methods The methods that return an accurate GPS time of day run as fast as possible. To @@ -903,6 +906,8 @@ runs. some limits to the absolute accuracy of the `get_t_split` method as discussed above. +##### [Contents](./GPS.md#1-contents) + # 7. Supported Sentences * GPRMC GP indicates NMEA sentence (US GPS system). @@ -1067,6 +1072,8 @@ On RAM-constrained devices `as_GPS_utils.py` may be omitted in which case the * `ast_pbrw.py` Test/demo script. ## 10.3 Files for timing applications + +Note that these require the `threadsafe` directory to be copied to the target. * `as_tGPS.py` The library. Provides `GPS_Timer` and `GPS_RWTimer` classes. * `as_GPS_time.py` Test scripts for read only driver. From a6342ef6f2abc28c2a98b2fb3d45160eaf748484 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 12 Apr 2023 16:47:13 +0100 Subject: [PATCH 214/305] GPS.md: Clarify platform options. --- v3/as_drivers/as_GPS/as_tGPS.py | 2 -- v3/docs/GPS.md | 43 ++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/v3/as_drivers/as_GPS/as_tGPS.py b/v3/as_drivers/as_GPS/as_tGPS.py index 542d3bf..92cce67 100644 --- a/v3/as_drivers/as_GPS/as_tGPS.py +++ b/v3/as_drivers/as_GPS/as_tGPS.py @@ -1,6 +1,4 @@ # as_tGPS.py Using GPS for precision timing and for calibrating Pyboard RTC -# This is STM-specific: requires pyb module. -# Hence not as RAM-critical as as_GPS # Copyright (c) 2018-2020 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file diff --git a/v3/docs/GPS.md b/v3/docs/GPS.md index 4fe6642..b359491 100644 --- a/v3/docs/GPS.md +++ b/v3/docs/GPS.md @@ -156,12 +156,16 @@ time.sleep(1) ## 2.2 Library installation The library is implemented as a Python package. To install copy the following -directories and their contents to the target hardware: - 1. `as_drivers/as_GPS` - 2. `threadsafe` Required for timing applications only. +directory and its contents to the target hardware: + * `as_drivers/as_GPS` + +The following directory is required for certain Pyboard-specific test scripts: + * `threadsafe` + +See [section 10.3](./GPS.md#103-files-for-timing-applications). On platforms with an underlying OS such as the Raspberry Pi ensure that the -directories are on the Python path and that the Python version is 3.8 or later. +directory is on the Python path and that the Python version is 3.8 or later. Code samples will need adaptation for the serial port. ## 2.3 Dependency @@ -1055,33 +1059,38 @@ applications will not need the read/write or timing files. * `as_GPS.py` The library. Supports the `AS_GPS` class for read-only access to GPS hardware. - * `as_GPS_utils.py` Additional formatted string methods for `AS_GPS`. - * `ast_pb.py` Test/demo program: assumes a MicroPython hardware device with - GPS connected to UART 4. + * `as_GPS_utils.py` Additional formatted string methods for `AS_GPS`. On + RAM-constrained devices this may be omitted in which case the `date_string` + and `compass_direction` methods will be unavailable. + +Demos. Written for Pyboard but readily portable. + * `ast_pb.py` Test/demo program: assumes a Pyboard with GPS connected to UART 4. * `log_kml.py` A simple demo which logs a route travelled to a .kml file which may be displayed on Google Earth. -On RAM-constrained devices `as_GPS_utils.py` may be omitted in which case the -`date_string` and `compass_direction` methods will be unavailable. - ## 10.2 Files for read/write operation * `as_rwGPS.py` Supports the `GPS` class. This subclass of `AS_GPS` enables writing PMTK packets. - * `as_rwGPS.py` Required if using the read/write variant. - * `ast_pbrw.py` Test/demo script. + +Demo. Written for Pyboard but readily portable. + * `ast_pbrw.py` ## 10.3 Files for timing applications -Note that these require the `threadsafe` directory to be copied to the target. - * `as_tGPS.py` The library. Provides `GPS_Timer` and `GPS_RWTimer` classes. - * `as_GPS_time.py` Test scripts for read only driver. - * `as_rwGPS_time.py` Test scripts for read/write driver. + Cross platform. + +Note that the following are Pyboard specific and require the `threadsafe` +directory to be copied to the target. + + * `as_GPS_time.py` Test scripts for read only driver (Pyboard). + * `as_rwGPS_time.py` Test scripts for read/write driver (Pyboard). ## 10.4 Special test programs -These tests allow NMEA parsing to be verified in the absence of GPS hardware: +These tests allow NMEA parsing to be verified in the absence of GPS hardware. +For those modifying or extending the sentence parsing: * `astests.py` Test with synthetic data. Run on PC under CPython 3.8+ or MicroPython. * `astests_pyb.py` Test with synthetic data on UART. GPS hardware replaced by From 9c8d45a5cfb754a6d318ec6dca1ea7cfa90a7f37 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 12 Apr 2023 16:53:50 +0100 Subject: [PATCH 215/305] GPS.md: Clarify platform options. --- v3/docs/GPS.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/v3/docs/GPS.md b/v3/docs/GPS.md index b359491..072024e 100644 --- a/v3/docs/GPS.md +++ b/v3/docs/GPS.md @@ -130,13 +130,13 @@ or 5V. If running the Pyboard from USB, GPS Vin may be wired to Pyboard V+. If the Pyboard is run from a voltage >5V the Pyboard 3V3 pin should be used. Testing on Pico and Pico W used the 3.3V output to power the GPS module. -| GPS | Pyboard | RP2 | Optional | -|:----|:-----------|:----|:--------:| -| Vin | V+ or 3V3 | 3V3 | | -| Gnd | Gnd | Gnd | | -| PPS | X3 | 2 | Y | -| Tx | X2 (U4 rx) | 1 | | -| Rx | X1 (U4 tx) | 0 | Y | +| GPS | Pyboard | RP2 | Optional | Use case | +|:----|:-----------|:----|:--------:|:--------------------------------| +| Vin | V+ or 3V3 | 3V3 | | | +| Gnd | Gnd | Gnd | | | +| PPS | X3 | 2 | Y | Precision timing applications. | +| Tx | X2 (U4 rx) | 1 | | | +| Rx | X1 (U4 tx) | 0 | Y | Changing GPS module parameters. | Pyboard connections are based on UART 4 as used in the test programs; any UART may be used. RP2 connections assume UART 0. From 179f886d1b7b48c1c4538f001ebd6dcee9e255ed Mon Sep 17 00:00:00 2001 From: peterhinch Date: Thu, 25 May 2023 10:06:55 +0100 Subject: [PATCH 216/305] THREADING.md: Fix broken link, add installation notes. --- v3/docs/THREADING.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index 89003a0..43796e4 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -32,7 +32,8 @@ is fixed. 2.1 [A pool](./THREADING.md#21-a-pool) Sharing a set of variables. 2.2 [ThreadSafeQueue](./THREADING.md#22-threadsafequeue)      2.2.1 [Blocking](./THREADING.md#221-blocking) -      2.2.3 [Object ownership](./THREADING.md#223-object-ownership) +      2.2.2 [Object ownership](./THREADING.md#222-object-ownership) +      2.2.3 [A complete example](./THREADING.md#223-a-complete-example) 3. [Synchronisation](./THREADING.md#3-synchronisation) 3.1 [Threadsafe Event](./THREADING.md#31-threadsafe-event) 3.2 [Message](./THREADING.md#32-message) A threadsafe event with data payload. @@ -192,6 +193,8 @@ Globals are implemented as a `dict`. Adding or deleting an entry is unsafe in the main program if there is a context which accesses global data and does not use the GIL. This means hard ISR's and code running on another core. Given that shared global data is widely used, the following guidelines should be followed. +([This pr](https://github.com/micropython/micropython/pull/11604) aims to fix +this issue). All globals should be declared in the main program before an ISR starts to run, and before code on another core is started. It is valid to insert placeholder @@ -301,7 +304,9 @@ read is in progress. This queue is designed to interface between one `uasyncio` task and a single thread running in a different context. This can be an interrupt service routine -(ISR), code running in a different thread or code on a different core. +(ISR), code running in a different thread or code on a different core. See +[section 2.2.3](./THREADING.md#223-a-complete-example) for a complete usage +example. Any Python object may be placed on a `ThreadSafeQueue`. If bi-directional communication is required between the two contexts, two `ThreadSafeQueue` @@ -342,7 +347,6 @@ Asynchronous methods: will block until an object is put on the queue. Normal retrieval is with `async for` but this method provides an alternative. - In use as a data consumer the `uasyncio` code will use `async for` to retrieve items from the queue. If it is a data provider it will use `put` to place objects on the queue. @@ -428,6 +432,8 @@ the MicroPython garbage collector delete them as per the first sample. This demonstrates an echo server running on core 2. The `sender` task sends consecutive integers to the server, which echoes them back on a second queue. +To install the threadsafe primitives, the `threadsafe` directory and its +contents should be copied to the MicroPython target. ```python import uasyncio as asyncio from threadsafe import ThreadSafeQueue @@ -480,7 +486,9 @@ and other pending tasks have run, the waiting task resumes. The `ThreadsafeFlag` has a limitation in that only a single task can wait on it. The `ThreadSafeEvent` overcomes this. It is subclassed from `Event` and presents the same interface. The `set` method may be called from an ISR or from -code running on another core. Any number of tasks may wait on it. +code running on another core. Any number of tasks may wait on it. To install +the threadsafe primitives, the `threadsafe` directory and its contents should +be copied to the MicroPython target. The following Pyboard-specific code demos its use in a hard ISR: ```python @@ -546,7 +554,9 @@ the `Message` as below. A `Message` can provide a means of communication from an interrupt handler and a task. The handler services the hardware and issues `.set()` which causes the waiting task to resume (in relatively slow time). -This illustrates basic usage: +To install the threadsafe primitives, the `threadsafe` directory and its +contents should be copied to the MicroPython target. This illustrates basic +usage: ```python import uasyncio as asyncio from threadsafe import Message From ee3f7ddccdae36a388b818722b97dcb9fc6d2f9b Mon Sep 17 00:00:00 2001 From: peterhinch Date: Thu, 25 May 2023 10:23:22 +0100 Subject: [PATCH 217/305] THREADING.md: Add note re PR11604. --- v3/docs/THREADING.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index 43796e4..acd01b5 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -191,10 +191,11 @@ thread safe classes offered here do not yet support Unix. Globals are implemented as a `dict`. Adding or deleting an entry is unsafe in the main program if there is a context which accesses global data and does not -use the GIL. This means hard ISR's and code running on another core. Given that -shared global data is widely used, the following guidelines should be followed. -([This pr](https://github.com/micropython/micropython/pull/11604) aims to fix -this issue). +use the GIL. This means hard ISR's and code running on another core. The +following guidelines should be followed. + +Note that [PR 11604](https://github.com/micropython/micropython/pull/11604) +aims to fix this issue. Once merged, the use of globals will be threadsafe. All globals should be declared in the main program before an ISR starts to run, and before code on another core is started. It is valid to insert placeholder @@ -215,10 +216,10 @@ def foo(): global bar bar = 42 ``` -Once again the hazard is avoided by, in global scope, populating `bar` prior -with a placeholder before allowing other contexts to run. +The hazard is avoided by instantiating `bar` in global scope (populated with a +placeholder) before allowing other contexts to run. -If globals must be created and destroyed dynamically, a lock must be used. +If globals must be created or destroyed dynamically, a lock must be used. ## 1.6 Debugging From 79271fe2885dccffb02f8a8d8265a2ae45503ca2 Mon Sep 17 00:00:00 2001 From: Ned Konz Date: Wed, 21 Jun 2023 08:08:04 -0700 Subject: [PATCH 218/305] v3/primitives/__init__.py: Fix classname and module name in lazy loader --- v3/primitives/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index 1dab8ba..e05e5db 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -35,7 +35,7 @@ def _handle_exception(loop, context): "Barrier": "barrier", "Condition": "condition", "Delay_ms": "delay_ms", - "Encode": "encoder_async", + "Encoder": "encoder", "Pushbutton": "pushbutton", "ESP32Touch": "pushbutton", "Queue": "queue", From c9022bb6e52391a9cdae9cbf41bf980098666337 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Mon, 26 Jun 2023 08:07:53 +0100 Subject: [PATCH 219/305] RingbufQueue: add peek synchronous method. --- v3/docs/EVENTS.md | 2 ++ v3/primitives/ringbuf_queue.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index b8c967c..edc8631 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -507,6 +507,8 @@ Synchronous methods (immediate return): * `put_nowait` Arg: the object to put on the queue. Raises `IndexError` if the queue is full. If the calling code ignores the exception the oldest item in the queue will be overwritten. In some applications this can be of use. + * `peek` No arg. Returns oldest entry without removing it from the queue. This + is a superset of the CPython compatible methods. Asynchronous methods: * `put` Arg: the object to put on the queue. If the queue is full, it will diff --git a/v3/primitives/ringbuf_queue.py b/v3/primitives/ringbuf_queue.py index 2b57a2b..6ca4757 100644 --- a/v3/primitives/ringbuf_queue.py +++ b/v3/primitives/ringbuf_queue.py @@ -39,6 +39,12 @@ def get_nowait(self): # Remove and return an item from the queue. self._evget.clear() return r + def peek(self): # Return oldest item from the queue without removing it. + # Return an item if one is immediately available, else raise QueueEmpty. + if self.empty(): + raise IndexError + return self._q[self._ri] + def put_nowait(self, v): self._q[self._wi] = v self._evput.set() # Schedule any tasks waiting on get From 235c6f9e87d5c4e02eb7cc7ea7dafa44a12f4305 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Mon, 26 Jun 2023 09:45:40 +0100 Subject: [PATCH 220/305] EVENTS.md: Fix ringbuf queue example indentation. --- v3/docs/EVENTS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index edc8631..d7c0114 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -532,10 +532,10 @@ not allowed to stall: where it becomes full, new items overwrite the oldest ones in the queue: ```python def add_item(q, data): -try: - q.put_nowait(data) -except IndexError: - pass + try: + q.put_nowait(data) + except IndexError: + pass ``` ###### [Contents](./EVENTS.md#0-contents) From 69bc2d433a310e14c61b75bd6b2e0b30bf86ed6b Mon Sep 17 00:00:00 2001 From: peterhinch Date: Thu, 13 Jul 2023 08:56:23 +0100 Subject: [PATCH 221/305] primitives/ringbuf_queue: Fix bug in init. --- v3/primitives/ringbuf_queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/primitives/ringbuf_queue.py b/v3/primitives/ringbuf_queue.py index 6ca4757..eb4a955 100644 --- a/v3/primitives/ringbuf_queue.py +++ b/v3/primitives/ringbuf_queue.py @@ -14,7 +14,7 @@ class RingbufQueue: # MicroPython optimised def __init__(self, buf): self._q = [0 for _ in range(buf)] if isinstance(buf, int) else buf - self._size = len(buf) + self._size = len(self._q) self._wi = 0 self._ri = 0 self._evput = asyncio.Event() # Triggered by put, tested by get From 6e8d72507a0b65cbb058f37d692c83cc682c0418 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Tue, 18 Jul 2023 10:01:55 +0100 Subject: [PATCH 222/305] ringbuf_queue: Add asynchronous get() method. --- v3/docs/EVENTS.md | 4 +++- v3/primitives/ringbuf_queue.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index d7c0114..e75cc9e 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -513,7 +513,9 @@ Synchronous methods (immediate return): Asynchronous methods: * `put` Arg: the object to put on the queue. If the queue is full, it will block until space is available. - + * `get` Return an object from the queue. If empty, block until an item is + available. + Retrieving items from the queue: The `RingbufQueue` is an asynchronous iterator. Results are retrieved using diff --git a/v3/primitives/ringbuf_queue.py b/v3/primitives/ringbuf_queue.py index eb4a955..5ddf766 100644 --- a/v3/primitives/ringbuf_queue.py +++ b/v3/primitives/ringbuf_queue.py @@ -64,6 +64,9 @@ def __aiter__(self): return self async def __anext__(self): + return await self.get() + + async def get(self): while self.empty(): # Empty. May be more than one task waiting on ._evput await self._evput.wait() r = self._q[self._ri] From 986b5c3c6ae5a29e403189fe8ecb2265e60c8449 Mon Sep 17 00:00:00 2001 From: stephanelsmith Date: Fri, 21 Jul 2023 01:57:41 +0000 Subject: [PATCH 223/305] queue: added task_done/join behavior and test (asynctest) --- v3/primitives/queue.py | 16 ++++++++ v3/primitives/tests/asyntest.py | 65 +++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/v3/primitives/queue.py b/v3/primitives/queue.py index 405c857..dfe48d7 100644 --- a/v3/primitives/queue.py +++ b/v3/primitives/queue.py @@ -26,6 +26,9 @@ def __init__(self, maxsize=0): self._evput = asyncio.Event() # Triggered by put, tested by get self._evget = asyncio.Event() # Triggered by get, tested by put + self._jncnt = 0 + self._jnevt = asyncio.Event() + def _get(self): self._evget.set() # Schedule all tasks waiting on get self._evget.clear() @@ -45,6 +48,7 @@ def get_nowait(self): # Remove and return an item from the queue. return self._get() def _put(self, val): + self._jncnt += 1 self._evput.set() # Schedule tasks waiting on put self._evput.clear() self._queue.append(val) @@ -71,3 +75,15 @@ def full(self): # Return True if there are maxsize items in the queue. # Note: if the Queue was initialized with maxsize=0 (the default) or # any negative number, then full() is never True. return self.maxsize > 0 and self.qsize() >= self.maxsize + + def task_done(self): + self._jncnt -= 1 + if self._jncnt <= 0: + self._jnevt.set() + else: + self._jnevt.clear() + + async def join(self): + await self._jnevt.wait() + + diff --git a/v3/primitives/tests/asyntest.py b/v3/primitives/tests/asyntest.py index 606e1fd..c69ce9f 100644 --- a/v3/primitives/tests/asyntest.py +++ b/v3/primitives/tests/asyntest.py @@ -34,6 +34,7 @@ def print_tests(): test(7) Test the Condition class. test(8) Test the Queue class. test(9) Test the RingbufQueue class. +test(10) Test the Queue task_done/join behavior. ''' print('\x1b[32m') print(st) @@ -636,6 +637,68 @@ def rbq_test(): ''', 6) asyncio.run(rbq_go()) + +# ************ Queue task_done/join test ************ +async def q_task_done_join_consumer(q): + while True: + r = await q.get() + print('consumer', 'got/processing {}'.format(r)) + await asyncio.sleep(.5) + q.task_done() +async def q_task_done_join_producer(q): + print('producer','loading jobs') + for x in range(10): + await q.put(x) + print('producer','await q.join') + await q.join() + print('producer','joined!', 'task done!') +async def q_task_done_join_go(): + q = Queue() + + consumer_task = asyncio.create_task(q_task_done_join_consumer(q)) + producer_task = asyncio.create_task(q_task_done_join_producer(q)) + await asyncio.sleep(0) + + print('test','await q.join') + await q.join() + print('test','all jobs done!') + + print('test','join again') + await q.join() + + await asyncio.sleep(0) + print('test','producer_task.done()?', producer_task.done()) + + consumer_task.cancel() + await asyncio.gather(consumer_task, return_exceptions=True) + + print('test','DONE') + + +def q_task_done_join_test(): + printexp('''Test Queue task_done/join behaviors +producer loading jobs +producer await q.join +test await q.join +consumer got/processing 0 +consumer got/processing 1 +consumer got/processing 2 +consumer got/processing 3 +consumer got/processing 4 +consumer got/processing 5 +consumer got/processing 6 +consumer got/processing 7 +consumer got/processing 8 +consumer got/processing 9 +producer joined! task done! +test all jobs done! +test join again +test producer_task.done()? True +test DONE +''', 5) + asyncio.run(q_task_done_join_go()) + + # ************ ************ def test(n): try: @@ -657,6 +720,8 @@ def test(n): queue_test() # Test the Queue class. elif n == 9: rbq_test() # Test the RingbufQueue class. + elif n == 10: + q_task_done_join_test() # Test the Queue task_done/join behavior. except KeyboardInterrupt: print('Interrupted') finally: From 07108cc47f010df21abb60cdecaba7ca05159ffa Mon Sep 17 00:00:00 2001 From: stephanelsmith Date: Fri, 21 Jul 2023 02:32:49 +0000 Subject: [PATCH 224/305] queue: task_done/join behavior updated for initial empty queue --- v3/primitives/queue.py | 13 ++++++++---- v3/primitives/tests/asyntest.py | 36 +++++++++++++++++---------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/v3/primitives/queue.py b/v3/primitives/queue.py index dfe48d7..93a1c7e 100644 --- a/v3/primitives/queue.py +++ b/v3/primitives/queue.py @@ -28,6 +28,7 @@ def __init__(self, maxsize=0): self._jncnt = 0 self._jnevt = asyncio.Event() + self._upd_jnevt(0) #update join event def _get(self): self._evget.set() # Schedule all tasks waiting on get @@ -48,7 +49,7 @@ def get_nowait(self): # Remove and return an item from the queue. return self._get() def _put(self, val): - self._jncnt += 1 + self._upd_jnevt(1) # update join event self._evput.set() # Schedule tasks waiting on put self._evput.clear() self._queue.append(val) @@ -76,14 +77,18 @@ def full(self): # Return True if there are maxsize items in the queue. # any negative number, then full() is never True. return self.maxsize > 0 and self.qsize() >= self.maxsize - def task_done(self): - self._jncnt -= 1 + + def _upd_jnevt(self, inc:int): # #Update join count and join event + self._jncnt += inc if self._jncnt <= 0: self._jnevt.set() else: self._jnevt.clear() - async def join(self): + def task_done(self): # Task Done decrements counter + self._upd_jnevt(-1) + + async def join(self): # Wait for join event await self._jnevt.wait() diff --git a/v3/primitives/tests/asyntest.py b/v3/primitives/tests/asyntest.py index c69ce9f..74a39c5 100644 --- a/v3/primitives/tests/asyntest.py +++ b/v3/primitives/tests/asyntest.py @@ -645,29 +645,31 @@ async def q_task_done_join_consumer(q): print('consumer', 'got/processing {}'.format(r)) await asyncio.sleep(.5) q.task_done() -async def q_task_done_join_producer(q): - print('producer','loading jobs') - for x in range(10): - await q.put(x) - print('producer','await q.join') +async def q_task_done_join_waiter(q): + print('waiter','await q.join') await q.join() - print('producer','joined!', 'task done!') + print('waiter','joined!', 'task done!') async def q_task_done_join_go(): q = Queue() + #empty queue should not block join + print('test', 'await empty q.join') + await q.join() + print('test', 'pass') + consumer_task = asyncio.create_task(q_task_done_join_consumer(q)) - producer_task = asyncio.create_task(q_task_done_join_producer(q)) - await asyncio.sleep(0) + waiter_task = asyncio.create_task(q_task_done_join_waiter(q)) + + #add jobs + for x in range(10): + await q.put(x) print('test','await q.join') await q.join() print('test','all jobs done!') - print('test','join again') - await q.join() - await asyncio.sleep(0) - print('test','producer_task.done()?', producer_task.done()) + print('test','waiter_task.done()?', waiter_task.done()) consumer_task.cancel() await asyncio.gather(consumer_task, return_exceptions=True) @@ -677,10 +679,11 @@ async def q_task_done_join_go(): def q_task_done_join_test(): printexp('''Test Queue task_done/join behaviors -producer loading jobs -producer await q.join +test await empty q.join +test pass test await q.join consumer got/processing 0 +waiter await q.join consumer got/processing 1 consumer got/processing 2 consumer got/processing 3 @@ -690,10 +693,9 @@ def q_task_done_join_test(): consumer got/processing 7 consumer got/processing 8 consumer got/processing 9 -producer joined! task done! test all jobs done! -test join again -test producer_task.done()? True +waiter joined! task done! +test waiter_task.done()? True test DONE ''', 5) asyncio.run(q_task_done_join_go()) From c438604f715bd870e3fff78ef69d560fe0cfbf14 Mon Sep 17 00:00:00 2001 From: stephanelsmith <10711596+stephanelsmith@users.noreply.github.com> Date: Thu, 20 Jul 2023 22:11:05 -0500 Subject: [PATCH 225/305] Update TUTORIAL.md Added task_done and join functions in the Queue section of the tutorial. --- v3/docs/TUTORIAL.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 926b268..b09cd64 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -973,12 +973,15 @@ Synchronous methods (immediate return): queue is full. * `get_nowait` No arg. Returns an object from the queue. Raises an exception if the queue is empty. + * `task_done` No arg. Indicate that a task associated with an enqueued item is complete. Asynchronous methods: * `put` Arg: the object to put on the queue. If the queue is full, it will block until space is available. * `get` No arg. Returns an object from the queue. If the queue is empty, it will block until an object is put on the queue. + * `join` No arg. Block until all items in the queue have been received and + processed (indicated via task_done). ```python import uasyncio as asyncio From 4554aff61ade27513f891d9c99537a053838c9c9 Mon Sep 17 00:00:00 2001 From: stephanelsmith <10711596+stephanelsmith@users.noreply.github.com> Date: Thu, 20 Jul 2023 22:13:41 -0500 Subject: [PATCH 226/305] Update TUTORIAL.md Word-smith task_done one-line description. --- v3/docs/TUTORIAL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index b09cd64..251f694 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -973,7 +973,7 @@ Synchronous methods (immediate return): queue is full. * `get_nowait` No arg. Returns an object from the queue. Raises an exception if the queue is empty. - * `task_done` No arg. Indicate that a task associated with an enqueued item is complete. + * `task_done` No arg. Indicate that a task associated with a dequeued item is complete. Asynchronous methods: * `put` Arg: the object to put on the queue. If the queue is full, it will From 33dfe3e73761f722e03a5981c9ca393b5b1fa036 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 2 Aug 2023 13:32:51 +0100 Subject: [PATCH 227/305] Tutorial: Add note on StreamReader read methods. --- v3/docs/TUTORIAL.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 251f694..d4e22d5 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2014,6 +2014,12 @@ asyncio.run(run()) ## 6.3 Using the stream mechanism +A stream is an abstraction of a device whose interface consists of a realtime +source of bytes. Examples include UARTs, I2S devices and sockets. Many streams +are continuous: an I2S microphone will source data until switched off and the +interface is closed. Streams are supported by `asyncio.StreamReader` and +`asyncio.StreamWriter` classes. + This section applies to platforms other than the Unix build. The latter handles stream I/O in a different way described [here](https://github.com/micropython/micropython/issues/7965#issuecomment-960259481). @@ -2054,6 +2060,7 @@ async def main(): asyncio.run(main()) ``` +The `.readline` method will pause until `\n` is received. The `.read` Writing to a `StreamWriter` occurs in two stages. The synchronous `.write` method concatenates data for later transmission. The asynchronous `.drain` causes transmission. To avoid allocation call `.drain` after each call to @@ -2078,6 +2085,12 @@ avoid buffer overflows and data loss. This can be ameliorated by using a larger UART read buffer or a lower baudrate. Alternatively hardware flow control will provide a solution if the data source supports it. +The `StreamReader` read methods fall into two categories depending on whether +they wait for a specific end condition. Thus `.readline` pauses until a newline +byte has been received, `.read(-1)` waits for EOF, and `readexactly` waits for +a precise number of bytes. Other methods return the number of bytes available +at the time they are called (upto a maximum). + ### 6.3.1 A UART driver example The program [auart_hd.py](../as_demos/auart_hd.py) illustrates a method of From 1b0899af9de993c63f400cf9a862291d449321aa Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 2 Aug 2023 13:36:14 +0100 Subject: [PATCH 228/305] Tutorial: Add note on StreamReader read methods. --- v3/docs/TUTORIAL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index d4e22d5..9b381c2 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2014,7 +2014,7 @@ asyncio.run(run()) ## 6.3 Using the stream mechanism -A stream is an abstraction of a device whose interface consists of a realtime +A stream is an abstraction of a device interface which consists of a realtime source of bytes. Examples include UARTs, I2S devices and sockets. Many streams are continuous: an I2S microphone will source data until switched off and the interface is closed. Streams are supported by `asyncio.StreamReader` and From 668ada88b8d89d40b19ef38ab0866b972d997741 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Thu, 3 Aug 2023 11:06:27 +0100 Subject: [PATCH 229/305] Main README: Fix broken link. --- v3/README.md | 2 +- v3/docs/TUTORIAL.md | 30 +++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/v3/README.md b/v3/README.md index f42711d..52049d9 100644 --- a/v3/README.md +++ b/v3/README.md @@ -6,7 +6,7 @@ aims to be a compatible subset of `asyncio`. The current version is 3.0.0. ## 1.1 Documents -[uasyncio official docs](http://docs.micropython.org/en/latest/library/uasyncio.html) +[uasyncio official docs](http://docs.micropython.org/en/latest/library/asyncio.html) [Tutorial](./docs/TUTORIAL.md) Intended for users with all levels of experience of asynchronous programming, including beginners. diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 9b381c2..8bc9542 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2076,20 +2076,32 @@ following methods: `ioctl`, `read`, `readline` and `write`. See [Writing streaming device drivers](./TUTORIAL.md#64-writing-streaming-device-drivers) for details on how such drivers may be written in Python. -A UART can receive data at any time. The stream I/O mechanism checks for pending -incoming characters whenever the scheduler has control. When a task is running -an interrupt service routine buffers incoming characters; these will be removed -when the task yields to the scheduler. Consequently UART applications should be -designed such that tasks minimise the time between yielding to the scheduler to -avoid buffer overflows and data loss. This can be ameliorated by using a larger -UART read buffer or a lower baudrate. Alternatively hardware flow control will -provide a solution if the data source supports it. +###### StreamReader read methods The `StreamReader` read methods fall into two categories depending on whether they wait for a specific end condition. Thus `.readline` pauses until a newline byte has been received, `.read(-1)` waits for EOF, and `readexactly` waits for a precise number of bytes. Other methods return the number of bytes available -at the time they are called (upto a maximum). +at the time they are called (upto a maximum). Consider the following fragment: +```python +async def foo(device): + sr = StreamReader(device) + data = sr.read(20) +``` +When `read` is issued, task `foo` is descheduled. Other tasks are scheduled, +resulting in a delay. During that period, depending on the stream source, bytes +may be received. The hardware or the device driver may buffer the data, at some +point flagging their availability. When the concurrent tasks permit, asyncio +polls the device. If data is available `foo` is rescheduled and pending data is +returned. It should be evident that the number of bytes returned and the +duration of the pause are variable. + +There are also implications for application and device driver design: in the +period while the task is descheduled, incoming data must be buffered to avoid +data loss. For example in the case of a UART an interrupt service routine +buffers incoming characters. To avoid data loss the size of the read buffer +should be set based on the maximum latency caused by other tasks along with the +baudrate. The buffer size can be reduced if hardware flow control is available. ### 6.3.1 A UART driver example From da50a593e17482c9b180eb714e8324e112ed4416 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sat, 19 Aug 2023 17:26:48 +0100 Subject: [PATCH 230/305] Make primitives and threadsafe mip installable. --- v3/primitives/package.json | 17 +++++++++++++++++ v3/threadsafe/package.json | 9 +++++++++ 2 files changed, 26 insertions(+) create mode 100644 v3/primitives/package.json create mode 100644 v3/threadsafe/package.json diff --git a/v3/primitives/package.json b/v3/primitives/package.json new file mode 100644 index 0000000..66dfd47 --- /dev/null +++ b/v3/primitives/package.json @@ -0,0 +1,17 @@ +{ + "urls": [ + ["primitives/__init__.py", "github:peterhinch/micropython-async/v3/primitives/__init__.py"], + ["primitives/aadc.py", "github:peterhinch/micropython-async/v3/primitives/aadc.py"], + ["primitives/barrier.py", "github:peterhinch/micropython-async/v3/primitives/barrier.py"], + ["primitives/condition.py", "github:peterhinch/micropython-async/v3/primitives/condition.py"], + ["primitives/delay_ms.py", "github:peterhinch/micropython-async/v3/primitives/delay_ms.py"], + ["primitives/encoder.py", "github:peterhinch/micropython-async/v3/primitives/encoder.py"], + ["primitives/events.py", "github:peterhinch/micropython-async/v3/primitives/events.py"], + ["primitives/pushbutton.py", "github:peterhinch/micropython-async/v3/primitives/pushbutton.py"], + ["primitives/queue.py", "github:peterhinch/micropython-async/v3/primitives/queue.py"], + ["primitives/ringbuf_queue.py", "github:peterhinch/micropython-async/v3/primitives/ringbuf_queue.py"], + ["primitives/semaphore.py", "github:peterhinch/micropython-async/v3/primitives/semaphore.py"], + ["primitives/switch.py", "github:peterhinch/micropython-async/v3/primitives/switch.py"], + ], + "version": "0.1" +} diff --git a/v3/threadsafe/package.json b/v3/threadsafe/package.json new file mode 100644 index 0000000..25d0822 --- /dev/null +++ b/v3/threadsafe/package.json @@ -0,0 +1,9 @@ +{ + "urls": [ + ["threadsafe/__init__.py", "github:peterhinch/micropython-async/v3/threadsafe/__init__.py"], + ["threadsafe/message.py", "github:peterhinch/micropython-async/v3/threadsafe/message.py"], + ["threadsafe/threadsafe_event.py", "github:peterhinch/micropython-async/v3/threadsafe/threadsafe_event.py"], + ["threadsafe/threadsafe_queue.py", "github:peterhinch/micropython-async/v3/threadsafe/threadsafe_queue.py"], + ], + "version": "0.1" +} From 30fc1e03822cecea44db39b9e338e557a7a03087 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sat, 19 Aug 2023 17:53:27 +0100 Subject: [PATCH 231/305] Document mip installation. --- v3/docs/DRIVERS.md | 11 ++++++++--- v3/docs/THREADING.md | 9 +++++++++ v3/docs/TUTORIAL.md | 14 +++++++++----- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 48b1395..88723f8 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -42,9 +42,14 @@ goes outside defined bounds. # 2. Installation and usage -The drivers require firmware version >=1.15. The drivers are in the primitives -package. To install copy the `primitives` directory and its contents to the -target hardware. +The latest release build of firmware or a newer nightly build is recommended. +To install the library, connect the target hardware to WiFi and issue: +```python +import mip +mip.install("github:peterhinch/micropython-async/v3/primitives") +``` +For non-networked targets use `mpremote` as described in +[the official docs](http://docs.micropython.org/en/latest/reference/packages.html#installing-packages-with-mpremote). Drivers are imported with: ```python diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index acd01b5..48ecff2 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -16,6 +16,15 @@ the `ThreadSafeFlag` class does not work under the Unix build. The classes presented here depend on this: none can be expected to work on Unix until this is fixed. +To install the threadsafe classes discussed here, connect the target hardware +to WiFi and issue: +```python +import mip +mip.install("github:peterhinch/micropython-async/v3/threadsafe") +``` +For non-networked targets use `mpremote` as described in +[the official docs](http://docs.micropython.org/en/latest/reference/packages.html#installing-packages-with-mpremote). + ###### [Main README](../README.md) ###### [Tutorial](./TUTORIAL.md) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 8bc9542..ccd594f 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -118,12 +118,16 @@ CPython V3.8 and above. ## 0.1 Installing uasyncio -Firmware builds after V1.13 incorporate `uasyncio`. Check the firmware version -number reported on boot and upgrade if necessary. - +The latest release build of firmware or a newer nightly build is recommended. This repository has optional unofficial primitives and extensions. To install -these the repo should be cloned to a PC. The directories `primitives` and -`threadsafe` (with contents) should be copied to the hardware plaform. +these, connect the target hardware to WiFi and issue: +```python +import mip +mip.install("github:peterhinch/micropython-async/v3/primitives") +mip.install("github:peterhinch/micropython-async/v3/threadsafe") +``` +For non-networked targets use `mpremote` as described in +[the official docs](http://docs.micropython.org/en/latest/reference/packages.html#installing-packages-with-mpremote). ###### [Main README](../README.md) From 33167c39168065b53922920b52529f42695bdbc6 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sat, 19 Aug 2023 18:47:11 +0100 Subject: [PATCH 232/305] primitives/encoder.py: Add async iterator protocol. --- v3/primitives/encoder.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index 759422b..59d7a5f 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -23,6 +23,7 @@ def __init__(self, pin_x, pin_y, v=0, div=1, vmin=None, vmax=None, self._v = v * div # Initialise hardware value self._cv = v # Current (divided) value self.delay = delay # Pause (ms) for motion to stop/limit callback frequency + self._trig = asyncio.Event() if ((vmin is not None) and v < vmin) or ((vmax is not None) and v > vmax): raise ValueError('Incompatible args: must have vmin <= v <= vmax') @@ -74,8 +75,17 @@ async def _run(self, vmin, vmax, div, mod, cb, args): self._cv = lcv # update ._cv for .value() before CB. if lcv != plcv: cb(lcv, lcv - plcv, *args) # Run user CB in uasyncio context + self._trig.set() # Enable async iterator pcv = cv plcv = lcv + def __aiter__(self): + return self + + def __anext__(self): + await self._trig.wait() + self.trig.clear() + return self._cv + def value(self): return self._cv From 04a0950efd645f56ef92708c4b7679fe0d455ae2 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sat, 19 Aug 2023 18:50:36 +0100 Subject: [PATCH 233/305] primitives/encoder.py: Add async iterator protocol. --- v3/primitives/encoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index 59d7a5f..9643fb9 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -84,7 +84,7 @@ def __aiter__(self): def __anext__(self): await self._trig.wait() - self.trig.clear() + self._trig.clear() return self._cv def value(self): From 2337452ee9431c41bf96866932f3e5559d6a370c Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sat, 19 Aug 2023 19:04:30 +0100 Subject: [PATCH 234/305] primitives/encoder.py: Document use with async for. --- v3/docs/DRIVERS.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 88723f8..ac9f9b9 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -542,6 +542,25 @@ operations are applied is: 2. Restrict the divided value by any maximum or minimum. 3. Reduce modulo N if specified. +An `Encoder` instance is an asynchronous iterator. This enables it to be used +as follows, with successive values being retrieved with `async for`: +```python +from machine import Pin +import uasyncio as asyncio +from primitives import Encoder + +async def main(): + px = Pin(16, Pin.IN, Pin.PULL_UP) # Change to match hardware + py = Pin(17, Pin.IN, Pin.PULL_UP) + enc = Encoder(px, py, div=4) # div mtches mechanical detents + async for value in enc: + print(f"Value = {value}") + +try: + asyncio.run(main()) +finally: + asyncio.new_event_loop() +``` See [this doc](https://github.com/peterhinch/micropython-samples/blob/master/encoders/ENCODERS.md) for further information on encoders and their limitations. From dfeb18c93ad59608aeae920385ae8bf123127345 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Fri, 25 Aug 2023 18:16:35 +0100 Subject: [PATCH 235/305] TUTORIAL.md: Rename uasyncio. Add note re task references. --- v3/README.md | 30 ++--- v3/docs/TUTORIAL.md | 232 +++++++++++++++++++------------------- v3/threadsafe/__init__.py | 5 - 3 files changed, 134 insertions(+), 133 deletions(-) diff --git a/v3/README.md b/v3/README.md index 52049d9..f2d42e4 100644 --- a/v3/README.md +++ b/v3/README.md @@ -1,12 +1,12 @@ -# 1. Guide to uasyncio +# 1. Guide to asyncio -MicroPython's `uasyncio` is pre-installed on all platforms except severely +MicroPython's `asyncio` is pre-installed on all platforms except severely constrained ones such as the 1MB ESP8266. It supports CPython 3.8 syntax and aims to be a compatible subset of `asyncio`. The current version is 3.0.0. ## 1.1 Documents -[uasyncio official docs](http://docs.micropython.org/en/latest/library/asyncio.html) +[asyncio official docs](http://docs.micropython.org/en/latest/library/asyncio.html) [Tutorial](./docs/TUTORIAL.md) Intended for users with all levels of experience of asynchronous programming, including beginners. @@ -16,14 +16,14 @@ describes device drivers for switches, pushbuttons, ESP32 touch buttons, ADC's and incremental encoders. [Interrupts](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/INTERRUPTS.md) -is a guide to interfacing interrupts to `uasyncio`. +is a guide to interfacing interrupts to `asyncio`. [Event-based programming](./docs/EVENTS.md) is a guide to a way of writing applications and device drivers which largely does away with callbacks. The doc -assumes some knowledge of `uasyncio`. +assumes some knowledge of `asyncio`. [Threading](./docs/THREADING.md) is a guide to the use of multi-threaded and -multi-core programming. Code is offered to enable a `uasyncio` application to +multi-core programming. Code is offered to enable a `asyncio` application to deal with blocking functions. ## 1.2 Debugging tools @@ -31,11 +31,11 @@ deal with blocking functions. [aiorepl](https://github.com/micropython/micropython-lib/tree/master/micropython/aiorepl) This official tool enables an application to launch a REPL which is active while the application is running. From this you can modify and query the -application and run `uasyncio` scripts concurrently with the running +application and run `asyncio` scripts concurrently with the running application. [monitor](https://github.com/peterhinch/micropython-monitor) enables a running -`uasyncio` application to be monitored using a Pi Pico, ideally with a scope or +`asyncio` application to be monitored using a Pi Pico, ideally with a scope or logic analyser. Normally requires only one GPIO pin on the target. ![Image](https://github.com/peterhinch/micropython-monitor/raw/master/images/monitor.jpg) @@ -58,14 +58,14 @@ Documented in the [tutorial](./docs/TUTORIAL.md). Comprises: ### 1.3.3 Threadsafe primitives [This doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/THREADING.md) -describes issues linking `uasyncio` code with code running on other cores or in +describes issues linking `asyncio` code with code running on other cores or in other threads. The `threadsafe` directory provides: * A threadsafe primitive `Message`. * `ThreadSafeQueue` * `ThreadSafeEvent` Extends `ThreadsafeFlag`. -The doc also provides code to enable `uasyncio` to handle blocking functions +The doc also provides code to enable `asyncio` to handle blocking functions using threading. ### 1.3.4 Asynchronous device drivers @@ -101,24 +101,24 @@ useful in their own right: These notes are intended for users familiar with `asyncio` under CPython. -The MicroPython language is based on CPython 3.4. The `uasyncio` library now +The MicroPython language is based on CPython 3.4. The `asyncio` library now supports a subset of the CPython 3.8 `asyncio` library. There are non-standard extensions to optimise services such as millisecond level timing. Its design focus is on high performance. Scheduling runs without RAM allocation. -The `uasyncio` library supports the following features: +The `asyncio` library supports the following features: * `async def` and `await` syntax. * Awaitable classes (using `__iter__` rather than `__await__`). * Asynchronous context managers. * Asynchronous iterators. - * `uasyncio.sleep(seconds)`. - * Timeouts (`uasyncio.wait_for`). + * `asyncio.sleep(seconds)`. + * Timeouts (`asyncio.wait_for`). * Task cancellation (`Task.cancel`). * Gather. It supports millisecond level timing with the following: - * `uasyncio.sleep_ms(time)` + * `asyncio.sleep_ms(time)` It includes the following CPython compatible synchronisation primitives: * `Event`. diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index ccd594f..7768cf2 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1,23 +1,29 @@ -# Application of uasyncio to hardware interfaces +# MicroPython asyncio: a tutorial This tutorial is intended for users having varying levels of experience with asyncio and includes a section for complete beginners. It is based on the -current version of `uasyncio`, V3.0.0. Most code samples are complete scripts +current version of `asyncio`, V3.0.0. Most code samples are complete scripts which can be cut and pasted at the REPL. -See [this overview](../README.md) for a summary of resources for `uasyncio` +See [this overview](../README.md) for a summary of resources for `asyncio` including device drivers, debugging aids, and documentation. +The name of the module was formerly `uasyncio`. To run the demo scripts on old +firmware please use +```python +import uasyncio as asyncio +``` + # Contents 0. [Introduction](./TUTORIAL.md#0-introduction) - 0.1 [Installing uasyncio](./TUTORIAL.md#01-installing-uasyncio) Also the optional extensions. + 0.1 [Installing asyncio](./TUTORIAL.md#01-installing-asyncio) Also the optional extensions. 1. [Cooperative scheduling](./TUTORIAL.md#1-cooperative-scheduling) 1.1 [Modules](./TUTORIAL.md#11-modules)      1.1.1 [Primitives](./TUTORIAL.md#111-primitives)      1.1.2 [Demo programs](./TUTORIAL.md#112-demo-programs)      1.1.3 [Device drivers](./TUTORIAL.md#113-device-drivers) - 2. [uasyncio](./TUTORIAL.md#2-uasyncio) + 2. [asyncio concept](./TUTORIAL.md#2-asyncio-concept) 2.1 [Program structure](./TUTORIAL.md#21-program-structure) 2.2 [Coroutines and Tasks](./TUTORIAL.md#22-coroutines-and-tasks)      2.2.1 [Queueing a task for scheduling](./TUTORIAL.md#221-queueing-a-task-for-scheduling) @@ -68,7 +74,7 @@ including device drivers, debugging aids, and documentation. temperature and humidity sensor. 7. [Hints and tips](./TUTORIAL.md#7-hints-and-tips) 7.1 [Program hangs](./TUTORIAL.md#71-program-hangs) - 7.2 [uasyncio retains state](./TUTORIAL.md#72-uasyncio-retains-state) + 7.2 [asyncio retains state](./TUTORIAL.md#72-asyncio-retains-state) 7.3 [Garbage Collection](./TUTORIAL.md#73-garbage-collection) 7.4 [Testing](./TUTORIAL.md#74-testing) 7.5 [A common error](./TUTORIAL.md#75-a-common-error) This can be hard to find. @@ -76,12 +82,12 @@ including device drivers, debugging aids, and documentation.      7.6.1 [WiFi issues](./TUTORIAL.md#761-wifi-issues) 7.7 [CPython compatibility and the event loop](./TUTORIAL.md#77-cpython-compatibility-and-the-event-loop) Compatibility with CPython 3.5+ 7.8 [Race conditions](./TUTORIAL.md#78-race-conditions) - 7.9 [Undocumented uasyncio features](./TUTORIAL.md#79-undocumented-uasyncio-features) + 7.9 [Undocumented asyncio features](./TUTORIAL.md#79-undocumented-asyncio-features) 8. [Notes for beginners](./TUTORIAL.md#8-notes-for-beginners) 8.1 [Problem 1: event loops](./TUTORIAL.md#81-problem-1:-event-loops) 8.2 [Problem 2: blocking methods](./TUTORIAL.md#8-problem-2:-blocking-methods) - 8.3 [The uasyncio approach](./TUTORIAL.md#83-the-uasyncio-approach) - 8.4 [Scheduling in uasyncio](./TUTORIAL.md#84-scheduling-in-uasyncio) + 8.3 [The asyncio approach](./TUTORIAL.md#83-the-asyncio-approach) + 8.4 [Scheduling in asyncio](./TUTORIAL.md#84-scheduling-in-asyncio) 8.5 [Why cooperative rather than pre-emptive?](./TUTORIAL.md#85-why-cooperative-rather-than-pre-emptive) 8.6 [Communication](./TUTORIAL.md#86-communication) 9. [Polling vs Interrupts](./TUTORIAL.md#9-polling-vs-interrupts) A common @@ -97,7 +103,7 @@ Most of this document assumes some familiarity with asynchronous programming. For those new to it an introduction may be found [in section 8](./TUTORIAL.md#8-notes-for-beginners). -The MicroPython `uasyncio` library comprises a subset of Python's `asyncio` +The MicroPython `asyncio` library comprises a subset of Python's `asyncio` library. It is designed for use on microcontrollers. As such it has a small RAM footprint and fast context switching with zero RAM allocation. This document describes its use with a focus on interfacing hardware devices. The aim is to @@ -109,14 +115,14 @@ Another major application area for asyncio is in network programming: many guides to this may be found online. Note that MicroPython is based on Python 3.4 with additions from later versions. -This version of `uasyncio` supports a subset of CPython 3.8 `asyncio`. This +This version of `asyncio` supports a subset of CPython 3.8 `asyncio`. This document identifies supported features. Except where stated program samples run under MicroPython and CPython 3.8. This tutorial aims to present a consistent programming style compatible with CPython V3.8 and above. -## 0.1 Installing uasyncio +## 0.1 Installing asyncio The latest release build of firmware or a newer nightly build is recommended. This repository has optional unofficial primitives and extensions. To install @@ -151,9 +157,7 @@ The directory `primitives` contains a Python package containing the following: switches and pushbuttons and an asynchronous ADC class. These are documented [here](./DRIVERS.md). -To install this Python package copy the `primitives` directory tree and its -contents to your hardware's filesystem. There is no need to copy the `tests` -subdirectory. +See above for installation. ### 1.1.2 Demo programs @@ -179,7 +183,7 @@ runs to soft reset the hardware. 6. [gather.py](../as_demos/gather.py) Use of `gather`. Any target. 7. [iorw.py](../as_demos/iorw.py) Demo of a read/write device driver using the stream I/O mechanism. Requires a Pyboard. - 8. [rate.py](../as_demos/rate.py) Benchmark for uasyncio. Any target. + 8. [rate.py](../as_demos/rate.py) Benchmark for asyncio. Any target. Demos are run using this pattern: ```python @@ -206,7 +210,7 @@ target. They have their own documentation as follows: ###### [Contents](./TUTORIAL.md#contents) -# 2. uasyncio +# 2. asyncio concept The asyncio concept is of cooperative multi-tasking based on coroutines (coros). A coro is similar to a function but is intended to run concurrently @@ -218,7 +222,7 @@ yielding to the scheduler, enabling other coros to be scheduled. Consider the following example: ```python -import uasyncio as asyncio +import asyncio async def bar(): count = 0 while True: @@ -237,7 +241,7 @@ In this trivial example, there is only one task: `bar`. If there were others, the scheduler would schedule them in periods when `bar` was paused: ```python -import uasyncio as asyncio +import asyncio async def bar(x): count = 0 while True: @@ -258,14 +262,14 @@ for execution. When `main` sleeps for 10s the `bar` instances are scheduled in turn, each time they yield to the scheduler with `await asyncio.sleep(1)`. In this instance `main()` terminates after 10s. This is atypical of embedded -`uasyncio` systems. Normally the application is started at power up by a one +`asyncio` systems. Normally the application is started at power up by a one line `main.py` and runs forever. ###### [Contents](./TUTORIAL.md#contents) ## 2.2 Coroutines and Tasks -The fundmental building block of `uasyncio` is a coro. This is defined with +The fundmental building block of `asyncio` is a coro. This is defined with `async def` and usually contains at least one `await` statement. This minimal example waits 1 second before printing a message: @@ -275,14 +279,14 @@ async def bar(): print('Done') ``` -V3 `uasyncio` introduced the concept of a `Task`. A `Task` instance is created +V3 `asyncio` introduced the concept of a `Task`. A `Task` instance is created from a coro by means of the `create_task` method, which causes the coro to be scheduled for execution and returns a `Task` instance. In many cases, coros and tasks are interchangeable: the official docs refer to them as `awaitable`, for the reason that either of them may be the target of an `await`. Consider this: ```python -import uasyncio as asyncio +import asyncio async def bar(t): print('Bar started: waiting {}secs'.format(t)) await asyncio.sleep(t) @@ -315,7 +319,7 @@ the `roundrobin.py` example. If a `Task` is run concurrently with `.create_task` it may be cancelled. The `.create_task` method returns the `Task` instance which may be saved for status -checking or cancellation. +checking or cancellation. See note below. In the following code sample three `Task` instances are created and scheduled for execution. The "Tasks are running" message is immediately printed. The @@ -324,7 +328,7 @@ pauses, the scheduler grants execution to the next, giving the illusion of concurrency: ```python -import uasyncio as asyncio +import asyncio async def bar(x): count = 0 while True: @@ -340,6 +344,14 @@ async def main(): asyncio.run(main()) ``` +##### Note + +The CPython [docs](https://docs.python.org/3/library/asyncio-task.html#creating-tasks) +have a warning that a reference to the task instance should be saved for the +task's duration. This was raised in +[this issue](https://github.com/micropython/micropython/issues/12299). I don't +believe MicroPython `asyncio` suffers from this bug, but writers of code which +must work in CPython and MicroPython should take note. ###### [Contents](./TUTORIAL.md#contents) @@ -378,7 +390,7 @@ async def schedule(cb, t, *args, **kwargs): ``` In this example the callback runs after three seconds: ```python -import uasyncio as asyncio +import asyncio async def schedule(cbk, t, *args, **kwargs): await asyncio.sleep(t) @@ -413,11 +425,11 @@ result = await my_task() It is possible to await completion of a set of multiple asynchronously running tasks, accessing the return value of each. This is done by -[uasyncio.gather](./TUTORIAL.md#33-gather) which launches the tasks and pauses +[asyncio.gather](./TUTORIAL.md#33-gather) which launches the tasks and pauses until the last terminates. It returns a list containing the data returned by each task: ```python -import uasyncio as asyncio +import asyncio async def bar(n): for count in range(n): @@ -453,14 +465,14 @@ will throw an exception in this case. MicroPython [does not](https://github.com/micropython/micropython/issues/6174), but it's wise to avoid doing this. -Lastly, `uasyncio` retains state. This means that, by default, you need to +Lastly, `asyncio` retains state. This means that, by default, you need to reboot between runs of an application. This can be fixed with the `new_event_loop` method discussed -[in 7.2](./TUTORIAL.md#72-uasyncio-retains-state). +[in 7.2](./TUTORIAL.md#72-asyncio-retains-state). These considerations suggest the following application structure: ```python -import uasyncio as asyncio +import asyncio from my_app import MyClass def set_global_exception(): @@ -528,7 +540,7 @@ until the consumer is ready to access the data. In simple applications, communication may be achieved with global flags or bound variables. A more elegant approach is to use synchronisation primitives. CPython provides the following classes: - * `Lock` - already incorporated in new `uasyncio`. + * `Lock` - already incorporated in new `asyncio`. * `Event` - already incorporated. * `ayncio.gather` - already incorporated. * `Semaphore` In this repository. @@ -554,11 +566,11 @@ target. A primitive is loaded by issuing (for example): from primitives import Semaphore, BoundedSemaphore from primitives import Queue ``` -When `uasyncio` acquires official versions of the CPython primitives, the +When `asyncio` acquires official versions of the CPython primitives, the invocation lines alone should be changed. E.g.: ```python -from uasyncio import Semaphore, BoundedSemaphore -from uasyncio import Queue +from asyncio import Semaphore, BoundedSemaphore +from asyncio import Queue ``` ##### Note on CPython compatibility @@ -584,8 +596,8 @@ wishing to access the shared resource. Each task attempts to acquire the lock, pausing execution until it succeeds. ```python -import uasyncio as asyncio -from uasyncio import Lock +import asyncio +from asyncio import Lock async def task(i, lock): while 1: @@ -614,8 +626,8 @@ A task waiting on a lock may be cancelled or may be run subject to a timeout. The normal way to use a `Lock` is in a context manager: ```python -import uasyncio as asyncio -from uasyncio import Lock +import asyncio +from asyncio import Lock async def task(i, lock): while 1: @@ -643,8 +655,8 @@ continue. An `Event` object is instantiated and made accessible to all tasks using it: ```python -import uasyncio as asyncio -from uasyncio import Event +import asyncio +from asyncio import Event async def waiter(event): print('Waiting for event') @@ -753,7 +765,7 @@ yet officially supported by MicroPython. ### 3.3.1 gather -This official `uasyncio` asynchronous method causes a number of tasks to run, +This official `asyncio` asynchronous method causes a number of tasks to run, pausing until all have either run to completion or been terminated by cancellation or timeout. It returns a list of the return values of each task. @@ -772,10 +784,7 @@ of return values. The following script may be used to demonstrate this behaviour: ```python -try: - import uasyncio as asyncio -except ImportError: - import asyncio +import asyncio async def barking(n): print('Start barking') @@ -841,7 +850,7 @@ but the other members continue to run. The following illustrates the basic salient points of using a `TaskGroup`: ```python -import uasyncio as asyncio +import asyncio async def foo(n): for x in range(10 + n): print(f"Task {n} running.") @@ -860,7 +869,7 @@ This more complete example illustrates an exception which is not trapped by the member task. Cleanup code on all members runs when the exception occurs, followed by exception handling code in `main()`. ```python -import uasyncio as asyncio +import asyncio fail = True # Set False to demo normal completion async def foo(n): print(f"Task {n} running...") @@ -914,7 +923,7 @@ The easiest way to use it is with an asynchronous context manager. The following illustrates tasks accessing a resource one at a time: ```python -import uasyncio as asyncio +import asyncio from primitives import Semaphore async def foo(n, sema): @@ -956,7 +965,7 @@ producer puts data items onto the queue with the consumer removing them. If the queue becomes full, the producer task will block, likewise if the queue becomes empty the consumer will block. Some queue implementations allow producer and consumer to run in different contexts: for example where one runs in an -interrupt service routine or on a different thread or core from the `uasyncio` +interrupt service routine or on a different thread or core from the `asyncio` application. Such a queue is termed "thread safe". The `Queue` class is an unofficial implementation whose API is a subset of that @@ -988,7 +997,7 @@ Asynchronous methods: processed (indicated via task_done). ```python -import uasyncio as asyncio +import asyncio from primitives import Queue async def slow_process(): @@ -1020,7 +1029,7 @@ asyncio.run(queue_go(4)) ## 3.6 ThreadSafeFlag -See also [Interfacing uasyncio to interrupts](./INTERRUPTS.md). Because of +See also [Interfacing asyncio to interrupts](./INTERRUPTS.md). Because of [this issue](https://github.com/micropython/micropython/issues/7965) the `ThreadSafeFlag` class does not work under the Unix build. @@ -1039,14 +1048,14 @@ Asynchronous method: * `wait` Wait for the flag to be set. If the flag is already set then it returns immediately. -Typical usage is having a `uasyncio` task wait on a hard ISR. Only one task +Typical usage is having a `asyncio` task wait on a hard ISR. Only one task should wait on a `ThreadSafeFlag`. The hard ISR services the interrupting device, sets the `ThreadSafeFlag`, and quits. A single task waits on the flag. This design conforms with the self-clearing behaviour of the `ThreadSafeFlag`. Each interrupting device has its own `ThreadSafeFlag` instance and its own waiting task. ```python -import uasyncio as asyncio +import asyncio from pyb import Timer tsf = asyncio.ThreadSafeFlag() @@ -1068,7 +1077,7 @@ An example [based on one posted by Damien](https://github.com/micropython/microp Link pins X1 and X2 to test. ```python from machine import Pin, Timer -import uasyncio as asyncio +import asyncio class AsyncPin: def __init__(self, pin, trigger): @@ -1145,7 +1154,7 @@ some hardware and transmits it concurrently on a number of interfaces. These run at different speeds. The `Barrier` synchronises these loops. This can run on a Pyboard. ```python -import uasyncio as asyncio +import asyncio from primitives import Barrier from machine import UART import ujson @@ -1273,7 +1282,7 @@ In this example a `Delay_ms` instance is created with the default duration of running. One second after the triggering ceases, the callback runs. ```python -import uasyncio as asyncio +import asyncio from primitives import Delay_ms async def my_app(): @@ -1297,7 +1306,7 @@ finally: This example illustrates multiple tasks waiting on a `Delay_ms`. No callback is used. ```python -import uasyncio as asyncio +import asyncio from primitives import Delay_ms async def foo(n, d): @@ -1375,7 +1384,7 @@ The calling coro blocks, but other coros continue to run. The key point is that it has completed. ```python -import uasyncio as asyncio +import asyncio class Foo(): def __iter__(self): @@ -1422,12 +1431,9 @@ method which retrieves a generator. This is portable and was tested under CPython 3.8: ```python -up = False # Running under MicroPython? -try: - import uasyncio as asyncio - up = True # Or can use sys.implementation.name -except ImportError: - import asyncio +import sys +up = sys.implementation.name == "micropython" +import asyncio async def times_two(n): # Coro to await await asyncio.sleep(1) @@ -1476,7 +1482,7 @@ its `next` method. The class must conform to the following requirements: Successive values are retrieved with `async for` as below: ```python -import uasyncio as asyncio +import asyncio class AsyncIterable: def __init__(self): self.data = (1, 2, 3, 4, 5) @@ -1544,7 +1550,7 @@ comes from the `Lock` class: If the `async with` has an `as variable` clause the variable receives the value returned by `__aenter__`. The following is a complete example: ```python -import uasyncio as asyncio +import asyncio class Foo: def __init__(self): @@ -1591,7 +1597,7 @@ block or in a context manager. # 5 Exceptions timeouts and cancellation -These topics are related: `uasyncio` enables the cancellation of tasks, and the +These topics are related: `asyncio` enables the cancellation of tasks, and the application of a timeout to a task, by throwing an exception to the task. ## 5.1 Exceptions @@ -1608,7 +1614,7 @@ exception propagates to that task, the scheduler will stop. This can be demonstrated as follows: ```python -import uasyncio as asyncio +import asyncio async def bar(): await asyncio.sleep(0) @@ -1639,7 +1645,7 @@ would stop. #### Warning Using `throw` or `close` to throw an exception to a task is unwise. It subverts -`uasyncio` by forcing the task to run, and possibly terminate, when it is still +`asyncio` by forcing the task to run, and possibly terminate, when it is still queued for execution. ### 5.1.1 Global exception handler @@ -1648,7 +1654,7 @@ During development, it is often best if untrapped exceptions stop the program rather than merely halting a single task. This can be achieved by setting a global exception handler. This debug aid is not CPython compatible: ```python -import uasyncio as asyncio +import asyncio import sys def _handle_exception(loop, context): @@ -1678,7 +1684,7 @@ There is a "gotcha" illustrated by the following code sample. If allowed to run to completion, it works as expected. ```python -import uasyncio as asyncio +import asyncio async def foo(): await asyncio.sleep(3) print('About to throw exception.') @@ -1709,7 +1715,7 @@ except KeyboardInterrupt: ``` However, issuing a keyboard interrupt causes the exception to go to the -outermost scope. This is because `uasyncio.sleep` causes execution to be +outermost scope. This is because `asyncio.sleep` causes execution to be transferred to the scheduler. Consequently, applications requiring cleanup code in response to a keyboard interrupt should trap the exception at the outermost scope. @@ -1727,7 +1733,7 @@ task waiting on (say) an `Event` or a `sleep` will be cancelled. For tasks launched with `.create_task` the exception is transparent to the user: the task simply stops as described above. It is possible to trap the exception, for example to perform cleanup code, typically in a `finally` -clause. The exception thrown to the task is `uasyncio.CancelledError` in both +clause. The exception thrown to the task is `asyncio.CancelledError` in both cancellation and timeout. There is no way for the task to distinguish between these two cases. @@ -1742,7 +1748,7 @@ the outer scope. The `Task` class has a `cancel` method. This throws a `CancelledError` to the task. This works with nested tasks. Usage is as follows: ```python -import uasyncio as asyncio +import asyncio async def printit(): print('Got here') await asyncio.sleep(1) @@ -1764,7 +1770,7 @@ asyncio.run(bar()) ``` The exception may be trapped as follows: ```python -import uasyncio as asyncio +import asyncio async def printit(): print('Got here') await asyncio.sleep(1) @@ -1805,15 +1811,15 @@ class Foo: ## 5.2.2 Tasks with timeouts -Timeouts are implemented by means of `uasyncio` methods `.wait_for()` and +Timeouts are implemented by means of `asyncio` methods `.wait_for()` and `.wait_for_ms()`. These take as arguments a task and a timeout in seconds or ms -respectively. If the timeout expires, a `uasyncio.CancelledError` is thrown to +respectively. If the timeout expires, a `asyncio.CancelledError` is thrown to the task, while the caller receives a `TimeoutError`. Trapping the exception in the task is optional. The caller must trap the `TimeoutError`, otherwise the exception will interrupt program execution. ```python -import uasyncio as asyncio +import asyncio async def forever(): try: @@ -1862,8 +1868,8 @@ The behaviour is "correct": CPython `asyncio` behaves identically. Ref # 6 Interfacing hardware -At heart, all interfaces between `uasyncio` and external asynchronous events -rely on polling. This is because of the cooperative nature of `uasyncio` +At heart, all interfaces between `asyncio` and external asynchronous events +rely on polling. This is because of the cooperative nature of `asyncio` scheduling: the task which is expected to respond to the event can only acquire control after another task has relinquished it. There are two ways to handle this. @@ -1969,7 +1975,7 @@ true in this example but the code fakes it with `await asyncio.sleep(0.1)`. Link pins X1 and X2 to run. ```python -import uasyncio as asyncio +import asyncio from pyb import UART class RecordOrientedUart(): @@ -2035,7 +2041,7 @@ demonstrates concurrent I/O on one UART. To run, link Pyboard pins X1 and X2 (UART Txd and Rxd). ```python -import uasyncio as asyncio +import asyncio from machine import UART uart = UART(4, 9600, timeout=0) # timeout=0 prevents blocking at low baudrates @@ -2161,7 +2167,7 @@ It should return immediately. The return value is the number of characters actually written (may well be 1 if the device is slow). The `ioctl` method ensures that this is only called if the device is ready to accept data. -Note that this has changed relative to `uasyncio` V2. Formerly `write` had +Note that this has changed relative to `asyncio` V2. Formerly `write` had two additional mandatory args. Existing code will fail because `Stream.drain` calls `write` with a single arg (which can be a `memoryview`). @@ -2192,7 +2198,7 @@ class MyIO(io.IOBase): The following is a complete awaitable delay class. ```python -import uasyncio as asyncio +import asyncio import utime import io MP_STREAM_POLL_RD = const(1) @@ -2233,7 +2239,7 @@ asyncio.run(timer_test(20)) ``` This currently confers no benefit over `await asyncio.sleep_ms()`, however if -`uasyncio` implements fast I/O scheduling it will be capable of more precise +`asyncio` implements fast I/O scheduling it will be capable of more precise timing. This is because I/O will be tested on every scheduler call. Currently it is polled once per complete pass, i.e. when all other pending tasks have run in round-robin fashion. @@ -2244,7 +2250,7 @@ is descheduled until `ioctl` returns a ready status. The following runs a callback when a pin changes state. ```python -import uasyncio as asyncio +import asyncio import io MP_STREAM_POLL_RD = const(1) MP_STREAM_POLL = const(3) @@ -2336,13 +2342,13 @@ hang the entire system. When developing, it is useful to have a task which periodically toggles an onboard LED. This provides confirmation that the scheduler is running. -## 7.2 uasyncio retains state +## 7.2 asyncio retains state -If a `uasyncio` application terminates, the state is retained. Embedded code seldom +If a `asyncio` application terminates, the state is retained. Embedded code seldom terminates, but in testing, it is useful to re-run a script without the need for a soft reset. This may be done as follows: ```python -import uasyncio as asyncio +import asyncio async def main(): await asyncio.sleep(5) # Dummy test script @@ -2432,7 +2438,7 @@ A coro instance is created and discarded, typically leading to a program silently failing to run correctly: ```python -import uasyncio as asyncio +import asyncio async def foo(): await asyncio.sleep(1) print('done') @@ -2447,13 +2453,13 @@ asyncio.run(main()) ## 7.6 Socket programming -There are two basic approaches to socket programming under `uasyncio`. By +There are two basic approaches to socket programming under `asyncio`. By default sockets block until a specified read or write operation completes. -`uasyncio` supports blocking sockets by using `select.poll` to prevent them +`asyncio` supports blocking sockets by using `select.poll` to prevent them from blocking the scheduler. In most cases it is simplest to use this mechanism. Example client and server code may be found in the `client_server` directory. The `userver` application uses `select.poll` explicitly to poll -the server socket. The client sockets use it implicitly in that the `uasyncio` +the server socket. The client sockets use it implicitly in that the `asyncio` stream mechanism employs it. Note that `socket.getaddrinfo` currently blocks. The time will be minimal in @@ -2478,7 +2484,7 @@ practice a timeout is likely to be required to cope with server outages. ### 7.6.1 WiFi issues -The `uasyncio` stream mechanism is not good at detecting WiFi outages. I have +The `asyncio` stream mechanism is not good at detecting WiFi outages. I have found it necessary to use nonblocking sockets to achieve resilient operation and client reconnection in the presence of outages. @@ -2516,7 +2522,7 @@ with loop = asyncio.get_event_loop() loop.create_task(my_task()) ``` -Event loop methods are supported in `uasyncio` and in CPython 3.8 but are +Event loop methods are supported in `asyncio` and in CPython 3.8 but are deprecated. To quote from the official docs: Application developers should typically use the high-level asyncio functions, @@ -2529,7 +2535,7 @@ This doc offers better alternatives to `get_event_loop` if you can confine support to CPython V3.8+. There is an event loop method `run_forever` which takes no args and causes the -event loop to run. This is supported by `uasyncio`. This has use cases, notably +event loop to run. This is supported by `asyncio`. This has use cases, notably when all of an application's tasks are instantiated in other modules. ## 7.8 Race conditions @@ -2556,7 +2562,7 @@ one is running, or to extend the timer to prolong the LED illumination. Alternatively a subsequent button press might be required to terminate the illumination. The "right" behaviour is application dependent. -## 7.9 Undocumented uasyncio features +## 7.9 Undocumented asyncio features These may be subject to change. @@ -2574,11 +2580,11 @@ holds the exception (or `CancelledError`). These notes are intended for those new to asynchronous code. They start by outlining the problems which schedulers seek to solve, and give an overview of -the `uasyncio` approach to a solution. +the `asyncio` approach to a solution. [Section 8.5](./TUTORIAL.md#85-why-cooperative-rather-than-pre-emptive) -discusses the relative merits of `uasyncio` and the `_thread` module and why -you may prefer to use cooperative (`uasyncio`) over pre-emptive (`_thread`) +discusses the relative merits of `asyncio` and the `_thread` module and why +you may prefer to use cooperative (`asyncio`) over pre-emptive (`_thread`) scheduling. ###### [Contents](./TUTORIAL.md#contents) @@ -2637,7 +2643,7 @@ class LED_flashable(): # things to happen at the same time ``` -A cooperative scheduler such as `uasyncio` enables classes such as this to be +A cooperative scheduler such as `asyncio` enables classes such as this to be created. ###### [Contents](./TUTORIAL.md#contents) @@ -2649,12 +2655,12 @@ Assume you need to read a number of bytes from a socket. If you call return) until `n` bytes have been received. During this period the application will be unresponsive to other events. -With `uasyncio` and a non-blocking socket you can write an asynchronous read +With `asyncio` and a non-blocking socket you can write an asynchronous read method. The task requiring the data will (necessarily) block until it is received but during that period other tasks will be scheduled enabling the application to remain responsive. -## 8.3 The uasyncio approach +## 8.3 The asyncio approach The following class provides for an LED which can be turned on and off, and which can also be made to flash at an arbitrary rate. A `LED_async` instance @@ -2663,7 +2669,7 @@ behaviour can be controlled by methods `on()`, `off()` and `flash(secs)`. ```python import pyb -import uasyncio as asyncio +import asyncio class LED_async(): def __init__(self, led_no): @@ -2696,14 +2702,14 @@ They change the behaviour of the LED but return immediately. The flashing occurs "in the background". This is explained in detail in the next section. The class conforms with the OOP principle of keeping the logic associated with -the device within the class. Further, the way `uasyncio` works ensures that +the device within the class. Further, the way `asyncio` works ensures that while the LED is flashing the application can respond to other events. The example below flashes the four Pyboard LED's at different rates while also responding to the USR button which terminates the program. ```python import pyb -import uasyncio as asyncio +import asyncio from led_async import LED_async # Class as listed above async def main(): @@ -2729,7 +2735,7 @@ asyncio.run(main()) # Execution passes to tasks. ###### [Contents](./TUTORIAL.md#contents) -## 8.4 Scheduling in uasyncio +## 8.4 Scheduling in asyncio Python 3.5 and MicroPython support the notion of an asynchronous function, known as a task. A task normally includes at least one `await` statement. @@ -2778,7 +2784,7 @@ async def good_code(): For the same reason it's bad practice to issue delays like `utime.sleep(1)` because that will lock out other tasks for 1s; use `await asyncio.sleep(1)`. -Note that the delays implied by `uasyncio` methods `sleep` and `sleep_ms` can +Note that the delays implied by `asyncio` methods `sleep` and `sleep_ms` can overrun the specified time. This is because while the delay is in progress other tasks will run. When the delay period completes, execution will not resume until the running task issues `await` or terminates. A well-behaved task @@ -2881,22 +2887,22 @@ latency of these platforms. Using an ISR to set a flag is probably best reserved for situations where an ISR is already needed for other reasons. -The above comments refer to an ideal scheduler. Currently `uasyncio` is not in +The above comments refer to an ideal scheduler. Currently `asyncio` is not in this category, with worst-case latency being > `N`ms. The conclusions remain valid. This, along with other issues, is discussed in -[Interfacing uasyncio to interrupts](./INTERRUPTS.md). +[Interfacing asyncio to interrupts](./INTERRUPTS.md). ###### [Contents](./TUTORIAL.md#contents) # 10. Interfacing threaded code -In the context of a `uasyncio` application, the `_thread` module has two main +In the context of a `asyncio` application, the `_thread` module has two main uses: 1. Defining code to run on another core (currently restricted to RP2). 2. Handling blocking functions. The technique assigns the blocking function to - another thread. The `uasyncio` system continues to run, with a single task + another thread. The `asyncio` system continues to run, with a single task paused pending the result of the blocking method. These techniques, and thread-safe classes to enable their use, are presented in diff --git a/v3/threadsafe/__init__.py b/v3/threadsafe/__init__.py index ae39d68..8a5db84 100644 --- a/v3/threadsafe/__init__.py +++ b/v3/threadsafe/__init__.py @@ -3,11 +3,6 @@ # Copyright (c) 2022 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -try: - import uasyncio as asyncio -except ImportError: - import asyncio - _attrs = { "ThreadSafeEvent": "threadsafe_event", "ThreadSafeQueue": "threadsafe_queue", From 86a5679e78c77f4549e3f18dc1c0513af5e52ede Mon Sep 17 00:00:00 2001 From: peterhinch Date: Fri, 25 Aug 2023 18:42:20 +0100 Subject: [PATCH 236/305] TUTORIAL.md: Rename uasyncio. Add note re task references. --- v3/docs/TUTORIAL.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 7768cf2..ac9a73c 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -17,7 +17,7 @@ import uasyncio as asyncio # Contents 0. [Introduction](./TUTORIAL.md#0-introduction) - 0.1 [Installing asyncio](./TUTORIAL.md#01-installing-asyncio) Also the optional extensions. + 0.1 [Installing asyncio primitives](./TUTORIAL.md#01-installing-asyncio-primitives) Extensions used in the demos. 1. [Cooperative scheduling](./TUTORIAL.md#1-cooperative-scheduling) 1.1 [Modules](./TUTORIAL.md#11-modules)      1.1.1 [Primitives](./TUTORIAL.md#111-primitives) @@ -122,9 +122,8 @@ under MicroPython and CPython 3.8. This tutorial aims to present a consistent programming style compatible with CPython V3.8 and above. -## 0.1 Installing asyncio +## 0.1 Installing asyncio primitives -The latest release build of firmware or a newer nightly build is recommended. This repository has optional unofficial primitives and extensions. To install these, connect the target hardware to WiFi and issue: ```python @@ -151,11 +150,17 @@ pitfalls associated with truly asynchronous threads of execution. The directory `primitives` contains a Python package containing the following: * Synchronisation primitives: "micro" versions of CPython's classes. - * Additional Python primitives including an ISR-compatible version of `Event` - and a software retriggerable delay class. + * Additional Python primitives including a software retriggerable delay class + and a MicroPython optimised `ringbuf_queue`. * Primitives for interfacing hardware. These comprise classes for debouncing - switches and pushbuttons and an asynchronous ADC class. These are documented - [here](./DRIVERS.md). + switches and pushbuttons, an `Encoder` class and an asynchronous ADC class. + These are documented [here](./DRIVERS.md). + * Primitives for event-based coding which aims to reduce the use of callbacks + and is discussed [here](./EVENTS.md). + +The directory `threadsafe` includes primitives designed to interface `asyncio` +tasks to code running on other threads. These are documented +[here](./THREADING.md). See above for installation. From ed4eea84a519c8fe6101dfd2f2547f7ad1f2f3ce Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sat, 26 Aug 2023 13:19:01 +0100 Subject: [PATCH 237/305] =?UTF-8?q?encoder.py:=20Add=20Raul=20Kompa=C3=9F?= =?UTF-8?q?=20credit.=20Add=20ref=20to=20ENCODERS.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- v3/primitives/encoder.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index 9643fb9..9ae4e49 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -3,12 +3,20 @@ # Copyright (c) 2021-2022 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -# Thanks are due to @ilium007 for identifying the issue of tracking detents, +# For an explanation of the design please see +# [ENCODERS.md](https://github.com/peterhinch/micropython-samples/blob/master/encoders/ENCODERS.md) + +# Thanks are due to the following collaborators: +# @ilium007 for identifying the issue of tracking detents, # https://github.com/peterhinch/micropython-async/issues/82. -# Also to Mike Teachman (@miketeachman) for design discussions and testing + +# Mike Teachman (@miketeachman) for design discussions and testing # against a state table design # https://github.com/miketeachman/micropython-rotary/blob/master/rotary.py +# Raul Kompaß (@rkompass) for suggesting a bugfix here +# https://forum.micropython.org/viewtopic.php?f=15&t=9929&p=66175#p66156 + import uasyncio as asyncio from machine import Pin From 5b7cc3f00d1cdaf1ffa258a2a427946fe9bb839a Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sun, 27 Aug 2023 18:09:38 +0100 Subject: [PATCH 238/305] Add reference to aioprof (asyncio profiler). --- v3/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/v3/README.md b/v3/README.md index f2d42e4..d2bd04f 100644 --- a/v3/README.md +++ b/v3/README.md @@ -34,6 +34,10 @@ while the application is running. From this you can modify and query the application and run `asyncio` scripts concurrently with the running application. +[aioprof](https://gitlab.com/alelec/aioprof/-/tree/main) A profiler for +`asyncio` applications: show the number of calls and the total time used by +each task. Author Matt Trentini. + [monitor](https://github.com/peterhinch/micropython-monitor) enables a running `asyncio` application to be monitored using a Pi Pico, ideally with a scope or logic analyser. Normally requires only one GPIO pin on the target. From 754c75db5a93cb1decc0581d710e30c10053d8c9 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Tue, 29 Aug 2023 10:52:34 +0100 Subject: [PATCH 239/305] Fix package.json files for CPython. --- v3/primitives/package.json | 2 +- v3/threadsafe/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/primitives/package.json b/v3/primitives/package.json index 66dfd47..4c823f2 100644 --- a/v3/primitives/package.json +++ b/v3/primitives/package.json @@ -11,7 +11,7 @@ ["primitives/queue.py", "github:peterhinch/micropython-async/v3/primitives/queue.py"], ["primitives/ringbuf_queue.py", "github:peterhinch/micropython-async/v3/primitives/ringbuf_queue.py"], ["primitives/semaphore.py", "github:peterhinch/micropython-async/v3/primitives/semaphore.py"], - ["primitives/switch.py", "github:peterhinch/micropython-async/v3/primitives/switch.py"], + ["primitives/switch.py", "github:peterhinch/micropython-async/v3/primitives/switch.py"] ], "version": "0.1" } diff --git a/v3/threadsafe/package.json b/v3/threadsafe/package.json index 25d0822..7071868 100644 --- a/v3/threadsafe/package.json +++ b/v3/threadsafe/package.json @@ -3,7 +3,7 @@ ["threadsafe/__init__.py", "github:peterhinch/micropython-async/v3/threadsafe/__init__.py"], ["threadsafe/message.py", "github:peterhinch/micropython-async/v3/threadsafe/message.py"], ["threadsafe/threadsafe_event.py", "github:peterhinch/micropython-async/v3/threadsafe/threadsafe_event.py"], - ["threadsafe/threadsafe_queue.py", "github:peterhinch/micropython-async/v3/threadsafe/threadsafe_queue.py"], + ["threadsafe/threadsafe_queue.py", "github:peterhinch/micropython-async/v3/threadsafe/threadsafe_queue.py"] ], "version": "0.1" } From 11a3c705f3e76f97e0086b87880cd2e159bf26e8 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Wed, 30 Aug 2023 07:40:41 +0100 Subject: [PATCH 240/305] Tutorial: amend note on task references. --- v3/docs/TUTORIAL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index ac9a73c..e2a8406 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -354,8 +354,8 @@ asyncio.run(main()) The CPython [docs](https://docs.python.org/3/library/asyncio-task.html#creating-tasks) have a warning that a reference to the task instance should be saved for the task's duration. This was raised in -[this issue](https://github.com/micropython/micropython/issues/12299). I don't -believe MicroPython `asyncio` suffers from this bug, but writers of code which +[this issue](https://github.com/micropython/micropython/issues/12299). +MicroPython `asyncio` does not suffer from this bug, but writers of code which must work in CPython and MicroPython should take note. ###### [Contents](./TUTORIAL.md#contents) From cbb97f4b28f9923eb1b23f90ccf392bb858aae69 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 3 Sep 2023 11:32:31 +0100 Subject: [PATCH 241/305] Tutorial: Make samples CPython-compatible (task references). --- v3/docs/TUTORIAL.md | 61 ++++++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index e2a8406..b77aca3 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -255,8 +255,9 @@ async def bar(x): await asyncio.sleep(1) # Pause 1s async def main(): + tasks = [None] * 3 # For CPython compaibility must store a reference see Note for x in range(3): - asyncio.create_task(bar(x)) + tasks[x] = asyncio.create_task(bar(x)) await asyncio.sleep(10) asyncio.run(main()) @@ -342,21 +343,40 @@ async def bar(x): await asyncio.sleep(1) # Pause 1s async def main(): + tasks = [None] * 3 # For CPython compaibility must store a reference see Note for x in range(3): - asyncio.create_task(bar(x)) + tasks[x] = asyncio.create_task(bar(x)) print('Tasks are running') await asyncio.sleep(10) asyncio.run(main()) ``` -##### Note +### Note on CPython compatibility The CPython [docs](https://docs.python.org/3/library/asyncio-task.html#creating-tasks) have a warning that a reference to the task instance should be saved for the task's duration. This was raised in [this issue](https://github.com/micropython/micropython/issues/12299). MicroPython `asyncio` does not suffer from this bug, but writers of code which -must work in CPython and MicroPython should take note. +must work in CPython and MicroPython should take note. Code samples in this doc +are CPython-compatible, but following version is valid in MicroPython: +```python +import asyncio +async def bar(x): + count = 0 + while True: + count += 1 + print('Instance: {} count: {}'.format(x, count)) + await asyncio.sleep(1) # Pause 1s + +async def main(): + for x in range(3): + asyncio.create_task(bar(x)) # No reference stored + print('Tasks are running') + await asyncio.sleep(10) + +asyncio.run(main()) +``` ###### [Contents](./TUTORIAL.md#contents) @@ -491,7 +511,7 @@ def set_global_exception(): async def main(): set_global_exception() # Debug aid my_class = MyClass() # Constructor might create tasks - asyncio.create_task(my_class.foo()) # Or you might do this + task = asyncio.create_task(my_class.foo()) # Or you might do this await my_class.run_forever() # Non-terminating method try: asyncio.run(main()) @@ -613,8 +633,9 @@ async def task(i, lock): async def main(): lock = Lock() # The Lock instance + tasks = [None] * 3 # For CPython compaibility must store a reference see Note for n in range(1, 4): - asyncio.create_task(task(n, lock)) + tasks[n - 1] = asyncio.create_task(task(n, lock)) await asyncio.sleep(10) asyncio.run(main()) # Run for 10s @@ -642,8 +663,9 @@ async def task(i, lock): async def main(): lock = Lock() # The Lock instance + tasks = [None] * 3 # For CPython compaibility must store a reference see Note for n in range(1, 4): - asyncio.create_task(task(n, lock)) + tasks[n - 1] = asyncio.create_task(task(n, lock)) await asyncio.sleep(10) asyncio.run(main()) # Run for 10s @@ -671,7 +693,7 @@ async def waiter(event): async def main(): event = Event() - asyncio.create_task(waiter(event)) + task = asyncio.create_task(waiter(event)) await asyncio.sleep(2) print('Setting event') event.set() @@ -821,7 +843,7 @@ async def main(): tasks = [asyncio.create_task(bar(70))] tasks.append(barking(21)) tasks.append(asyncio.wait_for(foo(10), 7)) - asyncio.create_task(do_cancel(tasks[0])) + can = asyncio.create_task(do_cancel(tasks[0])) res = None try: res = await asyncio.gather(*tasks, return_exceptions=True) @@ -939,8 +961,9 @@ async def foo(n, sema): async def main(): sema = Semaphore() + tasks = [None] * 3 # For CPython compaibility must store a reference see Note for num in range(3): - asyncio.create_task(foo(num, sema)) + tasks[num] = asyncio.create_task(foo(num, sema)) await asyncio.sleep(2) asyncio.run(main()) @@ -1022,8 +1045,8 @@ async def consume(queue): async def queue_go(delay): queue = Queue() - asyncio.create_task(consume(queue)) - asyncio.create_task(produce(queue)) + t1 = asyncio.create_task(consume(queue)) + t2 = asyncio.create_task(produce(queue)) await asyncio.sleep(delay) print("Done") @@ -1188,8 +1211,9 @@ async def main(): sw1 = asyncio.StreamWriter(UART(1, 9600), {}) sw2 = asyncio.StreamWriter(UART(2, 1200), {}) barrier = Barrier(3) + tasks = [None] * 2 # For CPython compaibility must store a reference see Note for n, sw in enumerate((sw1, sw2)): - asyncio.create_task(sender(barrier, sw, n + 1)) + tasks[n] = asyncio.create_task(sender(barrier, sw, n + 1)) await provider(barrier) asyncio.run(main()) @@ -1321,8 +1345,9 @@ async def foo(n, d): async def my_app(): d = Delay_ms() + tasks = [None] * 4 # For CPython compaibility must store a reference see Note for n in range(4): - asyncio.create_task(foo(n, d)) + tasks[n] = asyncio.create_task(foo(n, d)) d.trigger(3000) print('Waiting on d') await d.wait() @@ -1632,7 +1657,7 @@ async def foo(): print('Does not print') # Because bar() raised an exception async def main(): - asyncio.create_task(foo()) + task = asyncio.create_task(foo()) for _ in range(5): print('Working') # Carries on after the exception await asyncio.sleep(0.5) @@ -1675,7 +1700,7 @@ async def bar(): async def main(): loop = asyncio.get_event_loop() loop.set_exception_handler(_handle_exception) - asyncio.create_task(bar()) + task = asyncio.create_task(bar()) for _ in range(5): print('Working') await asyncio.sleep(0.5) @@ -2270,7 +2295,7 @@ class PinCall(io.IOBase): self.cbf_args = cbf_args self.pinval = pin.value() self.sreader = asyncio.StreamReader(self) - asyncio.create_task(self.run()) + self.task = asyncio.create_task(self.run()) async def run(self): while True: @@ -2680,7 +2705,7 @@ class LED_async(): def __init__(self, led_no): self.led = pyb.LED(led_no) self.rate = 0 - asyncio.create_task(self.run()) + self.task = asyncio.create_task(self.run()) async def run(self): while True: From 46438be23632cc3d74d9ecd72768c9108dde1298 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 5 Sep 2023 09:43:59 +0100 Subject: [PATCH 242/305] Tutorial: Improve Task Groups section. --- v3/docs/TUTORIAL.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index b77aca3..1571a7f 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -862,9 +862,10 @@ The `TaskGroup` class is unofficially provided by suited to applications where one or more of a group of tasks is subject to runtime exceptions. A `TaskGroup` is instantiated in an asynchronous context manager. The `TaskGroup` instantiates member tasks. When all have run to -completion, the context manager terminates. Return values from member tasks -cannot be retrieved. Results should be passed in other ways such as via bound -variables, queues etc. +completion, the context manager terminates. Where `gather` is static, a task +group can be dynamic: a task in a group may spawn further group members. Return +values from member tasks cannot be retrieved. Results should be passed in other +ways such as via bound variables, queues etc. An exception in a member task not trapped by that task is propagated to the task that created the `TaskGroup`. All tasks in the `TaskGroup` then terminate @@ -922,6 +923,9 @@ async def main(): asyncio.run(main()) ``` +[This doc](https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/) +provides background on the theory behind task groups and how they can improve +program structure and reliablity. ###### [Contents](./TUTORIAL.md#contents) From 4279c8bbd16126798d00966b21177224c480578e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 30 Sep 2023 10:22:38 +0100 Subject: [PATCH 243/305] Fix bug in threadsafe_queue constructor. --- v3/threadsafe/threadsafe_queue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/threadsafe/threadsafe_queue.py b/v3/threadsafe/threadsafe_queue.py index 86917a2..5d07682 100644 --- a/v3/threadsafe/threadsafe_queue.py +++ b/v3/threadsafe/threadsafe_queue.py @@ -12,7 +12,7 @@ class ThreadSafeQueue: # MicroPython optimised def __init__(self, buf): self._q = [0 for _ in range(buf)] if isinstance(buf, int) else buf - self._size = len(buf) + self._size = len(self._q) self._wi = 0 self._ri = 0 self._evput = asyncio.ThreadSafeFlag() # Triggered by put, tested by get From 5afbcc5ffb8f5e4ef36bdac9fec61a44b5ec2c39 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 1 Oct 2023 15:12:13 +0100 Subject: [PATCH 244/305] Add threadsafe/context.py --- v3/docs/THREADING.md | 54 +++++++++++++++++++++++++++++++++++--- v3/threadsafe/__init__.py | 1 + v3/threadsafe/context.py | 31 ++++++++++++++++++++++ v3/threadsafe/package.json | 1 + 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 v3/threadsafe/context.py diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index 48ecff2..88e6de2 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -22,8 +22,10 @@ to WiFi and issue: import mip mip.install("github:peterhinch/micropython-async/v3/threadsafe") ``` -For non-networked targets use `mpremote` as described in -[the official docs](http://docs.micropython.org/en/latest/reference/packages.html#installing-packages-with-mpremote). +On any target `mpremote` may be used: +```bash +$ mpremote mip install github:peterhinch/micropython-async/v3/threadsafe +``` ###### [Main README](../README.md) ###### [Tutorial](./TUTORIAL.md) @@ -47,6 +49,8 @@ For non-networked targets use `mpremote` as described in 3.1 [Threadsafe Event](./THREADING.md#31-threadsafe-event) 3.2 [Message](./THREADING.md#32-message) A threadsafe event with data payload. 4. [Taming blocking functions](./THREADING.md#4-taming-blocking-functions) Enabling uasyncio to handle blocking code. + 4.1 [Basic approach](./THREADING.md#41-basic-approach) + 4.2 [More general solution](./THREADING,md#42-more-general-solution) 5. [Sharing a stream device](./THREADING.md#5-sharing-a-stream-device) 6. [Glossary](./THREADING.md#6-glossary) Terminology of realtime coding. @@ -188,7 +192,7 @@ thread safe classes offered here do not yet support Unix. is only required if mutual consistency of the three values is essential. 3. In the absence of a GIL some operations on built-in objects are not thread safe. For example adding or deleting items in a `dict`. This extends to global - variables which are implemented as a `dict`. See [Globals](./THREADING.md#15-globals). + variables because these are implemented as a `dict`. See [Globals](./THREADING.md#15-globals). 4. The observations in 1.3 re user defined data structures and `uasyncio` interfacing apply. 5. Code running on a core other than that running `uasyncio` may block for @@ -643,7 +647,13 @@ again before it is accessed, the first data item will be lost. Blocking functions or methods have the potential of stalling the `uasyncio` scheduler. Short of rewriting them to work properly the only way to tame them -is to run them in another thread. The following is a way to achieve this. +is to run them in another thread. Any function to be run in this way must +conform to the guiedelines above, notably with regard to allocation and side +effects. + +## 4.1 Basic approach + +The following is a way to "unblock" a single function or method. ```python async def unblock(func, *args, **kwargs): def wrap(func, message, args, kwargs): @@ -699,6 +709,42 @@ asyncio.run(main()) ``` ###### [Contents](./THREADING.md#contents) +## 4.1 More general solution + +This provides a queueing mechanism. A task can assign a blocking function to a +core even if the core is already busy. Further it allows for multiple cores or +threads; these are defined as `Context` instances. Typical use: +```python +from threadsafe import Context + +core1 = Context() # Has an instance of _thread, so a core on RP2 + +def rats(t, n): # Arbitrary blocking function or method + time.sleep(t) + return n * n + +async def some_task(): + await core1.assign(rats, t=3, n=99) # rats() runs on other core +``` +#### Context class + +Constructor arg: + * `qsize=10` Size of function queue. + +Asynchronous method: + * `assign(func, *args, **kwargs)` Accepts a synchronous function with optional + args. These are placed on a queue for execution in the `Context` instance. The + method pauses until execution is complete, returning the fuction's return + value. + +The `Context` class constructor spawns a thread which waits on the `Context` +queue. The`assign` method accepts a fuction and creates a `Job` instance. This +includes a `ThreadSafeFlag` along with the function and its args. The `Assign` +method places the `Job` on the queue and waits on the `ThreadSafeFlag`. + +The thread removes a `Job` from the queue and executes it. When complete it +assigns the return value to the `Job` and sets the `ThreadSafeFlag`. + # 5. Sharing a stream device Typical stream devices are a UART or a socket. These are typically employed to diff --git a/v3/threadsafe/__init__.py b/v3/threadsafe/__init__.py index 8a5db84..a60c707 100644 --- a/v3/threadsafe/__init__.py +++ b/v3/threadsafe/__init__.py @@ -7,6 +7,7 @@ "ThreadSafeEvent": "threadsafe_event", "ThreadSafeQueue": "threadsafe_queue", "Message": "message", + "Context": "context", } # Copied from uasyncio.__init__.py diff --git a/v3/threadsafe/context.py b/v3/threadsafe/context.py new file mode 100644 index 0000000..f0d1655 --- /dev/null +++ b/v3/threadsafe/context.py @@ -0,0 +1,31 @@ +# context.py: Run functions or methods on another core or in another thread + +import uasyncio as asyncio +import _thread +from threadsafe import ThreadSafeQueue + +# Object describing a job to be run on another core +class Job: + def __init__(self, func, args, kwargs): + self.kwargs = kwargs + self.args = args + self.func = func + self.rval = None # Return value + self.done = asyncio.ThreadSafeFlag() # "done" indicator + +def worker(q): # Runs forever on a core executing jobs as they arrive + while True: + job = q.get_sync(True) # Block until a Job arrives + job.rval = job.func(*job.args, **job.kwargs) + job.done.set() + +class Context: + def __init__(self, qsize=10): + self.q = ThreadSafeQueue(qsize) + _thread.start_new_thread(worker, (self.q,)) + + async def assign(self, func, *args, **kwargs): + job = Job(func, args, kwargs) + await self.q.put(job) # Will pause if q is full. + await job.done.wait() # Pause until function has run + return job.rval diff --git a/v3/threadsafe/package.json b/v3/threadsafe/package.json index 7071868..0237a27 100644 --- a/v3/threadsafe/package.json +++ b/v3/threadsafe/package.json @@ -4,6 +4,7 @@ ["threadsafe/message.py", "github:peterhinch/micropython-async/v3/threadsafe/message.py"], ["threadsafe/threadsafe_event.py", "github:peterhinch/micropython-async/v3/threadsafe/threadsafe_event.py"], ["threadsafe/threadsafe_queue.py", "github:peterhinch/micropython-async/v3/threadsafe/threadsafe_queue.py"] + ["threadsafe/context.py", "github:peterhinch/micropython-async/v3/threadsafe/context.py"] ], "version": "0.1" } From ffee28203a70ae6b15c59a028a6f71a56d2aec77 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 1 Oct 2023 15:14:47 +0100 Subject: [PATCH 245/305] Add threadsafe/context.py --- v3/threadsafe/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/threadsafe/package.json b/v3/threadsafe/package.json index 0237a27..39d51d0 100644 --- a/v3/threadsafe/package.json +++ b/v3/threadsafe/package.json @@ -3,7 +3,7 @@ ["threadsafe/__init__.py", "github:peterhinch/micropython-async/v3/threadsafe/__init__.py"], ["threadsafe/message.py", "github:peterhinch/micropython-async/v3/threadsafe/message.py"], ["threadsafe/threadsafe_event.py", "github:peterhinch/micropython-async/v3/threadsafe/threadsafe_event.py"], - ["threadsafe/threadsafe_queue.py", "github:peterhinch/micropython-async/v3/threadsafe/threadsafe_queue.py"] + ["threadsafe/threadsafe_queue.py", "github:peterhinch/micropython-async/v3/threadsafe/threadsafe_queue.py"], ["threadsafe/context.py", "github:peterhinch/micropython-async/v3/threadsafe/context.py"] ], "version": "0.1" From fb7f2068c19c99dad0353725fc0b922b9d929f79 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 3 Oct 2023 11:04:41 +0100 Subject: [PATCH 246/305] Add Keyboard primitive. --- v3/docs/EVENTS.md | 96 +++++++++++++++++++++++++++++++-------- v3/docs/THREADING.md | 24 +++++++--- v3/primitives/__init__.py | 1 + v3/primitives/events.py | 42 ++++++++++++++++- 4 files changed, 135 insertions(+), 28 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index e75cc9e..0b5c9f5 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -1,15 +1,15 @@ # Synopsis -Using `Event` instances rather than callbacks in `uasyncio` device drivers can +Using `Event` instances rather than callbacks in `asyncio` device drivers can simplify their design and standardise their APIs. It can also simplify application logic. -This document assumes familiarity with `uasyncio`. See [official docs](http://docs.micropython.org/en/latest/library/uasyncio.html) and +This document assumes familiarity with `asyncio`. See [official docs](http://docs.micropython.org/en/latest/library/asyncio.html) and [unofficial tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md). # 0. Contents - 1. [An alternative to callbacks in uasyncio code](./EVENTS.md#1-an-alternative-to-callbacks-in-uasyncio-code) + 1. [An alternative to callbacks in asyncio code](./EVENTS.md#1-an-alternative-to-callbacks-in-asyncio-code) 2. [Rationale](./EVENTS.md#2-rationale) 3. [Device driver design](./EVENTS.md#3-device-driver-design) 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 6.2 [EButton](./EVENTS.md#62-ebutton) Debounced pushbutton with double and long press events      6.2.1 [The suppress constructor argument](./EVENTS.md#621-the-suppress-constructor-argument)      6.2.2 [The sense constructor argument](./EVENTS.md#622-the-sense-constructor-argument) + 6.3 [Keyboard](./EVENTS.md#63-keyboard) A crosspoint array of pushbuttons. 7. [Ringbuf queue](./EVENTS.md#7-ringbuf-queue) A MicroPython optimised queue primitive. [Appendix 1 Polling](./EVENTS.md#100-appendix-1-polling) -# 1. An alternative to callbacks in uasyncio code +# 1. An alternative to callbacks in asyncio code Callbacks have two merits. They are familiar, and they enable an interface which allows an asynchronous application to be accessed by synchronous code. @@ -49,7 +50,7 @@ async def handle_messages(input_stream): Callbacks are not a natural fit in this model. Viewing the declaration of a synchronous function, it is not evident how the function gets called or in what context the code runs. Is it an ISR? Is it called from another thread or core? -Or is it a callback running in a `uasyncio` context? You cannot tell without +Or is it a callback running in a `asyncio` context? You cannot tell without trawling the code. By contrast, a routine such as the above example is a self contained process whose context and intended behaviour are evident. @@ -93,15 +94,15 @@ know to access this driver interface is the name of the bound `Event`. This doc aims to demostrate that the event based approach can simplify application logic by eliminating the need for callbacks. -The design of `uasyncio` V3 and its `Event` class enables this approach +The design of `asyncio` V3 and its `Event` class enables this approach because: 1. A task waiting on an `Event` is put on a queue where it consumes no CPU cycles until the event is triggered. - 2. The design of `uasyncio` can support large numbers of tasks (hundreds) on + 2. The design of `asyncio` can support large numbers of tasks (hundreds) on a typical microcontroller. Proliferation of tasks is not a problem, especially where they are small and spend most of the time paused waiting on queues. -This contrasts with other schedulers (such as `uasyncio` V2) where there was no +This contrasts with other schedulers (such as `asyncio` V2) where there was no built-in `Event` class; typical `Event` implementations used [polling](./EVENTS.md#100-appendix-1-polling) and were convenience objects rather than performance solutions. @@ -151,7 +152,7 @@ Drivers exposing `Event` instances include: Applying `Events` to typical logic problems requires two new primitives: `WaitAny` and `WaitAll`. Each is an ELO. These primitives may be cancelled or -subject to a timeout with `uasyncio.wait_for()`, although judicious use of +subject to a timeout with `asyncio.wait_for()`, although judicious use of `Delay_ms` offers greater flexibility than `wait_for`. ## 4.1 WaitAny @@ -325,13 +326,16 @@ async def foo(): This document describes drivers for mechanical switches and pushbuttons. These have event based interfaces exclusively and support debouncing. The drivers are -simplified alternatives for +simplified alternatives for [Switch](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/switch.py) and [Pushbutton](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/pushbutton.py), which also support callbacks. ## 6.1 ESwitch +```python +from primitives import ESwitch +``` This provides a debounced interface to a switch connected to gnd or to 3V3. A pullup or pull down resistor should be supplied to ensure a valid logic level when the switch is open. The default constructor arg `lopen=1` is for a switch @@ -348,7 +352,7 @@ Constructor arguments: down as appropriate. 2. `lopen=1` Electrical level when switch is open circuit i.e. 1 is 3.3V, 0 is gnd. - + Methods: 1. `__call__` Call syntax e.g. `myswitch()` returns the logical debounced @@ -363,7 +367,7 @@ Bound objects: Application code is responsible for clearing the `Event` instances. Usage example: ```python -import uasyncio as asyncio +import asyncio from machine import Pin from primitives import ESwitch es = ESwitch(Pin("Y1", Pin.IN, Pin.PULL_UP)) @@ -390,7 +394,11 @@ asyncio.run(main()) ###### [Contents](./EVENTS.md#0-contents) ## 6.2 EButton - + +```python +from primitives import EButton +``` + This extends the functionality of `ESwitch` to provide additional events for long and double presses. @@ -479,12 +487,63 @@ determine whether the button is closed or open. ###### [Contents](./EVENTS.md#0-contents) +## 6.3 Keyboard + +```python +from primitives import Keyboard +``` +A `Keyboard` provides an interface to a set of pushbuttons arranged as a +crosspoint array. If a key is pressed its array index (scan code) is placed on a + queue. Keypresses are retrieved with `async for`. The driver operates by + polling each row, reading the response of each column. N-key rollover is + supported - this is the case where a key is pressed before the prior key has + been released. + + Example usage: +```python +import asyncio +from primitives import Keyboard +from machine import Pin +rowpins = [Pin(p, Pin.OUT) for p in range(10, 14)] +colpins = [Pin(p, Pin.IN, Pin.PULL_DOWN) for p in range(16, 20)] + +async def main(): + kp = Keyboard(rowpins, colpins) + async for scan_code in kp: + print(scan_code) + if not scan_code: + break # Quit on key with code 0 + +asyncio.run(main()) +``` +Constructor mandatory args: + * `rowpins` A list or tuple of initialised output pins. + * `colpins` A list or tuple of initialised input pins (pulled down). +Constructor optional keyword only args: + * `buffer=bytearray(10)` Keyboard buffer. + * `db_delay=50` Debounce delay in ms. + +The `Keyboard` class is subclassed from [Ringbuf queue](./EVENTS.md#7-ringbuf-queue) +enabling scan codes to be retrieved with an asynchronous iterator. + +In typical use the scan code would be used as the index into a string of +keyboard characters ordered to match the physical layout of the keys. If data +is not removed from the buffer, on overflow the oldest scan code is discarded. +There is no limit on the number of rows or columns however if more than 256 keys +are used, the `buffer` arg would need to be adapted to handle scan codes > 255. + +###### [Contents](./EVENTS.md#0-contents) + # 7. Ringbuf Queue +```python +from primitives import RingbufQueue +``` + The API of the `Queue` aims for CPython compatibility. This is at some cost to efficiency. As the name suggests, the `RingbufQueue` class uses a pre-allocated circular buffer which may be of any mutable type supporting the buffer protocol -e.g. `list`, `array` or `bytearray`. +e.g. `list`, `array` or `bytearray`. Attributes of `RingbufQueue`: 1. It is of fixed size, `Queue` can grow to arbitrary size. @@ -515,7 +574,7 @@ Asynchronous methods: block until space is available. * `get` Return an object from the queue. If empty, block until an item is available. - + Retrieving items from the queue: The `RingbufQueue` is an asynchronous iterator. Results are retrieved using @@ -539,7 +598,6 @@ def add_item(q, data): except IndexError: pass ``` - ###### [Contents](./EVENTS.md#0-contents) # 100 Appendix 1 Polling @@ -547,20 +605,20 @@ def add_item(q, data): The primitives or drivers referenced here do not use polling with the following exceptions: 1. Switch and pushbutton drivers. These poll the `Pin` instance for electrical - reasons described below. + reasons described below. 2. `ThreadSafeFlag` and subclass `Message`: these use the stream mechanism. Other drivers and primitives are designed such that paused tasks are waiting on queues and are therefore using no CPU cycles. [This reference][1e] states that bouncing contacts can assume invalid logic -levels for a period. It is a reaonable assumption that `Pin.value()` always +levels for a period. It is a reasonable assumption that `Pin.value()` always returns 0 or 1: the drivers are designed to cope with any sequence of such readings. By contrast, the behaviour of IRQ's under such conditions may be abnormal. It would be hard to prove that IRQ's could never be missed, across all platforms and input conditions. -Pin polling aims to use minimal resources, the main overhead being `uasyncio`'s +Pin polling aims to use minimal resources, the main overhead being `asyncio`'s task switching overhead: typically about 250 μs. The default polling interval is 50 ms giving an overhead of ~0.5%. diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index 88e6de2..f5122fd 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -38,7 +38,8 @@ $ mpremote mip install github:peterhinch/micropython-async/v3/threadsafe 1.3 [Threaded code on one core](./THREADING.md#13-threaded-code-on-one-core) 1.4 [Threaded code on multiple cores](./THREADING.md#14-threaded-code-on-multiple-cores) 1.5 [Globals](./THREADING.md#15-globals) - 1.6 [Debugging](./THREADING.md#16-debugging) + 1.6 [Allocation](./THREADING.md#16-allocation) + 1.7 [Debugging](./THREADING.md#17-debugging) 2. [Sharing data](./THREADING.md#2-sharing-data) 2.1 [A pool](./THREADING.md#21-a-pool) Sharing a set of variables. 2.2 [ThreadSafeQueue](./THREADING.md#22-threadsafequeue) @@ -146,7 +147,7 @@ async def foo(): await process(d) ``` -## 1.2 Soft Interrupt Service Routines +## 1.2 Soft Interrupt Service Routines This also includes code scheduled by `micropython.schedule()` which is assumed to have been called from a hard ISR. @@ -234,10 +235,20 @@ placeholder) before allowing other contexts to run. If globals must be created or destroyed dynamically, a lock must be used. -## 1.6 Debugging +## 1.6 Allocation + +Memory allocation must be prevented from occurring while a garbage collection +(GC) is in progress. Normally this is handled transparently by the GIL; where +there is no GIL a lock is used. The one exception is the case of a hard ISR. It +is invalid to have a hard ISR waiting on a lock. Consequently hard ISR's are +disallowed from allocating and an exception is thrown if this is attempted. + +Consequently code running in all other contexts is free to allocate. + +## 1.7 Debugging A key practical point is that coding errors in synchronising threads can be -hard to locate: consequences can be extremely rare bugs or (in the case of +hard to locate: consequences can be extremely rare bugs or (in the case of multi-core systems) crashes. It is vital to be careful in the way that communication between the contexts is achieved. This doc aims to provide some guidelines and code to assist in this task. @@ -463,7 +474,7 @@ def core_2(getq, putq): # Run on core 2 putq.put_sync(x, block=True) # Wait if queue fills. buf.clear() sleep_ms(30) - + async def sender(to_core2): x = 0 while True: @@ -648,8 +659,7 @@ again before it is accessed, the first data item will be lost. Blocking functions or methods have the potential of stalling the `uasyncio` scheduler. Short of rewriting them to work properly the only way to tame them is to run them in another thread. Any function to be run in this way must -conform to the guiedelines above, notably with regard to allocation and side -effects. +conform to the guiedelines above, notably with regard to side effects. ## 4.1 Basic approach diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index e05e5db..9767564 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -47,6 +47,7 @@ def _handle_exception(loop, context): "ESwitch": "events", "EButton": "events", "RingbufQueue": "ringbuf_queue", + "Keyboard": "events", } # Copied from uasyncio.__init__.py diff --git a/v3/primitives/events.py b/v3/primitives/events.py index bce928c..4aa8568 100644 --- a/v3/primitives/events.py +++ b/v3/primitives/events.py @@ -5,6 +5,7 @@ import uasyncio as asyncio from . import Delay_ms +from . import RingbufQueue # An Event-like class that can wait on an iterable of Event-like instances. # .wait pauses until any passed event is set. @@ -28,7 +29,7 @@ async def wt(self, event): await event.wait() self.evt.set() self.trig_event = event - + def event(self): return self.trig_event @@ -140,7 +141,7 @@ async def _ltf(self): # Long timeout await self._ltim.wait() self._ltim.clear() # Clear the event self.long.set() # User event - + # Runs if suppress set. Delay response to single press until sure it is a single short pulse. async def _dtf(self): while True: @@ -164,3 +165,40 @@ def deinit(self): task.cancel() for evt in (self.press, self.double, self.long, self.release): evt.clear() + +# A crosspoint array of pushbuttons +# Tuples/lists of pins. Rows are OUT, cols are IN +class Keyboard(RingbufQueue): + def __init__(self, rowpins, colpins, *, buffer=bytearray(10), db_delay=50): + super().__init__(buffer) + self.rowpins = rowpins + self.colpins = colpins + self.db_delay = db_delay # Deounce delay in ms + for opin in self.rowpins: # Initialise output pins + opin(0) + asyncio.create_task(self.scan(len(rowpins) * len(colpins))) + + async def scan(self, nbuttons): + prev = 0 + while True: + await asyncio.sleep_ms(0) + cur = 0 + for opin in self.rowpins: + opin(1) # Assert output + for ipin in self.colpins: + cur <<= 1 + cur |= ipin() + opin(0) + if cur != prev: # State change + pressed = cur & ~prev + prev = cur + if pressed: # Ignore button release + for v in range(nbuttons): # Find button index + if pressed & 1: + break + pressed >>= 1 + try: + self.put_nowait(v) + except IndexError: # q full. Overwrite oldest + pass + await asyncio.sleep_ms(self.db_delay) # Wait out bounce From 219c94e838d4684e241fcf2dbfcf52443ccee27d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 4 Oct 2023 17:06:44 +0100 Subject: [PATCH 247/305] Keyboard class: add __getitem__. --- v3/docs/EVENTS.md | 13 +++++++++++++ v3/primitives/events.py | 11 +++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 0b5c9f5..e18b69c 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -519,10 +519,16 @@ asyncio.run(main()) Constructor mandatory args: * `rowpins` A list or tuple of initialised output pins. * `colpins` A list or tuple of initialised input pins (pulled down). + Constructor optional keyword only args: * `buffer=bytearray(10)` Keyboard buffer. * `db_delay=50` Debounce delay in ms. + Magic method: + * `__getitem__(self, scan_code)` Return the state of a given pin. Enables code + that causes actions after a button press, for example on release or auto-repeat + while pressed. + The `Keyboard` class is subclassed from [Ringbuf queue](./EVENTS.md#7-ringbuf-queue) enabling scan codes to be retrieved with an asynchronous iterator. @@ -532,6 +538,13 @@ is not removed from the buffer, on overflow the oldest scan code is discarded. There is no limit on the number of rows or columns however if more than 256 keys are used, the `buffer` arg would need to be adapted to handle scan codes > 255. +##### Application note + +Scanning of the keyboard occurs rapidly, and built-in pull-down resistors have a +high value. If the capacitance between wires is high, spurious keypresses may be +registed. To prevent this it is wise to add physical resistors between the input +pins and gnd. A value in the region of 1KΩ to 5KΩ is recommended. + ###### [Contents](./EVENTS.md#0-contents) # 7. Ringbuf Queue diff --git a/v3/primitives/events.py b/v3/primitives/events.py index 4aa8568..5e78696 100644 --- a/v3/primitives/events.py +++ b/v3/primitives/events.py @@ -174,12 +174,15 @@ def __init__(self, rowpins, colpins, *, buffer=bytearray(10), db_delay=50): self.rowpins = rowpins self.colpins = colpins self.db_delay = db_delay # Deounce delay in ms + self._state = 0 # State of all keys as bit array for opin in self.rowpins: # Initialise output pins opin(0) asyncio.create_task(self.scan(len(rowpins) * len(colpins))) + def __getitem__(self, scan_code): + return bool(self._state & (1 << scan_code)) + async def scan(self, nbuttons): - prev = 0 while True: await asyncio.sleep_ms(0) cur = 0 @@ -189,9 +192,9 @@ async def scan(self, nbuttons): cur <<= 1 cur |= ipin() opin(0) - if cur != prev: # State change - pressed = cur & ~prev - prev = cur + if cur != self._state: # State change + pressed = cur & ~self._state + self._state = cur if pressed: # Ignore button release for v in range(nbuttons): # Find button index if pressed & 1: From 4e6c1b3a234de2f69d6a3239bde10f4c740152cd Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 4 Oct 2023 18:05:43 +0100 Subject: [PATCH 248/305] EVENTS.md: Add Keyboard usage example. --- v3/docs/EVENTS.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index e18b69c..7fc0920 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -528,7 +528,7 @@ Constructor optional keyword only args: * `__getitem__(self, scan_code)` Return the state of a given pin. Enables code that causes actions after a button press, for example on release or auto-repeat while pressed. - + The `Keyboard` class is subclassed from [Ringbuf queue](./EVENTS.md#7-ringbuf-queue) enabling scan codes to be retrieved with an asynchronous iterator. @@ -538,6 +538,36 @@ is not removed from the buffer, on overflow the oldest scan code is discarded. There is no limit on the number of rows or columns however if more than 256 keys are used, the `buffer` arg would need to be adapted to handle scan codes > 255. +Usage example. Keypresses on a numeric keypad are sent to a UART with auto + repeat. +```python +import asyncio +from primitives import Keyboard, Delay_ms +from machine import Pin, UART + +async def repeat(tim, uart, ch): # Send at least one char + while True: + uart.write(ch) + tim.clear() # Clear any pre-existing event + tim.trigger() # Start the timer + await tim.wait() + +async def main(): # Run forever + rowpins = [Pin(p, Pin.OUT) for p in range(10, 14)] + colpins = [Pin(p, Pin.IN, Pin.PULL_DOWN) for p in range(16, 20)] + uart = UART(0, 9600, tx=0, rx=1) + pad = Keyboard(rowpins, colpins) + tim = Delay_ms(duration=200) # 200ms auto repeat timer + cmap = "123456789*0#" # Numeric keypad character map + async for scan_code in pad: + ch = cmap[scan_code] # Get character + rpt = asyncio.create_task(repeat(tim, uart, ch)) + while pad[scan_code]: # While key is held down + await asyncio.sleep_ms(0) + rpt.cancel() + +asyncio.run(main()) +``` ##### Application note Scanning of the keyboard occurs rapidly, and built-in pull-down resistors have a From 6422422c48c8806d26082c5dcdbe37605392829b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 5 Oct 2023 09:48:45 +0100 Subject: [PATCH 249/305] events.py: Fix Keyboard behaviour on release. --- v3/primitives/events.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/v3/primitives/events.py b/v3/primitives/events.py index 5e78696..ba00df6 100644 --- a/v3/primitives/events.py +++ b/v3/primitives/events.py @@ -173,35 +173,33 @@ def __init__(self, rowpins, colpins, *, buffer=bytearray(10), db_delay=50): super().__init__(buffer) self.rowpins = rowpins self.colpins = colpins - self.db_delay = db_delay # Deounce delay in ms - self._state = 0 # State of all keys as bit array + self._state = 0 # State of all keys as bitmap for opin in self.rowpins: # Initialise output pins opin(0) - asyncio.create_task(self.scan(len(rowpins) * len(colpins))) + asyncio.create_task(self.scan(db_delay)) def __getitem__(self, scan_code): return bool(self._state & (1 << scan_code)) - async def scan(self, nbuttons): + async def scan(self, db_delay): while True: - await asyncio.sleep_ms(0) - cur = 0 + cur = 0 # Current bitmap of key states for opin in self.rowpins: opin(1) # Assert output for ipin in self.colpins: cur <<= 1 cur |= ipin() opin(0) - if cur != self._state: # State change - pressed = cur & ~self._state - self._state = cur - if pressed: # Ignore button release - for v in range(nbuttons): # Find button index - if pressed & 1: - break - pressed >>= 1 - try: - self.put_nowait(v) - except IndexError: # q full. Overwrite oldest - pass - await asyncio.sleep_ms(self.db_delay) # Wait out bounce + pressed = cur & ~self._state # Newly pressed + if pressed: # There is a newly pressed button + sc = 0 # Find its scan code + while not pressed & 1: + pressed >>= 1 + sc += 1 + try: + self.put_nowait(sc) + except IndexError: # q full. Overwrite oldest + pass + changed = cur ^ self._state # Any new press or release + self._state = cur + await asyncio.sleep_ms(db_delay if changed else 0) # Wait out bounce From 9a9b0a2f069db58db77a20d784030bf25c96a1db Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 5 Oct 2023 17:19:21 +0100 Subject: [PATCH 250/305] Keyboard: Allow simultaneous keystrokes. --- v3/primitives/events.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/v3/primitives/events.py b/v3/primitives/events.py index ba00df6..72942a4 100644 --- a/v3/primitives/events.py +++ b/v3/primitives/events.py @@ -176,12 +176,12 @@ def __init__(self, rowpins, colpins, *, buffer=bytearray(10), db_delay=50): self._state = 0 # State of all keys as bitmap for opin in self.rowpins: # Initialise output pins opin(0) - asyncio.create_task(self.scan(db_delay)) + asyncio.create_task(self.scan(len(rowpins) * len(colpins), db_delay)) def __getitem__(self, scan_code): return bool(self._state & (1 << scan_code)) - async def scan(self, db_delay): + async def scan(self, nkeys, db_delay): while True: cur = 0 # Current bitmap of key states for opin in self.rowpins: @@ -190,16 +190,14 @@ async def scan(self, db_delay): cur <<= 1 cur |= ipin() opin(0) - pressed = cur & ~self._state # Newly pressed - if pressed: # There is a newly pressed button - sc = 0 # Find its scan code - while not pressed & 1: + if pressed := (cur & ~self._state): # 1's are newly pressed button(s) + for sc in range(nkeys): + if pressed & 1: + try: + self.put_nowait(sc) + except IndexError: # q full. Overwrite oldest + pass pressed >>= 1 - sc += 1 - try: - self.put_nowait(sc) - except IndexError: # q full. Overwrite oldest - pass changed = cur ^ self._state # Any new press or release self._state = cur await asyncio.sleep_ms(db_delay if changed else 0) # Wait out bounce From d549e5bcb832b5ee072fc0a9ad8440440a91fd33 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 6 Oct 2023 20:09:47 +0100 Subject: [PATCH 251/305] Keyboard: Invert pin states for open drain output. --- v3/docs/EVENTS.md | 22 +++++++++++----------- v3/primitives/events.py | 10 +++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 7fc0920..3ab143d 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -494,18 +494,18 @@ from primitives import Keyboard ``` A `Keyboard` provides an interface to a set of pushbuttons arranged as a crosspoint array. If a key is pressed its array index (scan code) is placed on a - queue. Keypresses are retrieved with `async for`. The driver operates by - polling each row, reading the response of each column. N-key rollover is - supported - this is the case where a key is pressed before the prior key has - been released. +queue. Keypresses are retrieved with `async for`. The driver operates by +polling each row, reading the response of each column. 1-key rollover is +supported - this is the case where a key is pressed before the prior key has +been released. - Example usage: +Example usage: ```python import asyncio from primitives import Keyboard from machine import Pin -rowpins = [Pin(p, Pin.OUT) for p in range(10, 14)] -colpins = [Pin(p, Pin.IN, Pin.PULL_DOWN) for p in range(16, 20)] +rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] +colpins = [Pin(p, Pin.IN, Pin.PULL_UP) for p in range(16, 20)] async def main(): kp = Keyboard(rowpins, colpins) @@ -553,7 +553,7 @@ async def repeat(tim, uart, ch): # Send at least one char await tim.wait() async def main(): # Run forever - rowpins = [Pin(p, Pin.OUT) for p in range(10, 14)] + rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] colpins = [Pin(p, Pin.IN, Pin.PULL_DOWN) for p in range(16, 20)] uart = UART(0, 9600, tx=0, rx=1) pad = Keyboard(rowpins, colpins) @@ -570,10 +570,10 @@ asyncio.run(main()) ``` ##### Application note -Scanning of the keyboard occurs rapidly, and built-in pull-down resistors have a +Scanning of the keyboard occurs rapidly, and built-in pull-up resistors have a high value. If the capacitance between wires is high, spurious keypresses may be -registed. To prevent this it is wise to add physical resistors between the input -pins and gnd. A value in the region of 1KΩ to 5KΩ is recommended. +registered. To prevent this it is wise to add physical resistors between the +input pins and 3.3V. A value in the region of 1KΩ to 5KΩ is recommended. ###### [Contents](./EVENTS.md#0-contents) diff --git a/v3/primitives/events.py b/v3/primitives/events.py index 72942a4..9be4388 100644 --- a/v3/primitives/events.py +++ b/v3/primitives/events.py @@ -175,7 +175,7 @@ def __init__(self, rowpins, colpins, *, buffer=bytearray(10), db_delay=50): self.colpins = colpins self._state = 0 # State of all keys as bitmap for opin in self.rowpins: # Initialise output pins - opin(0) + opin(1) asyncio.create_task(self.scan(len(rowpins) * len(colpins), db_delay)) def __getitem__(self, scan_code): @@ -183,13 +183,13 @@ def __getitem__(self, scan_code): async def scan(self, nkeys, db_delay): while True: - cur = 0 # Current bitmap of key states + cur = 0 # Current bitmap of logical key states for opin in self.rowpins: - opin(1) # Assert output + opin(0) # Assert output for ipin in self.colpins: cur <<= 1 - cur |= ipin() - opin(0) + cur |= ipin() ^ 1 # Convert physical to logical + opin(1) if pressed := (cur & ~self._state): # 1's are newly pressed button(s) for sc in range(nkeys): if pressed & 1: From 1e27280accafd6780c40518f1d6580248763b0ea Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 11 Oct 2023 16:41:41 +0100 Subject: [PATCH 252/305] Add sw_array.py and 1st pass at docs. --- v3/docs/EVENTS.md | 61 ++++++++++++++++ v3/docs/isolate.png | Bin 0 -> 58184 bytes v3/primitives/__init__.py | 3 +- v3/primitives/events.py | 36 --------- v3/primitives/package.json | 3 +- v3/primitives/sw_array.py | 146 +++++++++++++++++++++++++++++++++++++ 6 files changed, 211 insertions(+), 38 deletions(-) create mode 100644 v3/docs/isolate.png create mode 100644 v3/primitives/sw_array.py diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 3ab143d..d693c57 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -575,6 +575,67 @@ high value. If the capacitance between wires is high, spurious keypresses may be registered. To prevent this it is wise to add physical resistors between the input pins and 3.3V. A value in the region of 1KΩ to 5KΩ is recommended. +## 6.4 SwArray +```python +from primitives import SwArray +``` +An `SwArray` is similar to a `Keyboard` except that single, double and long +presses are supported. Items in the array may be switches or pushbuttons, +however if switches are used they must be diode-isolated. This is because +pushbuttons are normally open, while switches may be left in open or closed +states. If more than two switches are closed, unwanted electrical connections +are made. +![Image](./isolate.jpg) + +Constructor mandatory args: + * `rowpins` A list or tuple of initialised output pins. + * `colpins` A list or tuple of initialised input pins (pulled down). + * `cfg` An integer defining conditions requiring a response. See Module + Constants below. + +Constructor optional keyword only args: + * `bufsize=10` Size of buffer. + + Magic method: + * `__getitem__(self, scan_code)` Return the state of a given pin. Enables code + that causes actions after a button press, for example on release or auto-repeat + while pressed. + + Class variables: + * `debounce_ms = 50` + * `long_press_ms = 1000` + * `double_click_ms = 400` + +Module constants. +The `cfg` constructor arg may be defined as the bitwise or of these constants. +If the `CLOSE` bit is specified, switch closures will be reported + * `CLOSE = const(1)` Contact closure. + * `OPEN = const(2)` Contact opening. + * `LONG = const(4)` Contact closure longer than `long_press_ms`. + * `DOUBLE = const(8)` Two closures in less than `double_click_ms`. + * `SUPPRESS = const(16)` # Disambiguate. For explanation see `EButton`. + +The `SwArray` class is subclassed from [Ringbuf queue](./EVENTS.md#7-ringbuf-queue) +enabling scan codes and event types to be retrieved with an asynchronous iterator. + +```python +import asyncio +from primitives import SwArray +from machine import Pin +rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] +colpins = [Pin(p, Pin.IN, Pin.PULL_UP) for p in range(16, 20)] + +async def main(): + cfg = CLOSE | OPEN #LONG | DOUBLE | SUPPRESS + swa = SwArray(rowpins, colpins, cfg) + async for scan_code, evt in swa: + print(scan_code, evt) + if not scan_code: + break # Quit on key with code 0 + +asyncio.run(main()) +``` + ###### [Contents](./EVENTS.md#0-contents) # 7. Ringbuf Queue diff --git a/v3/docs/isolate.png b/v3/docs/isolate.png new file mode 100644 index 0000000000000000000000000000000000000000..d92fc409d809ab33c02add50aee5e7616a96cce1 GIT binary patch literal 58184 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 O33F2`lS*T^=>H3WAUZ_= literal 0 HcmV?d00001 diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index 9767564..523a13a 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -47,7 +47,8 @@ def _handle_exception(loop, context): "ESwitch": "events", "EButton": "events", "RingbufQueue": "ringbuf_queue", - "Keyboard": "events", + "Keyboard": "sw_array", + "SwArray": "sw_array", } # Copied from uasyncio.__init__.py diff --git a/v3/primitives/events.py b/v3/primitives/events.py index 9be4388..8fe436e 100644 --- a/v3/primitives/events.py +++ b/v3/primitives/events.py @@ -165,39 +165,3 @@ def deinit(self): task.cancel() for evt in (self.press, self.double, self.long, self.release): evt.clear() - -# A crosspoint array of pushbuttons -# Tuples/lists of pins. Rows are OUT, cols are IN -class Keyboard(RingbufQueue): - def __init__(self, rowpins, colpins, *, buffer=bytearray(10), db_delay=50): - super().__init__(buffer) - self.rowpins = rowpins - self.colpins = colpins - self._state = 0 # State of all keys as bitmap - for opin in self.rowpins: # Initialise output pins - opin(1) - asyncio.create_task(self.scan(len(rowpins) * len(colpins), db_delay)) - - def __getitem__(self, scan_code): - return bool(self._state & (1 << scan_code)) - - async def scan(self, nkeys, db_delay): - while True: - cur = 0 # Current bitmap of logical key states - for opin in self.rowpins: - opin(0) # Assert output - for ipin in self.colpins: - cur <<= 1 - cur |= ipin() ^ 1 # Convert physical to logical - opin(1) - if pressed := (cur & ~self._state): # 1's are newly pressed button(s) - for sc in range(nkeys): - if pressed & 1: - try: - self.put_nowait(sc) - except IndexError: # q full. Overwrite oldest - pass - pressed >>= 1 - changed = cur ^ self._state # Any new press or release - self._state = cur - await asyncio.sleep_ms(db_delay if changed else 0) # Wait out bounce diff --git a/v3/primitives/package.json b/v3/primitives/package.json index 4c823f2..8f7e7a7 100644 --- a/v3/primitives/package.json +++ b/v3/primitives/package.json @@ -11,7 +11,8 @@ ["primitives/queue.py", "github:peterhinch/micropython-async/v3/primitives/queue.py"], ["primitives/ringbuf_queue.py", "github:peterhinch/micropython-async/v3/primitives/ringbuf_queue.py"], ["primitives/semaphore.py", "github:peterhinch/micropython-async/v3/primitives/semaphore.py"], - ["primitives/switch.py", "github:peterhinch/micropython-async/v3/primitives/switch.py"] + ["primitives/switch.py", "github:peterhinch/micropython-async/v3/primitives/switch.py"], + ["primitives/sw_array.py", "github:peterhinch/micropython-async/v3/primitives/sw_array.py"] ], "version": "0.1" } diff --git a/v3/primitives/sw_array.py b/v3/primitives/sw_array.py new file mode 100644 index 0000000..6ca1ea8 --- /dev/null +++ b/v3/primitives/sw_array.py @@ -0,0 +1,146 @@ +# sw_array.py A crosspoint array of pushbuttons + +# Copyright (c) 2023 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import asyncio +from . import RingbufQueue +from time import ticks_ms, ticks_diff + +# A crosspoint array of pushbuttons +# Tuples/lists of pins. Rows are OUT, cols are IN +class Keyboard(RingbufQueue): + def __init__(self, rowpins, colpins, *, buffer=bytearray(10), db_delay=50): + super().__init__(buffer) + self.rowpins = rowpins + self.colpins = colpins + self._state = 0 # State of all keys as bitmap + for opin in self.rowpins: # Initialise output pins + opin(1) + asyncio.create_task(self.scan(len(rowpins) * len(colpins), db_delay)) + + def __getitem__(self, scan_code): + return bool(self._state & (1 << scan_code)) + + async def scan(self, nkeys, db_delay): + while True: + cur = 0 # Current bitmap of logical key states + for opin in self.rowpins: + opin(0) # Assert output + for ipin in self.colpins: + cur <<= 1 + cur |= ipin() ^ 1 # Convert physical to logical + opin(1) + if pressed := (cur & ~self._state): # 1's are newly pressed button(s) + for sc in range(nkeys): + if pressed & 1: + try: + self.put_nowait(sc) + except IndexError: # q full. Overwrite oldest + pass + pressed >>= 1 + changed = cur ^ self._state # Any new press or release + self._state = cur + await asyncio.sleep_ms(db_delay if changed else 0) # Wait out bounce + +CLOSE = const(1) # cfg comprises the OR of these constants +OPEN = const(2) +LONG = const(4) +DOUBLE = const(8) +SUPPRESS = const(16) # Disambiguate + +# Entries in queue are (scan_code, event) where event is an OR of above constants +# Tuples/lists of pins. Rows are OUT, cols are IN +class SwArray(RingbufQueue): + debounce_ms = 50 # Attributes can be varied by user + long_press_ms = 1000 + double_click_ms = 400 + def __init__(self, rowpins, colpins, cfg, *, bufsize=10): + super().__init__(bufsize) + self._rowpins = rowpins + self._colpins = colpins + self._cfg = cfg + self._state = 0 # State of all keys as bitmap + self._flags = 0 # Busy bitmap + self._basic = not bool(cfg & (SUPPRESS | LONG | DOUBLE)) # Basic mode + self._suppress = bool(cfg & SUPPRESS) + for opin in self._rowpins: # Initialise output pins + opin(1) + asyncio.create_task(self._scan(len(rowpins) * len(colpins))) + + def __getitem__(self, scan_code): + return bool(self._state & (1 << scan_code)) + + def _put(self, sc, evt): + if evt & self._cfg: # Only if user has requested it + try: + self.put_nowait((sc, evt)) + except IndexError: # q full. Overwrite oldest + pass + + def _timeout(self, ts, condition): + t = SwArray.long_press_ms if condition == LONG else SwArray.double_click_ms + return ticks_diff(ticks_ms(), ts) > t + + def _busy(self, sc, v): + of = self._flags # Return prior state + if v: + self._flags |= 1 << sc + else: + self._flags &= ~(1 << sc) + return (of >> sc) & 1 + + async def _finish(self, sc): # Tidy up. If necessary await a contact open + while self[sc]: + await asyncio.sleep_ms(0) + self._put(sc, OPEN) + self._busy(sc, False) + + # Handle long, double. Switch has closed. + async def _defer(self, sc): + # Wait for contact closure to be registered: let calling loop complete + await asyncio.sleep_ms(0) + ts = ticks_ms() + if not self._suppress: + self._put(sc, CLOSE) + while self[sc]: # Pressed + await asyncio.sleep_ms(0) + if self._timeout(ts, LONG): + self._put(sc, LONG) + await self._finish(sc) + return + if not self._suppress: + self._put(sc, OPEN) + while not self[sc]: + await asyncio.sleep_ms(0) + if self._timeout(ts, DOUBLE): # No second closure + self._put(sc, CLOSE) # Single press. Report CLOSE + await self._finish(sc) # then OPEN + return + self._put(sc, DOUBLE) + await self._finish(sc) + + async def _scan(self, nkeys): + db_delay = SwArray.debounce_ms + while True: + cur = 0 # Current bitmap of logical key states (1 == pressed) + for opin in self._rowpins: + opin(0) # Assert output + for ipin in self._colpins: + cur <<= 1 + cur |= ipin() ^ 1 # Convert physical to logical + opin(1) + curb = cur # Copy current bitmap + if changed := (cur ^ self._state): # 1's are newly canged button(s) + for sc in range(nkeys): + if (changed & 1): # Current key has changed state + if self._basic: # No timed behaviour + self._put(sc, CLOSE if cur & 1 else OPEN) + elif cur & 1: # Closed + if not self._busy(sc, True): # Currently not busy + asyncio.create_task(self._defer(sc)) # Q is handled asynchronously + changed >>= 1 + cur >>= 1 + changed = curb ^ self._state # Any new press or release + self._state = curb + await asyncio.sleep_ms(db_delay if changed else 0) # Wait out bounce From 2129a0701ccacd5157c26d47adee42c5caf630a4 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 11 Oct 2023 16:55:17 +0100 Subject: [PATCH 253/305] Add sw_array.py and 1st pass at docs. --- v3/docs/DRIVERS.md | 8 +++++--- v3/docs/EVENTS.md | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index ac9f9b9..98aa90b 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -48,8 +48,10 @@ To install the library, connect the target hardware to WiFi and issue: import mip mip.install("github:peterhinch/micropython-async/v3/primitives") ``` -For non-networked targets use `mpremote` as described in -[the official docs](http://docs.micropython.org/en/latest/reference/packages.html#installing-packages-with-mpremote). +For any target including non-networked ones use `mpremote`: +```bash +$ mpremote mip install "github:peterhinch/micropython-async/v3/primitives" +``` Drivers are imported with: ```python @@ -102,7 +104,7 @@ implicit: contact bounce will not cause spurious execution of the `callable`. Constructor argument (mandatory): 1. `pin` The initialised Pin instance. - + Methods: 1. `close_func` Args: `func` (mandatory) a `callable` to run on contact diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index d693c57..c705bac 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -620,7 +620,7 @@ enabling scan codes and event types to be retrieved with an asynchronous iterato ```python import asyncio -from primitives import SwArray +from primitives.sw_array import SwArray, CLOSE, OPEN, LONG, DOUBLE, SUPPRESS from machine import Pin rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] colpins = [Pin(p, Pin.IN, Pin.PULL_UP) for p in range(16, 20)] From 076740535a14801df73e4e3034410ee71b4d2e9d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 12 Oct 2023 09:31:11 +0100 Subject: [PATCH 254/305] EVENTS.md: Add diode isolation image. --- v3/docs/EVENTS.md | 4 ++-- v3/docs/isolate.png | Bin 58184 -> 37576 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index c705bac..39252fd 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -584,8 +584,8 @@ presses are supported. Items in the array may be switches or pushbuttons, however if switches are used they must be diode-isolated. This is because pushbuttons are normally open, while switches may be left in open or closed states. If more than two switches are closed, unwanted electrical connections -are made. -![Image](./isolate.jpg) +are made. The diodes prevent this. +![Image](./isolate.png) Constructor mandatory args: * `rowpins` A list or tuple of initialised output pins. diff --git a/v3/docs/isolate.png b/v3/docs/isolate.png index d92fc409d809ab33c02add50aee5e7616a96cce1..0c6f368029a122867b9b3941c11c2fc0178bd746 100644 GIT binary patch literal 37576 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 K6kp1lc>ga!=74Yj literal 58184 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 O33F2`lS*T^=>H3WAUZ_= From abb070c396cb6e7d966d8ce0451d4b2071544a78 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 12 Oct 2023 13:16:11 +0100 Subject: [PATCH 255/305] SwArray: Add keymap bound method. --- v3/docs/EVENTS.md | 56 +++++++++++++++++++++++++-------------- v3/primitives/sw_array.py | 17 +++++++----- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 39252fd..952d32f 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -517,8 +517,8 @@ async def main(): asyncio.run(main()) ``` Constructor mandatory args: - * `rowpins` A list or tuple of initialised output pins. - * `colpins` A list or tuple of initialised input pins (pulled down). + * `rowpins` A list or tuple of initialised open drain output pins. + * `colpins` A list or tuple of initialised input pins (pulled up). Constructor optional keyword only args: * `buffer=bytearray(10)` Keyboard buffer. @@ -554,7 +554,7 @@ async def repeat(tim, uart, ch): # Send at least one char async def main(): # Run forever rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] - colpins = [Pin(p, Pin.IN, Pin.PULL_DOWN) for p in range(16, 20)] + colpins = [Pin(p, Pin.IN, Pin.PULL_UP) for p in range(16, 20)] uart = UART(0, 9600, tx=0, rx=1) pad = Keyboard(rowpins, colpins) tim = Delay_ms(duration=200) # 200ms auto repeat timer @@ -575,6 +575,8 @@ high value. If the capacitance between wires is high, spurious keypresses may be registered. To prevent this it is wise to add physical resistors between the input pins and 3.3V. A value in the region of 1KΩ to 5KΩ is recommended. +###### [Contents](./EVENTS.md#0-contents) + ## 6.4 SwArray ```python from primitives import SwArray @@ -588,8 +590,8 @@ are made. The diodes prevent this. ![Image](./isolate.png) Constructor mandatory args: - * `rowpins` A list or tuple of initialised output pins. - * `colpins` A list or tuple of initialised input pins (pulled down). + * `rowpins` A list or tuple of initialised open drain output pins. + * `colpins` A list or tuple of initialised input pins (pulled up). * `cfg` An integer defining conditions requiring a response. See Module Constants below. @@ -601,32 +603,40 @@ Constructor optional keyword only args: that causes actions after a button press, for example on release or auto-repeat while pressed. + Synchronous bound method: + * `keymap()` Return an integer representing a bitmap of the debounced state of + all switches in the array. 1 == closed. + Class variables: - * `debounce_ms = 50` - * `long_press_ms = 1000` - * `double_click_ms = 400` + * `debounce_ms = 50` Assumed maximum duration of contact bounce. + * `long_press_ms = 1000` Threshold for long press detection. + * `double_click_ms = 400` Threshold for double-click detection. Module constants. -The `cfg` constructor arg may be defined as the bitwise or of these constants. -If the `CLOSE` bit is specified, switch closures will be reported - * `CLOSE = const(1)` Contact closure. - * `OPEN = const(2)` Contact opening. - * `LONG = const(4)` Contact closure longer than `long_press_ms`. - * `DOUBLE = const(8)` Two closures in less than `double_click_ms`. - * `SUPPRESS = const(16)` # Disambiguate. For explanation see `EButton`. - -The `SwArray` class is subclassed from [Ringbuf queue](./EVENTS.md#7-ringbuf-queue) -enabling scan codes and event types to be retrieved with an asynchronous iterator. - +The folowing constants are provided to simplify defining the `cfg` constructor +arg. This may be defined as a bitwise or of selected constants. For example if +the `CLOSE` bit is specified, switch closures will be reported. An omitted event +will be ignored. Where the array comprises switches it is usual to specify only +`CLOSE` and/or `OPEN`. This invokes a more efficient mode of operation because +timing is not required. + * `CLOSE` Report contact closure. + * `OPEN` Contact opening. + * `LONG` Contact closure longer than `long_press_ms`. + * `DOUBLE` Two closures in less than `double_click_ms`. + * `SUPPRESS` Disambiguate. For explanation see `EButton`. + +The `SwArray` class is subclassed from [Ringbuf queue](./EVENTS.md#7-ringbuf-queue). +This is an asynchronous iterator, enabling scan codes and event types to be +retrieved as state changes occur with `async for`: ```python import asyncio from primitives.sw_array import SwArray, CLOSE, OPEN, LONG, DOUBLE, SUPPRESS from machine import Pin rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] colpins = [Pin(p, Pin.IN, Pin.PULL_UP) for p in range(16, 20)] +cfg = CLOSE | OPEN #LONG | DOUBLE | SUPPRESS async def main(): - cfg = CLOSE | OPEN #LONG | DOUBLE | SUPPRESS swa = SwArray(rowpins, colpins, cfg) async for scan_code, evt in swa: print(scan_code, evt) @@ -635,6 +645,12 @@ async def main(): asyncio.run(main()) ``` +##### Application note + +Scanning of the array occurs rapidly, and built-in pull-up resistors have a +high value. If the capacitance between wires is high, spurious closures may be +registered. To prevent this it is wise to add physical resistors between the +input pins and 3.3V. A value in the region of 1KΩ to 5KΩ is recommended. ###### [Contents](./EVENTS.md#0-contents) diff --git a/v3/primitives/sw_array.py b/v3/primitives/sw_array.py index 6ca1ea8..13fbf05 100644 --- a/v3/primitives/sw_array.py +++ b/v3/primitives/sw_array.py @@ -47,10 +47,11 @@ async def scan(self, nkeys, db_delay): OPEN = const(2) LONG = const(4) DOUBLE = const(8) -SUPPRESS = const(16) # Disambiguate +SUPPRESS = const(16) # Disambiguate: see docs. -# Entries in queue are (scan_code, event) where event is an OR of above constants -# Tuples/lists of pins. Rows are OUT, cols are IN +# Entries in queue are (scan_code, event) where event is an OR of above constants. +# rowpins/colpins are tuples/lists of pins. Rows are OUT, cols are IN. +# cfg is a logical OR of above constants. If a bit is 0 that state will never be reported. class SwArray(RingbufQueue): debounce_ms = 50 # Attributes can be varied by user long_press_ms = 1000 @@ -60,12 +61,12 @@ def __init__(self, rowpins, colpins, cfg, *, bufsize=10): self._rowpins = rowpins self._colpins = colpins self._cfg = cfg - self._state = 0 # State of all keys as bitmap + self._state = 0 # State of all buttons as bitmap self._flags = 0 # Busy bitmap self._basic = not bool(cfg & (SUPPRESS | LONG | DOUBLE)) # Basic mode self._suppress = bool(cfg & SUPPRESS) for opin in self._rowpins: # Initialise output pins - opin(1) + opin(1) # open circuit asyncio.create_task(self._scan(len(rowpins) * len(colpins))) def __getitem__(self, scan_code): @@ -96,6 +97,8 @@ async def _finish(self, sc): # Tidy up. If necessary await a contact open self._put(sc, OPEN) self._busy(sc, False) + def keymap(self): # Return a bitmap of debounced state of all buttons/switches + return self._state # Handle long, double. Switch has closed. async def _defer(self, sc): # Wait for contact closure to be registered: let calling loop complete @@ -123,7 +126,7 @@ async def _defer(self, sc): async def _scan(self, nkeys): db_delay = SwArray.debounce_ms while True: - cur = 0 # Current bitmap of logical key states (1 == pressed) + cur = 0 # Current bitmap of logical button states (1 == pressed) for opin in self._rowpins: opin(0) # Assert output for ipin in self._colpins: @@ -133,7 +136,7 @@ async def _scan(self, nkeys): curb = cur # Copy current bitmap if changed := (cur ^ self._state): # 1's are newly canged button(s) for sc in range(nkeys): - if (changed & 1): # Current key has changed state + if (changed & 1): # Current button has changed state if self._basic: # No timed behaviour self._put(sc, CLOSE if cur & 1 else OPEN) elif cur & 1: # Closed From 36d10c2c56fa181ab750416ad9154db923f32354 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 23 Oct 2023 13:32:30 +0100 Subject: [PATCH 256/305] DRIVERS.md: First draft. --- v3/docs/DRIVERS.md | 805 ++++++++++++++++++++++++------- v3/docs/EVENTS.md | 4 +- v3/docs/{ => images}/isolate.png | Bin v3/docs/images/keypad.png | Bin 0 -> 14577 bytes v3/primitives/sw_array.py | 19 +- 5 files changed, 636 insertions(+), 192 deletions(-) rename v3/docs/{ => images}/isolate.png (100%) create mode 100644 v3/docs/images/keypad.png diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 98aa90b..2e5338f 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -1,45 +1,134 @@ -# 0. Introduction +This document describes classes designed to enhance the capability of +MicroPython's `asyncio` when used in a microcontroller context. -Drivers for switches and pushbuttons are provided. Switch and button drivers -support debouncing. The switch driver provides for running a callback or -launching a coroutine (coro) on contact closure and/or opening. The pushbutton -driver extends this to support long-press and double-click events. The drivers -now support an optional event driven interface as a more flexible alternative -to callbacks. +# 0. Contents -An `Encoder` class is provided to support rotary control knobs based on -quadrature encoder switches. This is not intended for high throughput encoders -as used in CNC machines where -[an interrupt based solution](https://github.com/peterhinch/micropython-samples#47-rotary-incremental-encoder) -is required. - -The asynchronous ADC supports pausing a task until the value read from an ADC -goes outside defined bounds. - -# 1. Contents - - 1. [Contents](./DRIVERS.md#1-contents) + 1. [Introduction](./DRIVERS.md#1-introduction) + 1.1 [API Design](./DRIVERS.md#11-api-design) Callbacks vs. asynchronous interfaces. + 1.2 [Switches](./DRIVERS.md#12-switches) Electrical considerations. 2. [Installation and usage](./DRIVERS.md#2-installation-and-usage) - 3. [Interfacing switches](./DRIVERS.md#3-interfacing-switches) Switch debouncer with callbacks. - 3.1 [Switch class](./DRIVERS.md#31-switch-class) - 3.2 [Event interface](./DRIVERS.md#32-event-interface) + 3. [Interfacing switches](./DRIVERS.md#3-interfacing-switches) + 3.1 [ESwitch class](./DRIVERS.md#31-eswitch-class) Switch debouncer with event interface. + 3.2 [Switch class](./DRIVERS.md#32-switch-class) Switch debouncer with callbacks. 4. [Interfacing pushbuttons](./DRIVERS.md#4-interfacing-pushbuttons) Extends Switch for long and double-click events - 4.1 [Pushbutton class](./DRIVERS.md#41-pushbutton-class) -      4.1.1 [The suppress constructor argument](./DRIVERS.md#411-the-suppress-constructor-argument) -      4.1.2 [The sense constructor argument](./DRIVERS.md#412-the-sense-constructor-argument) - 4.2 [ESP32Touch class](./DRIVERS.md#42-esp32touch-class) + 4.1 [EButton class](./DRIVERS.md#41-ebutton-class) Pushbutton with Event-based interface. + 4.2 [Pushbutton class](./DRIVERS.md#42-pushbutton-class) +      4.2.1 [The suppress constructor argument](./DRIVERS.md#431-the-suppress-constructor-argument) +      4.2.2 [The sense constructor argument](./DRIVERS.md#432-the-sense-constructor-argument) + 4.3 [ESP32Touch class](./DRIVERS.md#43-esp32touch-class) + 4.4 [keyboard class](./DRIVERS.md#44-keyboard-class) + 4.5 [SwArray class](./DRIVERS.md#45-swarray-class) + 4.6 [Suppress mode](./DRIVERS.md#46-suppress-mode) Reduce the number of events/callbacks. 5. [ADC monitoring](./DRIVERS.md#5-adc-monitoring) Pause until an ADC goes out of bounds 5.1 [AADC class](./DRIVERS.md#51-aadc-class) 5.2 [Design note](./DRIVERS.md#52-design-note) 6. [Quadrature encoders](./DRIVERS.md#6-quadrature-encoders) 6.1 [Encoder class](./DRIVERS.md#61-encoder-class) - 7. [Additional functions](./DRIVERS.md#7-additional-functions) - 7.1 [launch](./DRIVERS.md#71-launch) Run a coro or callback interchangeably - 7.2 [set_global_exception](./DRIVERS.md#72-set_global_exception) Simplify debugging with a global exception handler. - 8. [Event based interface](./DRIVERS.md#8-event-based-interface) An alternative interface to Switch and Pushbutton objects. + 7. [Ringbuf Queue](./DRIVERS.md#7-ringbuf-queue) A MicroPython optimised queue primitive. + 8. [Additional functions](./DRIVERS.md#8-additional-functions) + 8.1 [launch](./DRIVERS.md#81-launch) Run a coro or callback interchangeably + 8.2 [set_global_exception](./DRIVERS.md#82-set_global_exception) Simplify debugging with a global exception handler. + 9. [Event based interface](./DRIVERS.md#9-event-based-interface) An alternative interface to Switch and Pushbutton objects. ###### [Tutorial](./TUTORIAL.md#contents) +# 1. Introduction + +The classes presented here include asynchronous interfaces to switches, +pushbuttons, incremental encoders and ADC's. Specifically they are interfaces to +devices defined in the `machine` module rather than device drivers for external +hardware: as such they are grouped with synchronisation primitives. There are +also synchronisation primitives providing a microcontroller-optimised alternative +to the existing CPython-compatible primitives. + +## 1.1 API design + +The traditional interface to asynchronous external events is a callback. When +the event occurs, the device driver runs a user-specified callback. Some classes +described here offer a callback interface; newer designs have abandoned this in +favour of asynchronous interfaces by exposing `Event` or asynchronous iterator +interfaces. Note that where callbacks are used the term `callable` implies a +Python `callable`: namely a function, bound method, coroutine or bound +coroutine. Any of these may be supplied as a callback function. + +Asynchronous interfaces allow the use of callbacks using patterns like the +following. In this case the device is an asynchronous iterator: +```python + async def run_callback(device, callback, *args): + async for result in device: + callback(result, *args) +``` +or, where the device presents an `Event` interface: +```python +async def run_callback(device, callback, *args): + while True: + await device.wait() # Wait on the Event + device.clear() # Clear it down + callback(*args) +``` +It is arguable that callbacks are outdated. Handling of arguments and return +values is messy and there are usually better ways using asynchronous coding. In +particular MicroPython's `asyncio` implements asynchronous interfaces in an +efficient manner. A task waiting on an `Event` consumes minimal resources. + +## 1.2 Switches + +From an electrical standpoint switches and pushbuttons are identical, however +from a programming perspective a switch is either open or closed, while a +pushbutton may be subject to single or double clicks, or to long presses. +Consequently switch drivers expose a simpler interface with a consequent saving +in code size. + +All switch drivers rely on millisecond-level timing: callback functions must +be designed to terminate rapidly. This applies to all functions in the +application; coroutines should yield regularly. If these constraints are not +met, switch events can be missed. + +All switches are prone to contact bounce, with a consequent risk of spurious +events: the drivers presented here embody debouncing. The phenomenon of contact +bounce is discussed in [this document](http://www.ganssle.com/debouncing.htm). + +Two ways of wiring switches are supported. For small numbers of switches, the +switch may link a pin to `gnd` with the pin being configured as an input with a +pull up resistor. Interfacing such a switch is simple: +```Python +import asyncio +from machine import Pin +from primitives import ESwitch +es = ESwitch(Pin(16, Pin.IN, Pin.PULL_UP)) + +async def closure(): + while True: + es.close.clear() # Clear the Event + await es.close.wait() # Wait for contact closure + print("Closed") # Run code + +asyncio.run(closure()) +``` + +As the number of switches increases, consumption of GPIO pins can be +problematic. A solution is to wire the switches as a crosspoint array with the +driver polling each row in turn and reading the columns. This is the usual configuration of keypads. + +![Image](./images/keypad.png) + +Crosspoint connection requires precautions to +cater for the case where multiple contacts are closed simultaneously, as this +can have the effect of linking two output pins. Risk of damage is averted by +defining the outputs as open drain. This allows for one key rollover: if a +second key is pressed before the first is released, the keys will be read +correctly. Invalid contact closures may be registered if more than two contacts +are closed. This also applies where the matrix comprises switches rather than +buttons. In this case diode isolation is required: + +![Image](./images/isolate.png) + +Whether or not diodes are used the column input pins must be pulled up. Scanning +of the array occurs rapidly, and built-in pull-up resistors have a high value. +If the capacitance between wires is high, spurious closures may be registered. +To prevent this it is wise to add physical resistors between the input pins and +3.3V. A value in the region of 1KΩ to 5KΩ is recommended. + # 2. Installation and usage The latest release build of firmware or a newer nightly build is recommended. @@ -71,29 +160,84 @@ from primitives.tests.adctest import test test() ``` -###### [Contents](./DRIVERS.md#1-contents) +###### [Contents](./DRIVERS.md#0-contents) # 3. Interfacing switches -The `primitives.switch` module provides the `Switch` class. This supports -debouncing a normally open switch connected between a pin and ground. Can run -callbacks or schedule coros on contact closure and/or opening. As an -alternative to a callback based interface, bound `Event` objects may be -triggered on switch state changes. To use an `Event` based interface -exclusively see the simpler [ESwitch class](./EVENTS.md#61-eswitch). +The `primitives` module provides `ESwitch` and `Switch` classes. The former is a +minimal driver providing an `Event` interface. The latter supports callbacks and +`Event`s. -In the following text the term `callable` implies a Python `callable`: namely a -function, bound method, coroutine or bound coroutine. The term implies that any -of these may be supplied. +## 3.1 ESwitch class -### Timing +```python +from primitives import ESwitch # evennts.py +``` +This provides a debounced interface to a switch connected to gnd or to 3V3. A +pullup or pull down resistor should be supplied to ensure a valid logic level +when the switch is open. The default constructor arg `lopen=1` is for a switch +connected between the pin and gnd, with a pullup to 3V3. Typically the pullup +is internal, the pin being as follows: +```python +from machine import Pin +pin_id = 0 # Depends on hardware +pin = Pin(pin_id, Pin.IN, Pin.PULL_UP) +``` +Constructor arguments: -The `Switch` class relies on millisecond-level timing: callback functions must -be designed to terminate rapidly. This applies to all functions in the -application; coroutines should yield regularly. If these constraints are not -met, switch events can be missed. + 1. `pin` The Pin instance: should be initialised as an input with a pullup or + down as appropriate. + 2. `lopen=1` Electrical level when switch is open circuit i.e. 1 is 3.3V, 0 is + gnd. -## 3.1 Switch class +Methods: + + 1. `__call__()` Call syntax e.g. `myswitch()` returns the logical debounced + state of the switch i.e. 0 if open, 1 if closed. + 2. `deinit()` No args. Cancels the polling task and clears bound `Event`s. + +Class variable: + 1. `debounce_ms=50` Debounce time in ms. + +Bound objects: + 1. `close` An `Event` instance. Set on contact closure. + 2. `open` An `Event` instance. Set on contact open. + +Application code is responsible for clearing the `Event` instances. +Usage example: +```python +import asyncio +from machine import Pin +from primitives import ESwitch +es = ESwitch(Pin("Y1", Pin.IN, Pin.PULL_UP)) + +async def closure(): + while True: + es.close.clear() + await es.close.wait() + print("Closed") + +async def open(): + while True: + es.open.clear() + await es.open.wait() + print("Open") + +async def main(): + asyncio.create_task(open()) + await closure() # Run forever + +asyncio.run(main()) +``` + +## 3.2 Switch class + +```python +from primitives import Switch # switch.py +``` +This can run callbacks or schedule coros on contact closure and/or opening. As +an alternative to a callback based interface, bound `Event` objects may be +triggered on switch state changes. This assumes a normally open switch connected between a pin and ground. The pin should be initialised as an input with a pullup. A `callable` may be specified @@ -107,21 +251,21 @@ Constructor argument (mandatory): Methods: - 1. `close_func` Args: `func` (mandatory) a `callable` to run on contact - closure. `args` a tuple of arguments for the `callable` (default `()`) - 2. `open_func` Args: `func` (mandatory) a `callable` to run on contact open. - `args` a tuple of arguments for the `callable` (default `()`) - 3. `__call__` Call syntax e.g. `myswitch()` returns the physical debounced + 1. `close_func(func, args=())` Args: `func` a `callable` to run on contact + closure, `args` a tuple of arguments for the `callable`. + 2. `open_func(func, args=())` Args: `func` a `callable` to run on contact open, + `args` a tuple of arguments for the `callable`. + 3. `__call__()` Call syntax e.g. `myswitch()` returns the physical debounced state of the switch i.e. 0 if grounded, 1 if connected to `3V3`. - 4. `deinit` No args. Cancels the running task. + 4. `deinit()` No args. Cancels the running task. Class attribute: - 1. `debounce_ms` Debounce time in ms. Default 50. + 1. `debounce_ms=50` Debounce time in ms. ```python from pyb import LED from machine import Pin -import uasyncio as asyncio +import asyncio from primitives import Switch async def pulse(led, ms): @@ -139,43 +283,98 @@ async def my_app(): asyncio.run(my_app()) # Run main application code ``` -## 3.2 Event interface +#### Event interface This enables a task to wait on a switch state as represented by a bound `Event` instance. A bound contact closure `Event` is created by passing `None` to `.close_func`, in which case the `Event` is named `.close`. Likewise a `.open` `Event` is created by passing `None` to `open_func`. -This is discussed further in -[Event based interface](./DRIVERS.md#8-event-based-interface) which includes a -code example. This API and the simpler [ESwitch class](./EVENTS.md#61-eswitch) -is recommended for new projects. - -###### [Contents](./DRIVERS.md#1-contents) +###### [Contents](./DRIVERS.md#0-contents) # 4. Interfacing pushbuttons -The `primitives.pushbutton` module provides the `Pushbutton` class for use with -simple mechanical, spring-loaded push buttons. This class is a generalisation -of the `Switch` class. `Pushbutton` supports open or normally closed buttons -connected to ground or 3V3. To a human, pushing a button is seen as a single -event, but the micro-controller sees voltage changes corresponding to two -events: press and release. A long button press adds the component of time and a -double-click appears as four voltage changes. The asynchronous `Pushbutton` -class provides the logic required to handle these user interactions by -monitoring these events over time. +The `primitives` module provides the following classes for interfacing +pushbuttons. The following support normally open or normally closed buttons +connected to gnd or to 3V3: +* `EButton` Provides an `Event` based interface. +* `Pushbutton` Offers `Event`s and/or callbacks. +The following support normally open pushbuttons connected in a crosspoint array. +* `Keyboard` An asynchronous iterator responding to button presses. +* `SwArray` As above, but also supporting open, double and long events. +The latter can also support switches in a diode-isolated array. -Instances of this class can run a `callable` on press, release, double-click or -long press events. +## 4.1 EButton class -As an alternative to callbacks bound `Event` instances may be created which are -triggered by press, release, double-click or long press events. This mode of -operation is more flexible than the use of callbacks and is covered in -[Event based interface](./DRIVERS.md#8-event-based-interface). To use an -`Event` based interface exclusively see the simpler -[EButton class](./EVENTS.md#62-ebutton). +```python +from primitives import EButton # events.py +``` -## 4.1 Pushbutton class +This extends the functionality of `ESwitch` to provide additional events for +long and double presses. + +This can support normally open or normally closed switches, connected to `gnd` +(with a pullup) or to `3V3` (with a pull-down). The `Pin` object should be +initialised appropriately. The default state of the switch can be passed in the +optional "sense" parameter on the constructor, otherwise the assumption is that +on instantiation the button is not pressed. + +The EButton class uses logical rather than physical state: a button's state +is considered `True` if pressed, otherwise `False` regardless of its physical +implementation. + +Constructor arguments: + + 1. `pin` Mandatory. The initialised Pin instance. + 2. `suppress=False`. See [Suppress mode](./DRIVERS.md#46-suppress-mode). + 3. `sense=None`. Optionally define the electrical connection: see + [section 4.2.1](./EVENTS.md#421-the-sense-constructor-argument). + +Methods: + + 1. `__call__()` Call syntax e.g. `mybutton()` Returns the logical debounced + state of the button (`True` corresponds to pressed). + 2. `rawstate()` Returns the logical instantaneous state of the button. There + is probably no reason to use this. + 3. `deinit()` No args. Cancels the running task and clears all events. + +Bound `Event`s: + + 1. `press` Set on button press. + 2. `release` Set on button release. + 3. `long` Set if button press is longer than `EButton.long_press_ms`. + 4. `double` Set if two button preses occur within `EButton.double_click_ms`. + +Application code is responsible for clearing any `Event`s that are used. + +Class attributes: + 1. `debounce_ms=50` Debounce time in ms. Default 50. + 2. `long_press_ms=1000` Threshold time in ms for a long press. + 3. `double_click_ms=400` Threshold time in ms for a double-click. + +### 4.1.1 The sense constructor argument + +In most applications it can be assumed that, at power-up, pushbuttons are not +pressed. The default `None` value uses this assumption to read the pin state +and to assign the result to the `False` (not pressed) state at power up. This +works with normally open or normally closed buttons wired to either supply +rail; this without programmer intervention. + +In certain use cases this assumption does not hold, and `sense` must explicitly +be specified. This defines the logical state of the un-pressed button. Hence +`sense=0` defines a button connected in such a way that when it is not pressed, +the voltage on the pin is gnd. + +Whenever the pin value changes, the new value is compared with `sense` to +determine whether the button is closed or open. + +###### [Contents](./DRIVERS.md#0-contents) + +## 4.2 Pushbutton class + +```py +from primitives import Pushbutton # pushbutton.py +``` This can support normally open or normally closed switches, connected to `gnd` (with a pullup) or to `3V3` (with a pull-down). The `Pin` object should be @@ -196,30 +395,29 @@ Please see the note on timing in [section 3](./DRIVERS.md#3-interfacing-switches Constructor arguments: 1. `pin` Mandatory. The initialised Pin instance. - 2. `suppress` Default `False`. See - [section 4.1.1](./DRIVERS.md#411-the-suppress-constructor-argument). + 2. `suppress` Default `False`. See [Suppress mode](./DRIVERS.md#46-suppress-mode). 3. `sense` Default `None`. Option to define electrical connection. See - [section 4.1.2](./DRIVERS.md#412-the-sense-constructor-argument). + [section 4.2.1](./DRIVERS.md#421-the-sense-constructor-argument). Methods: - 1. `press_func` Args: `func=False` a `callable` to run on button push, - `args=()` a tuple of arguments for the `callable`. - 2. `release_func` Args: `func=False` a `callable` to run on button release, - `args=()` a tuple of arguments for the `callable`. - 3. `long_func` Args: `func=False` a `callable` to run on long button push, - `args=()` a tuple of arguments for the `callable`. - 4. `double_func` Args: `func=False` a `callable` to run on double push, - `args=()` a tuple of arguments for the `callable`. - 5. `__call__` Call syntax e.g. `mybutton()` Returns the logical debounced + 1. `press_func(func=False, args=())` Args: `func` a `callable` to run on button + push, `args` a tuple of arguments for the `callable`. + 2. `release_func(func=False, args=())` Args: `func` a `callable` to run on + button release, `args` a tuple of arguments for the `callable`. + 3. `long_func(func=False, args=())` Args: `func` a `callable` to run on long + button push, `args` a tuple of arguments for the `callable`. + 4. `double_func(func=False, args=())` Args: `func` a `callable` to run on + double button push, `args` a tuple of arguments for the `callable`. + 5. `__call__()` Call syntax e.g. `mybutton()` Returns the logical debounced state of the button (`True` corresponds to pressed). 6. `rawstate()` Returns the logical instantaneous state of the button. There is probably no reason to use this. - 7. `deinit` No args. Cancels the running task. + 7. `deinit()` No args. Cancels the running debounce task. Methods 1 - 4 may be called at any time. If `False` is passed for a callable, any existing callback will be disabled. If `None` is passed, a bound `Event` is -created. See [Event based interface](./DRIVERS.md#8-event-based-interface). +created. See below for `Event` names. Class attributes: 1. `debounce_ms` Debounce time in ms. Default 50. @@ -230,7 +428,7 @@ A simple Pyboard demo: ```python from pyb import LED from machine import Pin -import uasyncio as asyncio +import asyncio from primitives import Pushbutton def toggle(led): @@ -246,52 +444,12 @@ async def my_app(): asyncio.run(my_app()) # Run main application code ``` -A `Pushbutton` subset is available -[here](https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py): -this implementation avoids the use of the `Delay_ms` class to minimise the -number of coroutines. - -### 4.1.1 The suppress constructor argument - -The purpose of the `suppress` argument is to disambiguate the response when an -application requires either, or both, long-press and double-click events. It -works by modifying the behavior of the `release_func`. By design, whenever a -button is pressed, the `press_func` runs immediately. This minimal latency is -ideal for applications such as games. The `Pushbutton` class provides the -ability to suppress 'intermediate' events and reduce them down to one single -event. The `suppress` argument is useful for applications where long-press, -single-press, and double-click events are desired, such as clocks, watches, or -menu navigation. However, long-press and double-click detection introduces -additional latency to ensure correct classification of events and is therefore -not suitable for all applications. To illustrate the default library behavior, -consider how long button presses and double-clicks are interpreted. - -A long press is seen as three events: - - * `press_func` - * `long_func` - * `release_func` - -Similarly, a double-click is seen as five events: - - * `press_func` - * `release_func` - * `press_func` - * `release_func` - * `double_func` - -There can be a need for a callable which runs if a button is pressed, but only -if a double-click or long-press function does not run. The suppress argument -changes the behaviour of the `release_func` to fill that role. This has timing -implications. The soonest that the absence of a long press can be detected is -on button release. Absence of a double-click can only be detected when the -double-click timer times out without a second press occurring. +### 4.2.1 The suppress constructor argument +See [Suppress mode](./DRIVERS.md#46-suppress-mode) for the purpose of this arg. Note: `suppress` affects the behaviour of the `release_func` only. Other -callbacks including `press_func` behave normally. - -If the `suppress = True` constructor argument is set, the `release_func` will -be launched as follows: +callbacks including `press_func` behave normally. If the `suppress = True` +constructor argument is set, the `release_func` will be launched as follows: * If `double_func` does not exist on rapid button release. * If `double_func` exists, after the expiration of the double-click timer. @@ -306,28 +464,7 @@ the case of a single short press, the `release_func` will be delayed until the expiry of the double-click timer (because until that time a second click might occur). -The following script may be used to demonstrate the effect of this argument. As -written, it assumes a Pi Pico with a push button attached between GPIO 18 and -Gnd, with the primitives installed. -```python -from machine import Pin -import uasyncio as asyncio -from primitives import Pushbutton - -btn = Pin(18, Pin.IN, Pin.PULL_UP) # Adapt for your hardware -pb = Pushbutton(btn, suppress=True) - -async def main(): - short_press = pb.release_func(print, ("SHORT",)) - double_press = pb.double_func(print, ("DOUBLE",)) - long_press = pb.long_func(print, ("LONG",)) - while True: - await asyncio.sleep(1) - -asyncio.run(main()) -``` - -### 4.1.2 The sense constructor argument +### 4.2.2 The sense constructor argument In most applications it can be assumed that, at power-up, pushbuttons are not pressed. The default `None` value uses this assumption to assign the `False` @@ -344,7 +481,23 @@ When the pin value changes, the new value is compared with `sense` to determine if the button is closed or open. This is to allow the designer to specify if the `closed` state of the button is active `high` or active `low`. -## 4.2 ESP32Touch class +#### Event interface + +Event names, where `None` is passed to a method listed below, are as follows: +| method | Event | +|:-------------|:--------| +| press_func | press | +| release_func | release | +| long_func | long | +| double_func | double | + +###### [Contents](./DRIVERS.md#0-contents) + +## 4.3 ESP32Touch class + +```py +from primitives import ESP32Touch # pushbutton.py +``` This subclass of `Pushbutton` supports ESP32 touchpads providing a callback based interface. See the @@ -365,7 +518,7 @@ threshold is currently 80% but this is subject to change. Example usage: ```python from machine import Pin -import uasyncio as asyncio +import asyncio from primitives import ESP32Touch ESP32Touch.threshold(70) # optional @@ -389,7 +542,215 @@ The best threshold value depends on physical design. Directly touching a large pad will result in a low value from `machine.TouchPad.read()`. A small pad covered with an insulating film will yield a smaller change. -###### [Contents](./DRIVERS.md#1-contents) +###### [Contents](./DRIVERS.md#0-contents) + +## 4.4 Keyboard class + +```python +from primitives import Keyboard # sw_array.py +``` +A `Keyboard` provides an interface to a set of pushbuttons arranged as a +crosspoint array. If a key is pressed its array index (scan code) is placed on a +queue. Keypresses are retrieved with `async for`. The driver operates by +polling each row, reading the response of each column. 1-key rollover is +supported - this is the case where a key is pressed before the prior key has +been released. + +Constructor mandatory args: + * `rowpins` A list or tuple of initialised open drain output pins. + * `colpins` A list or tuple of initialised input pins (pulled up). + +Constructor optional keyword only args: + * `bufsize=10)` Size of keyboard buffer. + * `db_delay=50` Debounce delay in ms. + + Methods: + * `deinit(self)` Cancels the running task. + * `__getitem__(self, scan_code)` Returns a `bool` being the instantaneous + debounced state of a given pin. Enables code that causes actions after a button + press, for example on release or auto-repeat while pressed. + +The `Keyboard` class is subclassed from [Ringbuf Queue](./DRIVERS.md#7-ringbuf-queue) +enabling scan codes to be retrieved with an asynchronous iterator. +Example usage: +```python +import asyncio +from primitives import Keyboard +from machine import Pin +rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] +colpins = [Pin(p, Pin.IN, Pin.PULL_UP) for p in range(16, 20)] + +async def main(): + kp = Keyboard(rowpins, colpins) + async for scan_code in kp: + print(scan_code) + if not scan_code: + break # Quit on key with code 0 + +asyncio.run(main()) +``` +In typical use the scan code would be used as the index into a string of +keyboard characters ordered to match the physical layout of the keys. If data +is not removed from the buffer, on overflow the oldest scan code is discarded. +There is no limit on the number of rows or columns however if more than 256 keys +are used, the `bufsize` arg would need to be adapted to handle scan codes > 255. +In this case an `array` or `list` object would be passed. + +Usage example. Keypresses on a numeric keypad are sent to a UART with auto +repeat. Optionally link GPIO0 and GPIO1 to view the result. +```python +import asyncio +from primitives import Keyboard +from machine import Pin, UART +cmap = b"123456789*0#" # Numeric keypad character map + +async def repeat(kpad, scan_code, uart): # Send at least one char + ch = cmap[scan_code : scan_code + 1] # Get character + uart.write(ch) + await asyncio.sleep_ms(400) # Longer initial delay + while kpad[scan_code]: # While key is pressed + uart.write(ch) + await asyncio.sleep_ms(150) # Faster repeat + +async def receiver(uart): + sreader = asyncio.StreamReader(uart) + while True: + res = await sreader.readexactly(1) + print('Received', res) + +async def main(): # Run forever + rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] + colpins = [Pin(p, Pin.IN, Pin.PULL_UP) for p in range(16, 20)] + uart = UART(0, 9600, tx=0, rx=1) + asyncio.create_task(receiver(uart)) + kpad = Keyboard(rowpins, colpins) + async for scan_code in kpad: + rpt = asyncio.create_task(repeat(kpad, scan_code, uart)) + +asyncio.run(main()) +``` + +###### [Contents](./DRIVERS.md#0-contents) + +## 4.5 SwArray class + +```python +from primitives import SwArray # sw_array.py +``` +An `SwArray` is similar to a `Keyboard` except that single, double and long +presses are supported. Items in the array may be switches or pushbuttons, +however if switches are used they must be diode-isolated. For the reason see +[Switches](./DRIVERS.md#12-switches). + +Constructor mandatory args: + * `rowpins` A list or tuple of initialised open drain output pins. + * `colpins` A list or tuple of initialised input pins (pulled up). + * `cfg` An integer defining conditions requiring a response. See Module + Constants below. + +Constructor optional keyword only args: + * `bufsize=10` Size of buffer. + + Methods: + * `deinit(self)` Cancels the running task. + * `__getitem__(self, scan_code)` Returns a `bool` being the instantaneous + debounced state of a given pin. Enables code that causes actions after a button + press. For example after a press a pin might periodically be polled to achieve + auto-repeat until released. + + Synchronous bound method: + * `keymap()` Return an integer representing a bitmap of the debounced state of + all switches in the array. 1 == closed. + + Class variables: + * `debounce_ms = 50` Assumed maximum duration of contact bounce. + * `long_press_ms = 1000` Threshold for long press detection. + * `double_click_ms = 400` Threshold for double-click detection. + +Module constants. +The folowing constants are provided to simplify defining the `cfg` constructor +arg. This may be defined as a bitwise or of selected constants. For example if +the `CLOSE` bit is specified, switch closures will be reported. An omitted event +will be ignored. Where the array comprises switches it is usual to specify only +`CLOSE` and/or `OPEN`. This invokes a more efficient mode of operation because +timing is not required. + * `CLOSE` Report contact closure. + * `OPEN` Contact opening. + * `LONG` Contact closure longer than `long_press_ms`. + * `DOUBLE` Two closures in less than `double_click_ms`. + * `SUPPRESS` Disambiguate. For explanation see `EButton`. + +The `SwArray` class is subclassed from [Ringbuf Queue](./DRIVERS.md#7-ringbuf-queue). +This is an asynchronous iterator, enabling scan codes and event types to be +retrieved as state changes occur with `async for`: +```python +import asyncio +from primitives.sw_array import SwArray, CLOSE, OPEN, LONG, DOUBLE, SUPPRESS +from machine import Pin +rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] +colpins = [Pin(p, Pin.IN, Pin.PULL_UP) for p in range(16, 20)] +cfg = CLOSE | OPEN #LONG | DOUBLE | SUPPRESS + +async def main(): + swa = SwArray(rowpins, colpins, cfg) + async for scan_code, evt in swa: + print(scan_code, evt) + if not scan_code: + break # Quit on key with code 0 + +asyncio.run(main()) +``` +###### [Contents](./DRIVERS.md#0-contents) + +## 4.6 Suppress mode + +The pushbutton drivers support a mode known as `suppress`. This option reduces +the number of events (or callbacks) that occur in the case of a double click. +Consider a button double-click. By default with `suppress=False` the following +events will occur in order: + + * `press` + * `release` + * `press` + * `release` + * `double` + +Similarly a long press will trigger `press`, `long` and `release` in that +order. Some applications may require only one event to be triggered. Setting +`suppress=True` ensures this. Outcomes are as follows: + +| Occurrence | Events set | Time of primary event | +|:-------------|:----------------|:-----------------------------| +| Short press | press, release | After `.double_click_ms` | +| Double press | double, release | When the second press occurs | +| Long press | long, release | After `long_press_ms` | + +The tradeoff is that the `press` and `release` events are delayed: the soonest +it is possible to detect the lack of a double click is `.double_click_ms`ms +after a short button press. Hence in the case of a short press when `suppress` +is `True`, `press` and `release` events are set on expiration of the double +click timer. + +The following script may be used to demonstrate the effect of `suppress`. As +written, it assumes a Pi Pico with a push button attached between GPIO 18 and +Gnd, with the primitives installed. +```python +from machine import Pin +import asyncio +from primitives import Pushbutton + +btn = Pin(18, Pin.IN, Pin.PULL_UP) # Adapt for your hardware + +async def main(): + pb = Pushbutton(btn, suppress=True) + pb.release_func(print, ("SHORT",)) + pb.double_func(print, ("DOUBLE",)) + pb.long_func(print, ("LONG",)) + await asyncio.sleep(60) # Run for one minute + +asyncio.run(main()) +``` +###### [Contents](./DRIVERS.md#0-contents) # 5. ADC monitoring @@ -400,7 +761,7 @@ value. Data from ADC's is usually noisy. Relative bounds provide a simple (if crude) means of eliminating this. Absolute bounds can be used to raise an alarm or log data, if the value goes out of range. Typical usage: ```python -import uasyncio as asyncio +import asyncio from machine import ADC import pyb from primitives import AADC @@ -416,6 +777,10 @@ asyncio.run(foo()) ## 5.1 AADC class +```py +from primitives import AADC # aadc.py +``` + `AADC` instances are awaitable. This is the principal mode of use. Constructor argument: @@ -441,7 +806,7 @@ In the sample below the coroutine pauses until the ADC is in range, then pauses until it goes out of range. ```python -import uasyncio as asyncio +import asyncio from machine import ADC from primitives import AADC @@ -464,21 +829,21 @@ obvious design. It was chosen because the plan for `uasyncio` is that it will include an option for prioritising I/O. I wanted this class to be able to use this for applications requiring rapid response. -###### [Contents](./DRIVERS.md#1-contents) +###### [Contents](./DRIVERS.md#0-contents) # 6. Quadrature encoders The [Encoder](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/encoder.py) class is an asynchronous driver for control knobs based on quadrature encoder -switches such as -[this Adafruit product](https://www.adafruit.com/product/377). The driver is -not intended for applications such as CNC machines where -[a solution such as this one](https://github.com/peterhinch/micropython-samples#47-rotary-incremental-encoder) -is required. Drivers for NC machines must never miss an edge. Contact bounce or -vibration induced jitter can cause transitions to occur at a high rate; these -must be tracked. Consequently callbacks occur in an interrupt context with the -associated concurrency issues. These issues, along with general discussion of -MicroPython encoder drivers, are covered +switches such as [this Adafruit product](https://www.adafruit.com/product/377). +The driver is not intended for applications such as CNC machines. Drivers for NC +machines must never miss an edge. Contact bounce or vibration induced jitter can +cause transitions to occur at a high rate; these must be tracked which +challenges software based solutions. + +Another issue affecting some solutions is that callbacks occur in an interrupt +context. This can lead to concurrency issues. These issues, along with general +discussion of MicroPython encoder drivers, are covered [in this doc](https://github.com/peterhinch/micropython-samples/blob/master/encoders/ENCODERS.md). This driver runs the user supplied callback in an `asyncio` context, so that @@ -505,6 +870,10 @@ value since the previous time the callback ran. ## 6.1 Encoder class +```python +from primitives import Encoder # encoder.py +``` + Existing users: the `delay` parameter is now a constructor arg rather than a class varaiable. @@ -548,7 +917,7 @@ An `Encoder` instance is an asynchronous iterator. This enables it to be used as follows, with successive values being retrieved with `async for`: ```python from machine import Pin -import uasyncio as asyncio +import asyncio from primitives import Encoder async def main(): @@ -566,11 +935,77 @@ finally: See [this doc](https://github.com/peterhinch/micropython-samples/blob/master/encoders/ENCODERS.md) for further information on encoders and their limitations. -###### [Contents](./DRIVERS.md#1-contents) +###### [Contents](./DRIVERS.md#0-contents) + +# 7. Ringbuf Queue + +```python +from primitives import RingbufQueue # ringbuf_queue.py +``` + +The API of the `Queue` aims for CPython compatibility. This is at some cost to +efficiency. As the name suggests, the `RingbufQueue` class uses a pre-allocated +circular buffer which may be of any mutable type supporting the buffer protocol +e.g. `list`, `array` or `bytearray`. + +Attributes of `RingbufQueue`: + 1. It is of fixed size, `Queue` can grow to arbitrary size. + 2. It uses pre-allocated buffers of various types (`Queue` uses a `list`). + 3. It is an asynchronous iterator allowing retrieval with `async for`. + 4. It has an "overwrite oldest data" synchronous write mode. + +Constructor mandatory arg: + * `buf` Buffer for the queue, e.g. list, bytearray or array. If an integer is + passed, a list of this size is created. A buffer of size `N` can hold a + maximum of `N-1` items. Note that, where items on the queue are suitably + limited, bytearrays or arrays are more efficient than lists. + +Synchronous methods (immediate return): + * `qsize` No arg. Returns the number of items in the queue. + * `empty` No arg. Returns `True` if the queue is empty. + * `full` No arg. Returns `True` if the queue is full. + * `get_nowait` No arg. Returns an object from the queue. Raises `IndexError` + if the queue is empty. + * `put_nowait` Arg: the object to put on the queue. Raises `IndexError` if the + queue is full. If the calling code ignores the exception the oldest item in + the queue will be overwritten. In some applications this can be of use. + * `peek` No arg. Returns oldest entry without removing it from the queue. This + is a superset of the CPython compatible methods. + +Asynchronous methods: + * `put` Arg: the object to put on the queue. If the queue is full, it will + block until space is available. + * `get` Return an object from the queue. If empty, block until an item is + available. + +Retrieving items from the queue: + +The `RingbufQueue` is an asynchronous iterator. Results are retrieved using +`async for`: +```python +async def handle_queued_data(q): + async for obj in q: + await asyncio.sleep(0) # See below + # Process obj +``` +The `sleep` is necessary if you have multiple tasks waiting on the queue, +otherwise one task hogs all the data. + +The following illustrates putting items onto a `RingbufQueue` where the queue is +not allowed to stall: where it becomes full, new items overwrite the oldest ones +in the queue: +```python +def add_item(q, data): + try: + q.put_nowait(data) + except IndexError: + pass +``` +###### [Contents](./DRIVERS.md#0-contents) -# 7. Additional functions +# 8. Additional functions -## 7.1 Launch +## 8.1 Launch Import as follows: ```python @@ -582,7 +1017,7 @@ runs it and returns the callback's return value. If a coro is passed, it is converted to a `task` and run asynchronously. The return value is the `task` instance. A usage example is in `primitives/switch.py`. -## 7.2 set_global_exception +## 8.2 set_global_exception Import as follows: ```python @@ -593,7 +1028,7 @@ handler to simplify debugging. The function takes no args. It is called as follows: ```python -import uasyncio as asyncio +import asyncio from primitives import set_global_exception async def main(): @@ -611,9 +1046,9 @@ continue to run. This means that the failure can be missed and the sequence of events can be hard to deduce. A global handler ensures that the entire application stops allowing the traceback and other debug prints to be studied. -###### [Contents](./DRIVERS.md#1-contents) +###### [Contents](./DRIVERS.md#0-contents) -# 8. Event based interface +# 9. Event based interface The `Switch` and `Pushbutton` classes offer a traditional callback-based interface. While familiar, it has drawbacks and requires extra code to perform @@ -642,7 +1077,7 @@ to a method: Typical usage is as follows: ```python -import uasyncio as asyncio +import asyncio from primitives import Switch from pyb import Pin @@ -665,4 +1100,4 @@ replicated, but with added benefits. For example the omitted code in `foo` could run a callback-style synchronous method, retrieving its value. Alternatively the code could create a task which could be cancelled. -###### [Contents](./DRIVERS.md#1-contents) +###### [Contents](./DRIVERS.md#0-contents) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 952d32f..fd502f3 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -165,7 +165,7 @@ The last ELO to trigger a `WaitAny` instance may also be retrieved by issuing the instance's `.event()` method. ```python from primitives import WaitAny -async def foo(elo1, elo2) +async def foo(elo1, elo2): evt = await WaitAny((elo1, elo2)).wait() if evt is elo1: # Handle elo1 @@ -457,7 +457,7 @@ order. Some applications may require only a single `Event` to be triggered. Setting `suppress=True` ensures this. Outcomes are as follows: -| Occurence | Events set | Time of pimary event | +| Occurrence | Events set | Time of primary event | |:-------------|:----------------|:-----------------------------| | Short press | press, release | After `.double_click_ms` | | Double press | double, release | When the second press occurs | diff --git a/v3/docs/isolate.png b/v3/docs/images/isolate.png similarity index 100% rename from v3/docs/isolate.png rename to v3/docs/images/isolate.png diff --git a/v3/docs/images/keypad.png b/v3/docs/images/keypad.png new file mode 100644 index 0000000000000000000000000000000000000000..2c8fdbd9a4b7af2acb6703e790b129cea1274d84 GIT binary patch literal 14577 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 Lx@?WKdDwpgXiXc1 literal 0 HcmV?d00001 diff --git a/v3/primitives/sw_array.py b/v3/primitives/sw_array.py index 13fbf05..edad126 100644 --- a/v3/primitives/sw_array.py +++ b/v3/primitives/sw_array.py @@ -10,14 +10,14 @@ # A crosspoint array of pushbuttons # Tuples/lists of pins. Rows are OUT, cols are IN class Keyboard(RingbufQueue): - def __init__(self, rowpins, colpins, *, buffer=bytearray(10), db_delay=50): - super().__init__(buffer) + def __init__(self, rowpins, colpins, *, bufsize=10, db_delay=50): + super().__init__(bytearray(bufsize) if isinstance(bufsize, int) else bufsize) self.rowpins = rowpins self.colpins = colpins self._state = 0 # State of all keys as bitmap for opin in self.rowpins: # Initialise output pins opin(1) - asyncio.create_task(self.scan(len(rowpins) * len(colpins), db_delay)) + self._run = asyncio.create_task(self.scan(len(rowpins) * len(colpins), db_delay)) def __getitem__(self, scan_code): return bool(self._state & (1 << scan_code)) @@ -43,6 +43,10 @@ async def scan(self, nkeys, db_delay): self._state = cur await asyncio.sleep_ms(db_delay if changed else 0) # Wait out bounce + def deinit(self): + self._run.cancel() + + CLOSE = const(1) # cfg comprises the OR of these constants OPEN = const(2) LONG = const(4) @@ -56,6 +60,7 @@ class SwArray(RingbufQueue): debounce_ms = 50 # Attributes can be varied by user long_press_ms = 1000 double_click_ms = 400 + def __init__(self, rowpins, colpins, cfg, *, bufsize=10): super().__init__(bufsize) self._rowpins = rowpins @@ -67,7 +72,7 @@ def __init__(self, rowpins, colpins, cfg, *, bufsize=10): self._suppress = bool(cfg & SUPPRESS) for opin in self._rowpins: # Initialise output pins opin(1) # open circuit - asyncio.create_task(self._scan(len(rowpins) * len(colpins))) + self._run = asyncio.create_task(self._scan(len(rowpins) * len(colpins))) def __getitem__(self, scan_code): return bool(self._state & (1 << scan_code)) @@ -99,6 +104,7 @@ async def _finish(self, sc): # Tidy up. If necessary await a contact open def keymap(self): # Return a bitmap of debounced state of all buttons/switches return self._state + # Handle long, double. Switch has closed. async def _defer(self, sc): # Wait for contact closure to be registered: let calling loop complete @@ -136,7 +142,7 @@ async def _scan(self, nkeys): curb = cur # Copy current bitmap if changed := (cur ^ self._state): # 1's are newly canged button(s) for sc in range(nkeys): - if (changed & 1): # Current button has changed state + if changed & 1: # Current button has changed state if self._basic: # No timed behaviour self._put(sc, CLOSE if cur & 1 else OPEN) elif cur & 1: # Closed @@ -147,3 +153,6 @@ async def _scan(self, nkeys): changed = curb ^ self._state # Any new press or release self._state = curb await asyncio.sleep_ms(db_delay if changed else 0) # Wait out bounce + + def deinit(self): + self._run.cancel() From 3fa515fa465129f3ce58dee721586eb0a0f71662 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 23 Oct 2023 15:51:18 +0100 Subject: [PATCH 257/305] First pass at revised docs. --- v3/docs/DRIVERS.md | 256 +++++++++++++++++----------- v3/docs/EVENTS.md | 397 +------------------------------------------- v3/docs/TUTORIAL.md | 119 ++----------- 3 files changed, 183 insertions(+), 589 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 2e5338f..c53eead 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -10,25 +10,25 @@ MicroPython's `asyncio` when used in a microcontroller context. 3. [Interfacing switches](./DRIVERS.md#3-interfacing-switches) 3.1 [ESwitch class](./DRIVERS.md#31-eswitch-class) Switch debouncer with event interface. 3.2 [Switch class](./DRIVERS.md#32-switch-class) Switch debouncer with callbacks. - 4. [Interfacing pushbuttons](./DRIVERS.md#4-interfacing-pushbuttons) Extends Switch for long and double-click events - 4.1 [EButton class](./DRIVERS.md#41-ebutton-class) Pushbutton with Event-based interface. - 4.2 [Pushbutton class](./DRIVERS.md#42-pushbutton-class) -      4.2.1 [The suppress constructor argument](./DRIVERS.md#431-the-suppress-constructor-argument) -      4.2.2 [The sense constructor argument](./DRIVERS.md#432-the-sense-constructor-argument) + 4. [Interfacing pushbuttons](./DRIVERS.md#4-interfacing-pushbuttons) Access short, long and double-click events. + 4.1 [EButton class](./DRIVERS.md#41-ebutton-class) Debounced pushbutton with Event-based interface. + 4.2 [Pushbutton class](./DRIVERS.md#42-pushbutton-class) Debounced pushbutton with callback interface. +      4.2.1 [The suppress constructor argument](./DRIVERS.md#421-the-suppress-constructor-argument) +      4.2.2 [The sense constructor argument](./DRIVERS.md#422-the-sense-constructor-argument) 4.3 [ESP32Touch class](./DRIVERS.md#43-esp32touch-class) - 4.4 [keyboard class](./DRIVERS.md#44-keyboard-class) - 4.5 [SwArray class](./DRIVERS.md#45-swarray-class) + 4.4 [Keyboard class](./DRIVERS.md#44-keyboard-class) Retrieve characters from a keypad. + 4.5 [SwArray class](./DRIVERS.md#45-swarray-class) Interface a crosspoint array of switches or buttons. 4.6 [Suppress mode](./DRIVERS.md#46-suppress-mode) Reduce the number of events/callbacks. 5. [ADC monitoring](./DRIVERS.md#5-adc-monitoring) Pause until an ADC goes out of bounds 5.1 [AADC class](./DRIVERS.md#51-aadc-class) 5.2 [Design note](./DRIVERS.md#52-design-note) - 6. [Quadrature encoders](./DRIVERS.md#6-quadrature-encoders) + 6. [Quadrature encoders](./DRIVERS.md#6-quadrature-encoders) Asynchronous interface for rotary encoders. 6.1 [Encoder class](./DRIVERS.md#61-encoder-class) 7. [Ringbuf Queue](./DRIVERS.md#7-ringbuf-queue) A MicroPython optimised queue primitive. - 8. [Additional functions](./DRIVERS.md#8-additional-functions) - 8.1 [launch](./DRIVERS.md#81-launch) Run a coro or callback interchangeably - 8.2 [set_global_exception](./DRIVERS.md#82-set_global_exception) Simplify debugging with a global exception handler. - 9. [Event based interface](./DRIVERS.md#9-event-based-interface) An alternative interface to Switch and Pushbutton objects. + 8. [Delay_ms class](./DRIVERS.md#8-delay_ms class) A flexible retriggerable delay with callback or Event interface. + 9. [Additional functions](./DRIVERS.md#9-additional-functions) + 9.1 [launch](./DRIVERS.md#91-launch) Run a coro or callback interchangeably. + 9.2 [set_global_exception](./DRIVERS.md#92-set_global_exception) Simplify debugging with a global exception handler. ###### [Tutorial](./TUTORIAL.md#contents) @@ -43,15 +43,21 @@ to the existing CPython-compatible primitives. ## 1.1 API design -The traditional interface to asynchronous external events is a callback. When -the event occurs, the device driver runs a user-specified callback. Some classes -described here offer a callback interface; newer designs have abandoned this in -favour of asynchronous interfaces by exposing `Event` or asynchronous iterator -interfaces. Note that where callbacks are used the term `callable` implies a -Python `callable`: namely a function, bound method, coroutine or bound -coroutine. Any of these may be supplied as a callback function. +The traditional interface to asynchronous external events is via a callback. +When the event occurs, the device driver runs a user-specified callback. Some +classes described here offer a callback interface. Where callbacks are used the +term `callable` implies a Python `callable`: namely a function, bound method, +coroutine or bound coroutine. Any of these may be supplied as a callback +function. -Asynchronous interfaces allow the use of callbacks using patterns like the + +Newer class designs abandon callbacks in favour of asynchronous interfaces. This +is done by exposing `Event` or asynchronous iterator interfaces. It is arguable +that callbacks are outdated. Handling of arguments and return values is +inelegant and there are usually better ways using asynchronous coding. In +particular MicroPython's `asyncio` implements asynchronous interfaces in an +efficient manner. A task waiting on an `Event` consumes minimal resources. If a +user wishes to use a callback it may readily be achieved using patterns like the following. In this case the device is an asynchronous iterator: ```python async def run_callback(device, callback, *args): @@ -66,10 +72,6 @@ async def run_callback(device, callback, *args): device.clear() # Clear it down callback(*args) ``` -It is arguable that callbacks are outdated. Handling of arguments and return -values is messy and there are usually better ways using asynchronous coding. In -particular MicroPython's `asyncio` implements asynchronous interfaces in an -efficient manner. A task waiting on an `Event` consumes minimal resources. ## 1.2 Switches @@ -131,13 +133,14 @@ To prevent this it is wise to add physical resistors between the input pins and # 2. Installation and usage -The latest release build of firmware or a newer nightly build is recommended. +The latest release build of firmware or a newer preview build is recommended. To install the library, connect the target hardware to WiFi and issue: ```python import mip mip.install("github:peterhinch/micropython-async/v3/primitives") ``` -For any target including non-networked ones use `mpremote`: +For any target including non-networked ones use +[mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html): ```bash $ mpremote mip install "github:peterhinch/micropython-async/v3/primitives" ``` @@ -171,7 +174,7 @@ minimal driver providing an `Event` interface. The latter supports callbacks and ## 3.1 ESwitch class ```python -from primitives import ESwitch # evennts.py +from primitives import ESwitch # events.py ``` This provides a debounced interface to a switch connected to gnd or to 3V3. A pullup or pull down resistor should be supplied to ensure a valid logic level @@ -185,7 +188,7 @@ pin = Pin(pin_id, Pin.IN, Pin.PULL_UP) ``` Constructor arguments: - 1. `pin` The Pin instance: should be initialised as an input with a pullup or + 1. `pin` The `Pin` instance: should be initialised as an input with a pullup or down as appropriate. 2. `lopen=1` Electrical level when switch is open circuit i.e. 1 is 3.3V, 0 is gnd. @@ -298,7 +301,7 @@ The `primitives` module provides the following classes for interfacing pushbuttons. The following support normally open or normally closed buttons connected to gnd or to 3V3: * `EButton` Provides an `Event` based interface. -* `Pushbutton` Offers `Event`s and/or callbacks. +* `Pushbutton` Offers `Event`s and/or callbacks. The following support normally open pushbuttons connected in a crosspoint array. * `Keyboard` An asynchronous iterator responding to button presses. * `SwArray` As above, but also supporting open, double and long events. @@ -328,7 +331,7 @@ Constructor arguments: 1. `pin` Mandatory. The initialised Pin instance. 2. `suppress=False`. See [Suppress mode](./DRIVERS.md#46-suppress-mode). 3. `sense=None`. Optionally define the electrical connection: see - [section 4.2.1](./EVENTS.md#421-the-sense-constructor-argument). + [section 4.2.1](./DRIVERS.md#411-the-sense-constructor-argument). Methods: @@ -395,7 +398,8 @@ Please see the note on timing in [section 3](./DRIVERS.md#3-interfacing-switches Constructor arguments: 1. `pin` Mandatory. The initialised Pin instance. - 2. `suppress` Default `False`. See [Suppress mode](./DRIVERS.md#46-suppress-mode). + 2. `suppress` Default `False`. See + [section 4.2.2](./DRIVERS.md#422-the-suppress-constructor-argument). 3. `sense` Default `None`. Option to define electrical connection. See [section 4.2.1](./DRIVERS.md#421-the-sense-constructor-argument). @@ -619,7 +623,7 @@ async def receiver(uart): print('Received', res) async def main(): # Run forever - rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] + rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 13)] colpins = [Pin(p, Pin.IN, Pin.PULL_UP) for p in range(16, 20)] uart = UART(0, 9600, tx=0, rx=1) asyncio.create_task(receiver(uart)) @@ -635,12 +639,14 @@ asyncio.run(main()) ## 4.5 SwArray class ```python -from primitives import SwArray # sw_array.py +from primitives.sw_array import SwArray, CLOSE, OPEN, LONG, DOUBLE, SUPPRESS ``` An `SwArray` is similar to a `Keyboard` except that single, double and long presses are supported. Items in the array may be switches or pushbuttons, however if switches are used they must be diode-isolated. For the reason see -[Switches](./DRIVERS.md#12-switches). +[Switches](./DRIVERS.md#12-switches). It is an asynchronous iterator with events +being retrieved with `async for`: this returns a pair of integers being the scan +code and a bit representing the event which occurred. Constructor mandatory args: * `rowpins` A list or tuple of initialised open drain output pins. @@ -668,8 +674,8 @@ Constructor optional keyword only args: * `double_click_ms = 400` Threshold for double-click detection. Module constants. -The folowing constants are provided to simplify defining the `cfg` constructor -arg. This may be defined as a bitwise or of selected constants. For example if +The following constants are provided to simplify defining the `cfg` constructor +arg. This may be defined as a bitwise `or` of selected constants. For example if the `CLOSE` bit is specified, switch closures will be reported. An omitted event will be ignored. Where the array comprises switches it is usual to specify only `CLOSE` and/or `OPEN`. This invokes a more efficient mode of operation because @@ -678,11 +684,17 @@ timing is not required. * `OPEN` Contact opening. * `LONG` Contact closure longer than `long_press_ms`. * `DOUBLE` Two closures in less than `double_click_ms`. - * `SUPPRESS` Disambiguate. For explanation see `EButton`. + * `SUPPRESS` Disambiguate. For explanation see + [Suppress mode](./DRIVERS.md#46-suppress-mode). If all the above bits are set, + a double click will result in `DOUBLE` and `OPEN` responses. If the `OPEN` bit + were clear, only `DOUBLE` would occur. The `SwArray` class is subclassed from [Ringbuf Queue](./DRIVERS.md#7-ringbuf-queue). This is an asynchronous iterator, enabling scan codes and event types to be -retrieved as state changes occur with `async for`: +retrieved as state changes occur. The event type is a single bit corresponding +to the above constants. + +Usage example: ```python import asyncio from primitives.sw_array import SwArray, CLOSE, OPEN, LONG, DOUBLE, SUPPRESS @@ -948,6 +960,9 @@ efficiency. As the name suggests, the `RingbufQueue` class uses a pre-allocated circular buffer which may be of any mutable type supporting the buffer protocol e.g. `list`, `array` or `bytearray`. +It should be noted that `Queue`, `RingbufQueue` (and CPython's `Queue`) are not +thread safe. See [Threading](./THREADING.md). + Attributes of `RingbufQueue`: 1. It is of fixed size, `Queue` can grow to arbitrary size. 2. It uses pre-allocated buffers of various types (`Queue` uses a `list`). @@ -1003,9 +1018,114 @@ def add_item(q, data): ``` ###### [Contents](./DRIVERS.md#0-contents) -# 8. Additional functions +## 3.8 Delay_ms class + +This implements the software equivalent of a retriggerable monostable or a +watchdog timer. It has an internal boolean `running` state. When instantiated +the `Delay_ms` instance does nothing, with `running` `False` until triggered. +Then `running` becomes `True` and a timer is initiated. This can be prevented +from timing out by triggering it again (with a new timeout duration). So long +as it is triggered before the time specified in the preceding trigger it will +never time out. + +If it does time out the `running` state will revert to `False`. This can be +interrogated by the object's `running()` method. In addition a `callable` can +be specified to the constructor. A `callable` can be a callback or a coroutine. +A callback will execute when a timeout occurs; where the `callable` is a +coroutine it will be converted to a `Task` and run asynchronously. + +Constructor arguments (defaults in brackets): + + 1. `func` The `callable` to call on timeout (default `None`). + 2. `args` A tuple of arguments for the `callable` (default `()`). + 3. `can_alloc` Unused arg, retained to avoid breaking code. + 4. `duration` Integer, default 1000 ms. The default timer period where no value + is passed to the `trigger` method. + +Synchronous methods: + + 1. `trigger` optional argument `duration=0`. A timeout will occur after + `duration` ms unless retriggered. If no arg is passed the period will be that + of the `duration` passed to the constructor. The method can be called from a + hard or soft ISR. It is now valid for `duration` to be less than the current + time outstanding. + 2. `stop` No argument. Cancels the timeout, setting the `running` status + `False`. The timer can be restarted by issuing `trigger` again. Also clears + the `Event` described in `wait` below. + 3. `running` No argument. Returns the running status of the object. + 4. `__call__` Alias for running. + 5. `rvalue` No argument. If a timeout has occurred and a callback has run, + returns the return value of the callback. If a coroutine was passed, returns + the `Task` instance. This allows the `Task` to be cancelled or awaited. + 6. `callback` args `func=None`, `args=()`. Allows the callable and its args to + be assigned, reassigned or disabled at run time. + 7. `deinit` No args. Cancels the running task. See [Object scope](./TUTORIAL.md#44-object-scope). + 8. `clear` No args. Clears the `Event` described in `wait` below. + 9. `set` No args. Sets the `Event` described in `wait` below. + +Asynchronous method: + 1. `wait` One or more tasks may wait on a `Delay_ms` instance. Pause until the + delay instance has timed out. + +In this example a `Delay_ms` instance is created with the default duration of +1 sec. It is repeatedly triggered for 5 secs, preventing the callback from +running. One second after the triggering ceases, the callback runs. + +```python +import asyncio +from primitives import Delay_ms + +async def my_app(): + d = Delay_ms(callback, ('Callback running',)) + print('Holding off callback') + for _ in range(10): # Hold off for 5 secs + await asyncio.sleep_ms(500) + d.trigger() + print('Callback will run in 1s') + await asyncio.sleep(2) + print('Done') + +def callback(v): + print(v) + +try: + asyncio.run(my_app()) +finally: + asyncio.new_event_loop() # Clear retained state +``` +This example illustrates multiple tasks waiting on a `Delay_ms`. No callback is +used. +```python +import asyncio +from primitives import Delay_ms + +async def foo(n, d): + await d.wait() + d.clear() # Task waiting on the Event must clear it + print('Done in foo no.', n) + +async def my_app(): + d = Delay_ms() + tasks = [None] * 4 # For CPython compaibility must store a reference see Note + for n in range(4): + tasks[n] = asyncio.create_task(foo(n, d)) + d.trigger(3000) + print('Waiting on d') + await d.wait() + print('Done in my_app.') + await asyncio.sleep(1) + print('Test complete.') + +try: + asyncio.run(my_app()) +finally: + _ = asyncio.new_event_loop() # Clear retained state +``` +###### [Contents](./DRIVERS.md#0-contents) + +# 9. Additional functions -## 8.1 Launch +## 9.1 Launch Import as follows: ```python @@ -1017,7 +1137,7 @@ runs it and returns the callback's return value. If a coro is passed, it is converted to a `task` and run asynchronously. The return value is the `task` instance. A usage example is in `primitives/switch.py`. -## 8.2 set_global_exception +## 9.2 set_global_exception Import as follows: ```python @@ -1047,57 +1167,3 @@ events can be hard to deduce. A global handler ensures that the entire application stops allowing the traceback and other debug prints to be studied. ###### [Contents](./DRIVERS.md#0-contents) - -# 9. Event based interface - -The `Switch` and `Pushbutton` classes offer a traditional callback-based -interface. While familiar, it has drawbacks and requires extra code to perform -tasks like retrieving the result of a callback or, where a task is launched, -cancelling that task. The reason for this API is historical; an efficient -`Event` class only materialised with `uasyncio` V3. The class ensures that a -task waiting on an `Event` consumes minimal processor time. - -It is suggested that this API is used in new projects. - -The event based interface to `Switch` and `Pushbutton` classes is engaged by -passing `None` to the methods used to register callbacks. This causes a bound -`Event` to be instantiated, which may be accessed by user code. - -The following shows the name of the bound `Event` created when `None` is passed -to a method: - -| Class | method | Event | -|:-----------|:-------------|:--------| -| Switch | close_func | close | -| Switch | open_func | open | -| Pushbutton | press_func | press | -| Pushbutton | release_func | release | -| Pushbutton | long_func | long | -| Pushbutton | double_func | double | - -Typical usage is as follows: -```python -import asyncio -from primitives import Switch -from pyb import Pin - -async def foo(evt): - while True: - evt.clear() # re-enable the event - await evt.wait() # minimal resources used while paused - print("Switch closed.") - # Omitted code runs each time the switch closes - -async def main(): - sw = Switch(Pin("X1", Pin.IN, Pin.PULL_UP)) - sw.close_func(None) # Use event based interface - await foo(sw.close) # Pass the bound event to foo - -asyncio.run(main()) -``` -With appropriate code the behaviour of the callback based interface may be -replicated, but with added benefits. For example the omitted code in `foo` -could run a callback-style synchronous method, retrieving its value. -Alternatively the code could create a task which could be cancelled. - -###### [Contents](./DRIVERS.md#0-contents) diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index fd502f3..63337f4 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -23,10 +23,7 @@ This document assumes familiarity with `asyncio`. See [official docs](http://doc 6. [Drivers](./EVENTS.md#6-drivers) Minimal Event-based drivers 6.1 [ESwitch](./EVENTS.md#61-eswitch) Debounced switch 6.2 [EButton](./EVENTS.md#62-ebutton) Debounced pushbutton with double and long press events -      6.2.1 [The suppress constructor argument](./EVENTS.md#621-the-suppress-constructor-argument) -      6.2.2 [The sense constructor argument](./EVENTS.md#622-the-sense-constructor-argument) - 6.3 [Keyboard](./EVENTS.md#63-keyboard) A crosspoint array of pushbuttons. - 7. [Ringbuf queue](./EVENTS.md#7-ringbuf-queue) A MicroPython optimised queue primitive. + [Appendix 1 Polling](./EVENTS.md#100-appendix-1-polling) # 1. An alternative to callbacks in asyncio code @@ -324,400 +321,20 @@ async def foo(): # 6. Drivers -This document describes drivers for mechanical switches and pushbuttons. These -have event based interfaces exclusively and support debouncing. The drivers are -simplified alternatives for -[Switch](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/switch.py) -and [Pushbutton](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/pushbutton.py), -which also support callbacks. +The following device drivers provide an `Event` based interface for switches and +pushbuttons. ## 6.1 ESwitch -```python -from primitives import ESwitch -``` -This provides a debounced interface to a switch connected to gnd or to 3V3. A -pullup or pull down resistor should be supplied to ensure a valid logic level -when the switch is open. The default constructor arg `lopen=1` is for a switch -connected between the pin and gnd, with a pullup to 3V3. Typically the pullup -is internal, the pin being as follows: -```python -from machine import Pin -pin_id = 0 # Depends on hardware -pin = Pin(pin_id, Pin.IN, Pin.PULL_UP) -``` -Constructor arguments: - - 1. `pin` The Pin instance: should be initialised as an input with a pullup or - down as appropriate. - 2. `lopen=1` Electrical level when switch is open circuit i.e. 1 is 3.3V, 0 is - gnd. - -Methods: - - 1. `__call__` Call syntax e.g. `myswitch()` returns the logical debounced - state of the switch i.e. 0 if open, 1 if closed. - 2. `deinit` No args. Cancels the polling task and clears bound `Event`s. - -Bound objects: - 1. `debounce_ms` An `int`. Debounce time in ms. Default 50. - 2. `close` An `Event` instance. Set on contact closure. - 3. `open` An `Event` instance. Set on contact open. - -Application code is responsible for clearing the `Event` instances. -Usage example: -```python -import asyncio -from machine import Pin -from primitives import ESwitch -es = ESwitch(Pin("Y1", Pin.IN, Pin.PULL_UP)) - -async def closure(): - while True: - es.close.clear() - await es.close.wait() - print("Closed") - -async def open(): - while True: - es.open.clear() - await es.open.wait() - print("Open") - -async def main(): - asyncio.create_task(open()) - await closure() - -asyncio.run(main()) -``` - -###### [Contents](./EVENTS.md#0-contents) +This is now documented [here](./DRIVERS.md#31-eswitch-class). ## 6.2 EButton -```python -from primitives import EButton -``` - -This extends the functionality of `ESwitch` to provide additional events for -long and double presses. - -This can support normally open or normally closed switches, connected to `gnd` -(with a pullup) or to `3V3` (with a pull-down). The `Pin` object should be -initialised appropriately. The default state of the switch can be passed in the -optional "sense" parameter on the constructor, otherwise the assumption is that -on instantiation the button is not pressed. - -The Pushbutton class uses logical rather than physical state: a button's state -is considered `True` if pressed, otherwise `False` regardless of its physical -implementation. - -Constructor arguments: - - 1. `pin` Mandatory. The initialised Pin instance. - 2. `suppress` Default `False`. See [section 6.2.1](./EVENTS.md#621-the-suppress-constructor-argument). - 3. `sense` Default `None`. Optionally define the electrical connection: see - [section 6.2.2](./EVENTS.md#622-the-sense-constructor-argument) - -Methods: - - 1. `__call__` Call syntax e.g. `mybutton()` Returns the logical debounced - state of the button (`True` corresponds to pressed). - 2. `rawstate()` Returns the logical instantaneous state of the button. There - is probably no reason to use this. - 3. `deinit` No args. Cancels the running task and clears all events. - -Bound `Event`s: - - 1. `press` Set on button press. - 2. `release` Set on button release. - 3. `long` Set if button press is longer than `EButton.long_press_ms`. - 4. `double` Set if two button preses occur within `EButton.double_click_ms`. - -Application code is responsible for clearing these `Event`s - -Class attributes: - 1. `debounce_ms` Debounce time in ms. Default 50. - 2. `long_press_ms` Threshold time in ms for a long press. Default 1000. - 3. `double_click_ms` Threshold time in ms for a double-click. Default 400. - -### 6.2.1 The suppress constructor argument - -Consider a button double-click. By default with `suppress=False` this will set -the bound `Event` instances in order, as follows: - - * `press` - * `release` - * `press` - * `release` - * `double` - -Similarly a long press will trigger `press`, `long` and `release` in that -order. Some -applications may require only a single `Event` to be triggered. Setting -`suppress=True` ensures this. Outcomes are as follows: - -| Occurrence | Events set | Time of primary event | -|:-------------|:----------------|:-----------------------------| -| Short press | press, release | After `.double_click_ms` | -| Double press | double, release | When the second press occurs | -| Long press | long, release | After `long_press_ms` | - -The tradeoff is that the `press` and `release` events are delayed: the soonest -it is possible to detect the lack of a double click is `.double_click_ms`ms -after a short button press. Hence in the case of a short press when `suppress` -is `True`, `press` and `release` events are set on expiration of the double -click timer. - -### 6.2.2 The sense constructor argument +This is now documented [here](./DRIVERS.md#41-ebutton-class). -In most applications it can be assumed that, at power-up, pushbuttons are not -pressed. The default `None` value uses this assumption to read the pin state -and to assign the result to the `False` (not pressed) state at power up. This -works with normally open or normally closed buttons wired to either supply -rail; this without programmer intervention. +Documentation for `Keyboard`, `SwArray` and `RingbufQueue` has also moved to +[primtives](./DRIVERS.md). -In certain use cases this assumption does not hold, and `sense` must explicitly -be specified. This defines the logical state of the un-pressed button. Hence -`sense=0` defines a button connected in such a way that when it is not pressed, -the voltage on the pin is gnd. - -Whenever the pin value changes, the new value is compared with `sense` to -determine whether the button is closed or open. - -###### [Contents](./EVENTS.md#0-contents) - -## 6.3 Keyboard - -```python -from primitives import Keyboard -``` -A `Keyboard` provides an interface to a set of pushbuttons arranged as a -crosspoint array. If a key is pressed its array index (scan code) is placed on a -queue. Keypresses are retrieved with `async for`. The driver operates by -polling each row, reading the response of each column. 1-key rollover is -supported - this is the case where a key is pressed before the prior key has -been released. - -Example usage: -```python -import asyncio -from primitives import Keyboard -from machine import Pin -rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] -colpins = [Pin(p, Pin.IN, Pin.PULL_UP) for p in range(16, 20)] - -async def main(): - kp = Keyboard(rowpins, colpins) - async for scan_code in kp: - print(scan_code) - if not scan_code: - break # Quit on key with code 0 - -asyncio.run(main()) -``` -Constructor mandatory args: - * `rowpins` A list or tuple of initialised open drain output pins. - * `colpins` A list or tuple of initialised input pins (pulled up). - -Constructor optional keyword only args: - * `buffer=bytearray(10)` Keyboard buffer. - * `db_delay=50` Debounce delay in ms. - - Magic method: - * `__getitem__(self, scan_code)` Return the state of a given pin. Enables code - that causes actions after a button press, for example on release or auto-repeat - while pressed. - -The `Keyboard` class is subclassed from [Ringbuf queue](./EVENTS.md#7-ringbuf-queue) -enabling scan codes to be retrieved with an asynchronous iterator. - -In typical use the scan code would be used as the index into a string of -keyboard characters ordered to match the physical layout of the keys. If data -is not removed from the buffer, on overflow the oldest scan code is discarded. -There is no limit on the number of rows or columns however if more than 256 keys -are used, the `buffer` arg would need to be adapted to handle scan codes > 255. - -Usage example. Keypresses on a numeric keypad are sent to a UART with auto - repeat. -```python -import asyncio -from primitives import Keyboard, Delay_ms -from machine import Pin, UART - -async def repeat(tim, uart, ch): # Send at least one char - while True: - uart.write(ch) - tim.clear() # Clear any pre-existing event - tim.trigger() # Start the timer - await tim.wait() - -async def main(): # Run forever - rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] - colpins = [Pin(p, Pin.IN, Pin.PULL_UP) for p in range(16, 20)] - uart = UART(0, 9600, tx=0, rx=1) - pad = Keyboard(rowpins, colpins) - tim = Delay_ms(duration=200) # 200ms auto repeat timer - cmap = "123456789*0#" # Numeric keypad character map - async for scan_code in pad: - ch = cmap[scan_code] # Get character - rpt = asyncio.create_task(repeat(tim, uart, ch)) - while pad[scan_code]: # While key is held down - await asyncio.sleep_ms(0) - rpt.cancel() - -asyncio.run(main()) -``` -##### Application note - -Scanning of the keyboard occurs rapidly, and built-in pull-up resistors have a -high value. If the capacitance between wires is high, spurious keypresses may be -registered. To prevent this it is wise to add physical resistors between the -input pins and 3.3V. A value in the region of 1KΩ to 5KΩ is recommended. - -###### [Contents](./EVENTS.md#0-contents) - -## 6.4 SwArray -```python -from primitives import SwArray -``` -An `SwArray` is similar to a `Keyboard` except that single, double and long -presses are supported. Items in the array may be switches or pushbuttons, -however if switches are used they must be diode-isolated. This is because -pushbuttons are normally open, while switches may be left in open or closed -states. If more than two switches are closed, unwanted electrical connections -are made. The diodes prevent this. -![Image](./isolate.png) - -Constructor mandatory args: - * `rowpins` A list or tuple of initialised open drain output pins. - * `colpins` A list or tuple of initialised input pins (pulled up). - * `cfg` An integer defining conditions requiring a response. See Module - Constants below. - -Constructor optional keyword only args: - * `bufsize=10` Size of buffer. - - Magic method: - * `__getitem__(self, scan_code)` Return the state of a given pin. Enables code - that causes actions after a button press, for example on release or auto-repeat - while pressed. - - Synchronous bound method: - * `keymap()` Return an integer representing a bitmap of the debounced state of - all switches in the array. 1 == closed. - - Class variables: - * `debounce_ms = 50` Assumed maximum duration of contact bounce. - * `long_press_ms = 1000` Threshold for long press detection. - * `double_click_ms = 400` Threshold for double-click detection. - -Module constants. -The folowing constants are provided to simplify defining the `cfg` constructor -arg. This may be defined as a bitwise or of selected constants. For example if -the `CLOSE` bit is specified, switch closures will be reported. An omitted event -will be ignored. Where the array comprises switches it is usual to specify only -`CLOSE` and/or `OPEN`. This invokes a more efficient mode of operation because -timing is not required. - * `CLOSE` Report contact closure. - * `OPEN` Contact opening. - * `LONG` Contact closure longer than `long_press_ms`. - * `DOUBLE` Two closures in less than `double_click_ms`. - * `SUPPRESS` Disambiguate. For explanation see `EButton`. - -The `SwArray` class is subclassed from [Ringbuf queue](./EVENTS.md#7-ringbuf-queue). -This is an asynchronous iterator, enabling scan codes and event types to be -retrieved as state changes occur with `async for`: -```python -import asyncio -from primitives.sw_array import SwArray, CLOSE, OPEN, LONG, DOUBLE, SUPPRESS -from machine import Pin -rowpins = [Pin(p, Pin.OPEN_DRAIN) for p in range(10, 14)] -colpins = [Pin(p, Pin.IN, Pin.PULL_UP) for p in range(16, 20)] -cfg = CLOSE | OPEN #LONG | DOUBLE | SUPPRESS - -async def main(): - swa = SwArray(rowpins, colpins, cfg) - async for scan_code, evt in swa: - print(scan_code, evt) - if not scan_code: - break # Quit on key with code 0 - -asyncio.run(main()) -``` -##### Application note - -Scanning of the array occurs rapidly, and built-in pull-up resistors have a -high value. If the capacitance between wires is high, spurious closures may be -registered. To prevent this it is wise to add physical resistors between the -input pins and 3.3V. A value in the region of 1KΩ to 5KΩ is recommended. - -###### [Contents](./EVENTS.md#0-contents) - -# 7. Ringbuf Queue - -```python -from primitives import RingbufQueue -``` - -The API of the `Queue` aims for CPython compatibility. This is at some cost to -efficiency. As the name suggests, the `RingbufQueue` class uses a pre-allocated -circular buffer which may be of any mutable type supporting the buffer protocol -e.g. `list`, `array` or `bytearray`. - -Attributes of `RingbufQueue`: - 1. It is of fixed size, `Queue` can grow to arbitrary size. - 2. It uses pre-allocated buffers of various types (`Queue` uses a `list`). - 3. It is an asynchronous iterator allowing retrieval with `async for`. - 4. It has an "overwrite oldest data" synchronous write mode. - -Constructor mandatory arg: - * `buf` Buffer for the queue, e.g. list, bytearray or array. If an integer is - passed, a list of this size is created. A buffer of size `N` can hold a - maximum of `N-1` items. Note that, where items on the queue are suitably - limited, bytearrays or arrays are more efficient than lists. - -Synchronous methods (immediate return): - * `qsize` No arg. Returns the number of items in the queue. - * `empty` No arg. Returns `True` if the queue is empty. - * `full` No arg. Returns `True` if the queue is full. - * `get_nowait` No arg. Returns an object from the queue. Raises `IndexError` - if the queue is empty. - * `put_nowait` Arg: the object to put on the queue. Raises `IndexError` if the - queue is full. If the calling code ignores the exception the oldest item in - the queue will be overwritten. In some applications this can be of use. - * `peek` No arg. Returns oldest entry without removing it from the queue. This - is a superset of the CPython compatible methods. - -Asynchronous methods: - * `put` Arg: the object to put on the queue. If the queue is full, it will - block until space is available. - * `get` Return an object from the queue. If empty, block until an item is - available. - -Retrieving items from the queue: - -The `RingbufQueue` is an asynchronous iterator. Results are retrieved using -`async for`: -```python -async def handle_queued_data(q): - async for obj in q: - await asyncio.sleep(0) # See below - # Process obj -``` -The `sleep` is necessary if you have multiple tasks waiting on the queue, -otherwise one task hogs all the data. - -The following illustrates putting items onto a `RingbufQueue` where the queue is -not allowed to stall: where it becomes full, new items overwrite the oldest ones -in the queue: -```python -def add_item(q, data): - try: - q.put_nowait(data) - except IndexError: - pass -``` ###### [Contents](./EVENTS.md#0-contents) # 100 Appendix 1 Polling diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 1571a7f..58684a9 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -355,7 +355,7 @@ asyncio.run(main()) The CPython [docs](https://docs.python.org/3/library/asyncio-task.html#creating-tasks) have a warning that a reference to the task instance should be saved for the -task's duration. This was raised in +task's duration. This was raised in [this issue](https://github.com/micropython/micropython/issues/12299). MicroPython `asyncio` does not suffer from this bug, but writers of code which must work in CPython and MicroPython should take note. Code samples in this doc @@ -660,7 +660,7 @@ async def task(i, lock): async with lock: print("Acquired lock in task", i) await asyncio.sleep(0.5) - + async def main(): lock = Lock() # The Lock instance tasks = [None] * 3 # For CPython compaibility must store a reference see Note @@ -773,7 +773,7 @@ until all passed `Event`s have been set: from primitives import WaitAll evt1 = Event() evt2 = Event() -wa = WaitAll((evt1, evt2)).wait() +wa = WaitAll((evt1, evt2)).wait() # Launch tasks that might trigger these events await wa # Both were triggered @@ -1003,7 +1003,7 @@ application. Such a queue is termed "thread safe". The `Queue` class is an unofficial implementation whose API is a subset of that of CPython's `asyncio.Queue`. Like `asyncio.Queue` this class is not thread safe. A queue class optimised for MicroPython is presented in -[Ringbuf queue](./EVENTS.md#7-ringbuf-queue). A thread safe version is +[Ringbuf Queue](./DRIVERS.md#7-ringbuf-queue). A thread safe version is documented in [ThreadSafeQueue](./THREADING.md#22-threadsafequeue). Constructor: @@ -1264,106 +1264,12 @@ indicate that is has passed the critical point. ## 3.8 Delay_ms class This implements the software equivalent of a retriggerable monostable or a -watchdog timer. It has an internal boolean `running` state. When instantiated -the `Delay_ms` instance does nothing, with `running` `False` until triggered. -Then `running` becomes `True` and a timer is initiated. This can be prevented -from timing out by triggering it again (with a new timeout duration). So long -as it is triggered before the time specified in the preceeding trigger it will -never time out. - -If it does time out the `running` state will revert to `False`. This can be -interrogated by the object's `running()` method. In addition a `callable` can -be specified to the constructor. A `callable` can be a callback or a coroutine. -A callback will execute when a timeout occurs; where the `callable` is a -coroutine it will be converted to a `Task` and run asynchronously. - -Constructor arguments (defaults in brackets): - - 1. `func` The `callable` to call on timeout (default `None`). - 2. `args` A tuple of arguments for the `callable` (default `()`). - 3. `can_alloc` Unused arg, retained to avoid breaking code. - 4. `duration` Integer, default 1000 ms. The default timer period where no value - is passed to the `trigger` method. - -Synchronous methods: - - 1. `trigger` optional argument `duration=0`. A timeout will occur after - `duration` ms unless retriggered. If no arg is passed the period will be that - of the `duration` passed to the constructor. The method can be called from a - hard or soft ISR. It is now valid for `duration` to be less than the current - time outstanding. - 2. `stop` No argument. Cancels the timeout, setting the `running` status - `False`. The timer can be restarted by issuing `trigger` again. Also clears - the `Event` described in `wait` below. - 3. `running` No argument. Returns the running status of the object. - 4. `__call__` Alias for running. - 5. `rvalue` No argument. If a timeout has occurred and a callback has run, - returns the return value of the callback. If a coroutine was passed, returns - the `Task` instance. This allows the `Task` to be cancelled or awaited. - 6. `callback` args `func=None`, `args=()`. Allows the callable and its args to - be assigned, reassigned or disabled at run time. - 7. `deinit` No args. Cancels the running task. See [Object scope](./TUTORIAL.md#44-object-scope). - 8. `clear` No args. Clears the `Event` described in `wait` below. - 9. `set` No args. Sets the `Event` described in `wait` below. - -Asynchronous method: - 1. `wait` One or more tasks may wait on a `Delay_ms` instance. Pause until the - delay instance has timed out. +watchdog timer. On timeout it can launch a callback or coroutine. It exposes an +`Event` allowing a task to pause until a timeout occurs. The delay period may be +altered dynamically. -In this example a `Delay_ms` instance is created with the default duration of -1 sec. It is repeatedly triggered for 5 secs, preventing the callback from -running. One second after the triggering ceases, the callback runs. - -```python -import asyncio -from primitives import Delay_ms - -async def my_app(): - d = Delay_ms(callback, ('Callback running',)) - print('Holding off callback') - for _ in range(10): # Hold off for 5 secs - await asyncio.sleep_ms(500) - d.trigger() - print('Callback will run in 1s') - await asyncio.sleep(2) - print('Done') - -def callback(v): - print(v) - -try: - asyncio.run(my_app()) -finally: - asyncio.new_event_loop() # Clear retained state -``` -This example illustrates multiple tasks waiting on a `Delay_ms`. No callback is -used. -```python -import asyncio -from primitives import Delay_ms - -async def foo(n, d): - await d.wait() - d.clear() # Task waiting on the Event must clear it - print('Done in foo no.', n) - -async def my_app(): - d = Delay_ms() - tasks = [None] * 4 # For CPython compaibility must store a reference see Note - for n in range(4): - tasks[n] = asyncio.create_task(foo(n, d)) - d.trigger(3000) - print('Waiting on d') - await d.wait() - print('Done in my_app.') - await asyncio.sleep(1) - print('Test complete.') - -try: - asyncio.run(my_app()) -finally: - _ = asyncio.new_event_loop() # Clear retained state -``` +It may be found in the `primitives` directory and is documented in +[Delay_ms class](./DRIVERS.md#8-delay_ms class). ## 3.9 Message @@ -1382,10 +1288,15 @@ It may be found in the `threadsafe` directory and is documented ## 3.10 Synchronising to hardware The following hardware-related classes are documented [here](./DRIVERS.md): + * `ESwitch` A debounced switch with an `Event` interface. * `Switch` A debounced switch which can trigger open and close user callbacks. + * `EButton` Debounced pushbutton with `Event` instances for pressed, released, + long press or double-press. * `Pushbutton` Debounced pushbutton with callbacks for pressed, released, long press or double-press. * `ESP32Touch` Extends `Pushbutton` class to support ESP32 touchpads. + * `Keyboard` Interface a crosspoint array of buttons e.g. keypads. + * `SwArray` Interface a crosspoint array of pushbuttons or switches. * `Encoder` An asynchronous interface for control knobs with switch contacts configured as a quadrature encoder. * `AADC` Asynchronous ADC. A task can pause until the value read from an ADC @@ -2925,7 +2836,7 @@ The above comments refer to an ideal scheduler. Currently `asyncio` is not in this category, with worst-case latency being > `N`ms. The conclusions remain valid. -This, along with other issues, is discussed in +This, along with other issues, is discussed in [Interfacing asyncio to interrupts](./INTERRUPTS.md). ###### [Contents](./TUTORIAL.md#contents) From f61b0861812aa78871633463743fe943df3e2883 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 23 Oct 2023 15:54:33 +0100 Subject: [PATCH 258/305] Fix broken link. --- v3/docs/DRIVERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index c53eead..54f2cc1 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -25,7 +25,7 @@ MicroPython's `asyncio` when used in a microcontroller context. 6. [Quadrature encoders](./DRIVERS.md#6-quadrature-encoders) Asynchronous interface for rotary encoders. 6.1 [Encoder class](./DRIVERS.md#61-encoder-class) 7. [Ringbuf Queue](./DRIVERS.md#7-ringbuf-queue) A MicroPython optimised queue primitive. - 8. [Delay_ms class](./DRIVERS.md#8-delay_ms class) A flexible retriggerable delay with callback or Event interface. + 8. [Delay_ms class](./DRIVERS.md#8-delay_ms-class) A flexible retriggerable delay with callback or Event interface. 9. [Additional functions](./DRIVERS.md#9-additional-functions) 9.1 [launch](./DRIVERS.md#91-launch) Run a coro or callback interchangeably. 9.2 [set_global_exception](./DRIVERS.md#92-set_global_exception) Simplify debugging with a global exception handler. From 84201653d5398ef9b219dae58782c59c3a74a0ad Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 23 Oct 2023 15:57:37 +0100 Subject: [PATCH 259/305] Fix broken link. --- v3/docs/DRIVERS.md | 2 +- v3/docs/TUTORIAL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 54f2cc1..44f0e93 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -1018,7 +1018,7 @@ def add_item(q, data): ``` ###### [Contents](./DRIVERS.md#0-contents) -## 3.8 Delay_ms class +# 8. Delay_ms class This implements the software equivalent of a retriggerable monostable or a watchdog timer. It has an internal boolean `running` state. When instantiated diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 58684a9..4dfedb6 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1269,7 +1269,7 @@ watchdog timer. On timeout it can launch a callback or coroutine. It exposes an altered dynamically. It may be found in the `primitives` directory and is documented in -[Delay_ms class](./DRIVERS.md#8-delay_ms class). +[Delay_ms class](./DRIVERS.md#8-delay_ms-class). ## 3.9 Message From bef5b9b5fcfe5cbd0913864eb9485df99c28160f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 25 Oct 2023 14:05:49 +0100 Subject: [PATCH 260/305] THREADING.md: Fix broken link. --- v3/docs/THREADING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/THREADING.md b/v3/docs/THREADING.md index f5122fd..e9a644c 100644 --- a/v3/docs/THREADING.md +++ b/v3/docs/THREADING.md @@ -719,7 +719,7 @@ asyncio.run(main()) ``` ###### [Contents](./THREADING.md#contents) -## 4.1 More general solution +## 4.2 More general solution This provides a queueing mechanism. A task can assign a blocking function to a core even if the core is already busy. Further it allows for multiple cores or From f8d1257f673629e130a51ce941f8bf7967950f73 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 6 Nov 2023 16:46:02 +0000 Subject: [PATCH 261/305] Tutorial: How to poll a ThreadSafeFlag. --- v3/docs/TUTORIAL.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 4dfedb6..5a4d0fe 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -42,6 +42,7 @@ import uasyncio as asyncio      3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore) 3.5 [Queue](./TUTORIAL.md#35-queue) 3.6 [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) Synchronisation with asynchronous events and interrupts. +      3.6.1 [Querying a ThreadSafeFlag](./TUTORIAL.md#361-querying-a-threadsafeflag) Check its state without blocking. 3.7 [Barrier](./TUTORIAL.md#37-barrier) 3.8 [Delay_ms](./TUTORIAL.md#38-delay_ms-class) Software retriggerable delay. 3.9 [Message](./TUTORIAL.md#39-message) @@ -1149,6 +1150,48 @@ processing received data. See [Threadsafe Event](./THREADING.md#31-threadsafe-event) for a thread safe class which allows multiple tasks to wait on it. +### 3.6.1 Querying a ThreadSafeFlag + +The state of a ThreadSafeFlag may be tested as follows: +```python +import asyncio +from select import poll, POLLIN +from time import ticks_us, ticks_diff + +async def foo(tsf): # Periodically set the ThreadSafeFlag + while True: + await asyncio.sleep(1) + tsf.set() + +def ready(tsf, poller): + poller.register(tsf, POLLIN) + + def is_rdy(): + return len([t for t in poller.ipoll(0) if t[0] is tsf]) > 0 + + return is_rdy + +async def test(): + tsf = asyncio.ThreadSafeFlag() + tsk = asyncio.create_task(foo(tsf)) + mpoll = poll() + tsf_ready = ready(tsf, mpoll) # Create a ready function + for _ in range(25): # Run for 5s + if tsf_ready(): + print("tsf ready") + t = ticks_us() + await tsf.wait() + print(f"got tsf in {ticks_diff(ticks_us(), t)}us") + else: + print("Not ready") + await asyncio.sleep_ms(200) + +asyncio.run(test()) +``` +The `ready` closure returns a nonblocking function which tests the status of a +given flag. In the above example `.wait()` is not called until the flag has been +set, consequently `.wait()` returns rapidly. + ###### [Contents](./TUTORIAL.md#contents) ## 3.7 Barrier From 5cc34b3dd64d2aad7b55fd316be112dc0633672b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 11 Nov 2023 16:54:53 +0000 Subject: [PATCH 262/305] Encoder: Improve driver, document. --- v3/docs/DRIVERS.md | 8 ++++---- v3/docs/TUTORIAL.md | 11 ++++++----- v3/primitives/encoder.py | 37 +++++++++++++++++++++++++++++++------ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 44f0e93..226cda9 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -866,7 +866,7 @@ behaviour. The `Encoder` can be instantiated in such a way that its effective resolution can be reduced. A virtual encoder with lower resolution can be useful in some -applications. +applications. In particular it can track the "clicks" of a mechanical detent. The driver allows limits to be assigned to the virtual encoder's value so that a dial running from (say) 0 to 100 may be implemented. If limits are used, @@ -908,10 +908,10 @@ Constructor arguments: receives two integer args, `v` being the virtual encoder's current value and `delta` being the signed difference between the current value and the previous one. Further args may be appended by the following. - 9. `args=()` An optional tuple of positionl args for the callback. + 9. `args=()` An optional tuple of positional args for the callback. 10. `delay=100` After motion is detected the driver waits for `delay` ms before - reading the current position. A delay can be used to limit the rate at which - the callback is invoked. This is a minimal approach. See + reading the current position. A delay limits the rate at which the callback is + invoked and improves debouncing. This is a minimal approach. See [this script](https://github.com/peterhinch/micropython-async/blob/master/v3/primitives/tests/encoder_stop.py) for a way to create a callback which runs only when the encoder stops moving. diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 5a4d0fe..cb4101b 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1163,13 +1163,14 @@ async def foo(tsf): # Periodically set the ThreadSafeFlag await asyncio.sleep(1) tsf.set() -def ready(tsf, poller): - poller.register(tsf, POLLIN) + def ready(tsf, poller): + r = (tsf, POLLIN) + poller.register(*r) - def is_rdy(): - return len([t for t in poller.ipoll(0) if t[0] is tsf]) > 0 + def is_rdy(): + return r in poller.ipoll(0) - return is_rdy + return is_rdy async def test(): tsf = asyncio.ThreadSafeFlag() diff --git a/v3/primitives/encoder.py b/v3/primitives/encoder.py index 9ae4e49..8365de7 100644 --- a/v3/primitives/encoder.py +++ b/v3/primitives/encoder.py @@ -1,9 +1,9 @@ # encoder.py Asynchronous driver for incremental quadrature encoder. -# Copyright (c) 2021-2022 Peter Hinch +# Copyright (c) 2021-2023 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -# For an explanation of the design please see +# For an explanation of the design please see # [ENCODERS.md](https://github.com/peterhinch/micropython-samples/blob/master/encoders/ENCODERS.md) # Thanks are due to the following collaborators: @@ -19,11 +19,33 @@ import uasyncio as asyncio from machine import Pin +from select import poll, POLLIN -class Encoder: - def __init__(self, pin_x, pin_y, v=0, div=1, vmin=None, vmax=None, - mod=None, callback=lambda a, b : None, args=(), delay=100): +def ready(tsf, poller): + r = (tsf, POLLIN) + poller.register(*r) + + def is_rdy(): + return r in poller.ipoll(0) + + return is_rdy + + +class Encoder: + def __init__( + self, + pin_x, + pin_y, + v=0, + div=1, + vmin=None, + vmax=None, + mod=None, + callback=lambda a, b: None, + args=(), + delay=100, + ): self._pin_x = pin_x self._pin_y = pin_y self._x = pin_x() @@ -34,8 +56,9 @@ def __init__(self, pin_x, pin_y, v=0, div=1, vmin=None, vmax=None, self._trig = asyncio.Event() if ((vmin is not None) and v < vmin) or ((vmax is not None) and v > vmax): - raise ValueError('Incompatible args: must have vmin <= v <= vmax') + raise ValueError("Incompatible args: must have vmin <= v <= vmax") self._tsf = asyncio.ThreadSafeFlag() + self._tsf_ready = ready(self._tsf, poll()) # Create a ready function trig = Pin.IRQ_RISING | Pin.IRQ_FALLING try: xirq = pin_x.irq(trigger=trig, handler=self._x_cb, hard=True) @@ -67,6 +90,8 @@ async def _run(self, vmin, vmax, div, mod, cb, args): plcv = pcv # Previous value after limits applied delay = self.delay while True: + if delay > 0 and self._tsf_ready(): # Ensure ThreadSafeFlag is clear + await self._tsf.wait() await self._tsf.wait() await asyncio.sleep_ms(delay) # Wait for motion to stop. hv = self._v # Sample hardware (atomic read). From a1b4995d9e6c8b8d2ea55a556bd00621325cf6c4 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 17 Nov 2023 10:22:30 +0000 Subject: [PATCH 263/305] TUTORIAL.md: Fix typo. --- v3/docs/TUTORIAL.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index cb4101b..ed5bbc2 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -2059,7 +2059,10 @@ async def main(): asyncio.run(main()) ``` -The `.readline` method will pause until `\n` is received. The `.read` +The `.readline` method will pause until `\n` is received. + +###### StreamWriter write methods + Writing to a `StreamWriter` occurs in two stages. The synchronous `.write` method concatenates data for later transmission. The asynchronous `.drain` causes transmission. To avoid allocation call `.drain` after each call to From 2061f308a0c6a5fd1371e92d1daf88b32406388d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 22 Nov 2023 16:25:14 +0000 Subject: [PATCH 264/305] sched: Add package.json. --- v3/as_drivers/sched/package.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 v3/as_drivers/sched/package.json diff --git a/v3/as_drivers/sched/package.json b/v3/as_drivers/sched/package.json new file mode 100644 index 0000000..5478e3b --- /dev/null +++ b/v3/as_drivers/sched/package.json @@ -0,0 +1,13 @@ +{ + "urls": [ + ["sched/primitives/__init__.py", "github:peterhinch/micropython-async/v3/sched/primitives/__init__.py"], + ["sched/__init__.py", "github:peterhinch/micropython-async/v3/sched/__init__.py"], + ["sched/asynctest.py", "github:peterhinch/micropython-async/v3/sched/asynctest.py"], + ["sched/cron.py", "github:peterhinch/micropython-async/v3/sched/cron.py"], + ["sched/crontest.py", "github:peterhinch/micropython-async/v3/sched/crontest.py"], + ["sched/sched.py", "github:peterhinch/micropython-async/v3/sched/sched.py"], + ["sched/simulate.py", "github:peterhinch/micropython-async/v3/sched/simulate.py"], + ["sched/synctest.py", "github:peterhinch/micropython-async/v3/sched/synctest.py"] + ], + "version": "0.1" +} From 4e9b4b18e0153fd83390298e16b27616a31f5cc4 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 22 Nov 2023 16:34:05 +0000 Subject: [PATCH 265/305] sched: Add package.json. --- v3/as_drivers/sched/package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/v3/as_drivers/sched/package.json b/v3/as_drivers/sched/package.json index 5478e3b..c862aae 100644 --- a/v3/as_drivers/sched/package.json +++ b/v3/as_drivers/sched/package.json @@ -1,13 +1,13 @@ { "urls": [ - ["sched/primitives/__init__.py", "github:peterhinch/micropython-async/v3/sched/primitives/__init__.py"], - ["sched/__init__.py", "github:peterhinch/micropython-async/v3/sched/__init__.py"], - ["sched/asynctest.py", "github:peterhinch/micropython-async/v3/sched/asynctest.py"], - ["sched/cron.py", "github:peterhinch/micropython-async/v3/sched/cron.py"], - ["sched/crontest.py", "github:peterhinch/micropython-async/v3/sched/crontest.py"], - ["sched/sched.py", "github:peterhinch/micropython-async/v3/sched/sched.py"], - ["sched/simulate.py", "github:peterhinch/micropython-async/v3/sched/simulate.py"], - ["sched/synctest.py", "github:peterhinch/micropython-async/v3/sched/synctest.py"] + ["sched/primitives/__init__.py", "github:peterhinch/micropython-async/v3/as_drivers/sched/primitives/__init__.py"], + ["sched/__init__.py", "github:peterhinch/micropython-async/v3/as_drivers/sched/__init__.py"], + ["sched/asynctest.py", "github:peterhinch/micropython-async/v3/as_drivers/sched/asynctest.py"], + ["sched/cron.py", "github:peterhinch/micropython-async/v3/as_drivers/sched/cron.py"], + ["sched/crontest.py", "github:peterhinch/micropython-async/v3/as_drivers/sched/crontest.py"], + ["sched/sched.py", "github:peterhinch/micropython-async/v3/as_drivers/sched/sched.py"], + ["sched/simulate.py", "github:peterhinch/micropython-async/v3/as_drivers/sched/simulate.py"], + ["sched/synctest.py", "github:peterhinch/micropython-async/v3/as_drivers/sched/synctest.py"] ], "version": "0.1" } From 2a6457800f0b25e2102276432ef5295323a2841e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 22 Nov 2023 17:11:13 +0000 Subject: [PATCH 266/305] SCHEDULE.md: Add mpremote installation. --- v3/docs/SCHEDULE.md | 51 +++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index 9d61c55..0303175 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -3,7 +3,7 @@ 1. [Scheduling tasks](./SCHEDULE.md#1-scheduling-tasks) 2. [Overview](./SCHEDULE.md#2-overview) 3. [Installation](./SCHEDULE.md#3-installation) - 4. [The schedule function](./SCHEDULE.md#4-the-schedule-function) The primary interface for uasyncio + 4. [The schedule function](./SCHEDULE.md#4-the-schedule-function) The primary interface for asyncio 4.1 [Time specifiers](./SCHEDULE.md#41-time-specifiers) 4.2 [Calendar behaviour](./SCHEDULE.md#42-calendar-behaviour) Calendars can be tricky...      4.2.1 [Behaviour of mday and wday values](./SCHEDULE.md#421-behaviour-of-mday-and-wday-values) @@ -37,8 +37,8 @@ It is partly inspired by the Unix cron table, also by the latter it is less capable but is small, fast and designed for microcontroller use. Repetitive and one-shot events may be created. -It is ideally suited for use with `uasyncio` and basic use requires minimal -`uasyncio` knowledge. Users intending only to schedule callbacks can simply +It is ideally suited for use with `asyncio` and basic use requires minimal +`asyncio` knowledge. Users intending only to schedule callbacks can simply adapt the example code. It can be used in synchronous code and an example is provided. @@ -48,13 +48,13 @@ and the Unix build. # 2. Overview The `schedule` function (`sched/sched.py`) is the interface for use with -`uasyncio`. The function takes a callback and causes that callback to run at +`asyncio`. The function takes a callback and causes that callback to run at specified times. A coroutine may be substituted for the callback - at the specified times it will be promoted to a `Task` and run. The `schedule` function instantiates a `cron` object (in `sched/cron.py`). This is the core of the scheduler: it is a closure created with a time specifier and -returning the time to the next scheduled event. Users of `uasyncio` do not need +returning the time to the next scheduled event. Users of `asyncio` do not need to deal with `cron` instances. This library can also be used in synchronous code, in which case `cron` @@ -64,23 +64,28 @@ instances must explicitly be created. # 3. Installation -Copy the `sched` directory and contents to the target's filesystem. It requires -`uasyncio` V3 which is included in daily firmware builds and in release builds -after V1.12. - -To install to an SD card using [rshell](https://github.com/dhylands/rshell) -move to the parent directory of `sched` and issue: +Copy the `sched` directory and contents to the target's filesystem. This may be +done with the official [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html): +```bash +$ mpremote mip install "github:peterhinch/micropython-async/v3/as_drivers/sched" +``` +On networked platforms it may be installed with [mip](https://docs.micropython.org/en/latest/reference/packages.html). +```py +>>> mip.install("github:peterhinch/micropython-async/v3/as_drivers/sched") +``` +Currently these tools install to `/lib` on the built-in Flash memory. To install +to a Pyboard's SD card [rshell](https://github.com/dhylands/rshell) may be used. +Move to the SD card root, run `rshell` and issue: ``` > rsync sched /sd/sched ``` -Adapt the destination as appropriate for your hardware. The following files are installed in the `sched` directory. 1. `cron.py` Computes time to next event. - 2. `sched.py` The `uasyncio` `schedule` function: schedule a callback or coro. + 2. `sched.py` The `asyncio` `schedule` function: schedule a callback or coro. 3. `primitives/__init__.py` Necessary for `sched.py`. 4. `asynctest.py` Demo of asynchronous scheduling. - 5. `synctest.py` Synchronous scheduling demo. For `uasyncio` phobics only. + 5. `synctest.py` Synchronous scheduling demo. For `asyncio` phobics only. 6. `crontest.py` A test for `cron.py` code. 7. `simulate.py` A simple script which may be adapted to prove that a `cron` instance will behave as expected. See [The simulate script](./SCHEDULE.md#8-the-simulate-script). @@ -125,7 +130,7 @@ import sched.asynctest ``` This is the demo code. ```python -import uasyncio as asyncio +import asyncio as asyncio from sched.sched import schedule from time import localtime @@ -157,7 +162,7 @@ finally: ``` The event-based interface can be simpler than using callables: ```python -import uasyncio as asyncio +import asyncio as asyncio from sched.sched import schedule from time import localtime @@ -201,7 +206,7 @@ Setting `secs=None` will cause a `ValueError`. Passing an iterable to `secs` is not recommended: this library is intended for scheduling relatively long duration events. For rapid sequencing, schedule a -coroutine which awaits `uasyncio` `sleep` or `sleep_ms` routines. If an +coroutine which awaits `asyncio` `sleep` or `sleep_ms` routines. If an iterable is passed, triggers must be at least ten seconds apart or a `ValueError` will result. @@ -253,7 +258,7 @@ asyncio.create_task(schedule(foo, month=(2, 7, 10), hrs=1, mins=59)) ## 4.3 Limitations The underlying `cron` code has a resolution of 1 second. The library is -intended for scheduling infrequent events (`uasyncio` has its own approach to +intended for scheduling infrequent events (`asyncio` has its own approach to fast scheduling). Specifying `secs=None` will cause a `ValueError`. The minimum interval between @@ -269,7 +274,7 @@ to the Unix build where daylight saving needs to be considered. ## 4.4 The Unix build -Asynchronous use requires `uasyncio` V3, so ensure this is installed on the +Asynchronous use requires `asyncio` V3, so ensure this is installed on the Linux target. The synchronous and asynchronous demos run under the Unix build. The module is @@ -283,7 +288,7 @@ is to avoid scheduling the times in your region where this occurs (01.00.00 to # 5. The cron object -This is the core of the scheduler. Users of `uasyncio` do not need to concern +This is the core of the scheduler. Users of `asyncio` do not need to concern themseleves with it. It is documented for those wishing to modify the code and for those wanting to perform scheduling in synchronous code. @@ -342,7 +347,7 @@ applications. On my reference board timing drifted by 1.4mins/hr, an error of Boards with internet connectivity can periodically synchronise to an NTP server but this carries a risk of sudden jumps in the system time which may disrupt -`uasyncio` and the scheduler. +`asyncio` and the scheduler. ##### [Top](./SCHEDULE.md#0-contents) @@ -403,9 +408,9 @@ main() ``` In my opinion the asynchronous version is cleaner and easier to understand. It -is also more versatile because the advanced features of `uasyncio` are +is also more versatile because the advanced features of `asyncio` are available to the application including cancellation of scheduled tasks. The -above code is incompatible with `uasyncio` because of the blocking calls to +above code is incompatible with `asyncio` because of the blocking calls to `time.sleep()`. ## 7.1 Initialisation From 1e6dbe5547af054ee98dfa3e23767b6e275017f2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 23 Nov 2023 18:52:45 +0000 Subject: [PATCH 267/305] Schedule: Add async for interface. --- v3/as_drivers/sched/sched.py | 31 +++++- v3/docs/SCHEDULE.md | 205 ++++++++++++++++++++++------------- 2 files changed, 159 insertions(+), 77 deletions(-) diff --git a/v3/as_drivers/sched/sched.py b/v3/as_drivers/sched/sched.py index 0fbaadd..e67a6df 100644 --- a/v3/as_drivers/sched/sched.py +++ b/v3/as_drivers/sched/sched.py @@ -14,6 +14,25 @@ # Wait prior to a sequence start _PAUSE = const(2) + +class Sequence: # Enable asynchronous iterator interface + def __init__(self): + self._evt = asyncio.Event() + self._args = None + + def __aiter__(self): + return self + + async def __anext__(self): + await self._evt.wait() + self._evt.clear() + return self._args + + def trigger(self, args): + self._args = args + self._evt.set() + + async def schedule(func, *args, times=None, **kwargs): async def long_sleep(t): # Sleep with no bounds. Immediate return if t < 0. while t > 0: @@ -23,16 +42,20 @@ async def long_sleep(t): # Sleep with no bounds. Immediate return if t < 0. tim = mktime(localtime()[:3] + (0, 0, 0, 0, 0)) # Midnight last night now = round(time()) # round() is for Unix fcron = cron(**kwargs) # Cron instance for search. - while tim < now: # Find first event in sequence + while tim < now: # Find first future trigger in sequence # Defensive. fcron should never return 0, but if it did the loop would never quit tim += max(fcron(tim), 1) - await long_sleep(tim - now - _PAUSE) # Time to wait (can be < 0) + # Wait until just before the first future trigger + await long_sleep(tim - now - _PAUSE) # Time to wait (can be < 0) - while times is None or times > 0: - tw = fcron(round(time())) # Time to wait (s) + while times is None or times > 0: # Until all repeats are done (or forever). + tw = fcron(round(time())) # Time to wait (s) (fcron is stateless). await long_sleep(tw) + res = None if isinstance(func, asyncio.Event): func.set() + elif isinstance(func, Sequence): + func.trigger(args) else: res = launch(func, args) if times is not None: diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index 0303175..a86bd6e 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -3,14 +3,16 @@ 1. [Scheduling tasks](./SCHEDULE.md#1-scheduling-tasks) 2. [Overview](./SCHEDULE.md#2-overview) 3. [Installation](./SCHEDULE.md#3-installation) - 4. [The schedule function](./SCHEDULE.md#4-the-schedule-function) The primary interface for asyncio + 4. [The schedule coroutine](./SCHEDULE.md#4-the-schedule-coroutine) The primary interface for asyncio. 4.1 [Time specifiers](./SCHEDULE.md#41-time-specifiers) 4.2 [Calendar behaviour](./SCHEDULE.md#42-calendar-behaviour) Calendars can be tricky...      4.2.1 [Behaviour of mday and wday values](./SCHEDULE.md#421-behaviour-of-mday-and-wday-values)      4.2.2 [Time causing month rollover](./SCHEDULE.md#422-time-causing-month-rollover) 4.3 [Limitations](./SCHEDULE.md#43-limitations) 4.4 [The Unix build](./SCHEDULE.md#44-the-unix-build) - 5. [The cron object](./SCHEDULE.md#5-the-cron-object) For hackers and synchronous coders + 4.5 [Callback interface](./SCHEDULE.md#45-callback-interface) Alternative interface using callbacks. + 4.6 [Event interface](./SCHEDULE.md#46-event-interface) Alternative interface using Event instances. +5. [The cron object](./SCHEDULE.md#5-the-cron-object) The rest of this doc is for hackers and synchronous coders. 5.1 [The time to an event](./SCHEDULE.md#51-the-time-to-an-event) 5.2 [How it works](./SCHEDULE.md#52-how-it-works) 6. [Hardware timing limitations](./SCHEDULE.md#6-hardware-timing-limitations) @@ -19,6 +21,7 @@ 8. [The simulate script](./SCHEDULE.md#8-the-simulate-script) Rapidly test sequences. Release note: +23rd Nov 2023 Add asynchronous iterator interface. 3rd April 2023 Fix issue #100. Where an iterable is passed to `secs`, triggers must now be at least 10s apart (formerly 2s). @@ -38,34 +41,40 @@ latter it is less capable but is small, fast and designed for microcontroller use. Repetitive and one-shot events may be created. It is ideally suited for use with `asyncio` and basic use requires minimal -`asyncio` knowledge. Users intending only to schedule callbacks can simply -adapt the example code. It can be used in synchronous code and an example is -provided. +`asyncio` knowledge. Example code is provided offering various ways of +responding to timing triggers including running callbacks. The module can be +also be used in synchronous code and an example is provided. It is cross-platform and has been tested on Pyboard, Pyboard D, ESP8266, ESP32 and the Unix build. # 2. Overview -The `schedule` function (`sched/sched.py`) is the interface for use with -`asyncio`. The function takes a callback and causes that callback to run at -specified times. A coroutine may be substituted for the callback - at the -specified times it will be promoted to a `Task` and run. +The `schedule` coroutine (`sched/sched.py`) is the interface for use with +`asyncio`. Three interface alternatives are offered which vary in the behaviour: +which occurs when a scheduled trigger occurs: +1. An asynchronous iterator is triggered. +2. A user defined `Event` is set. +3. A user defined callback or coroutine is launched. -The `schedule` function instantiates a `cron` object (in `sched/cron.py`). This -is the core of the scheduler: it is a closure created with a time specifier and -returning the time to the next scheduled event. Users of `asyncio` do not need -to deal with `cron` instances. +One or more `schedule` tasks may be assigned to a `Sequence` instance. This +enables an `async for` statement to be triggered whenever any of the `schedule` +tasks is triggered. -This library can also be used in synchronous code, in which case `cron` -instances must explicitly be created. +Under the hood the `schedule` function instantiates a `cron` object (in +`sched/cron.py`). This is the core of the scheduler: it is a closure created +with a time specifier and returning the time to the next scheduled event. Users +of `asyncio` do not need to deal with `cron` instances. This library can also be +used in synchronous code, in which case `cron` instances must explicitly be +created. ##### [Top](./SCHEDULE.md#0-contents) # 3. Installation -Copy the `sched` directory and contents to the target's filesystem. This may be -done with the official [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html): +The `sched` directory and contents must be copied to the target's filesystem. +This may be done with the official +[mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html): ```bash $ mpremote mip install "github:peterhinch/micropython-async/v3/as_drivers/sched" ``` @@ -75,7 +84,7 @@ On networked platforms it may be installed with [mip](https://docs.micropython.o ``` Currently these tools install to `/lib` on the built-in Flash memory. To install to a Pyboard's SD card [rshell](https://github.com/dhylands/rshell) may be used. -Move to the SD card root, run `rshell` and issue: +Move to `as_drivers` on the PC, run `rshell` and issue: ``` > rsync sched /sd/sched ``` @@ -94,16 +103,19 @@ The following files are installed in the `sched` directory. The `crontest` script is only of interest to those wishing to adapt `cron.py`. It will run on any MicroPython target. -# 4. The schedule function +# 4. The schedule coroutine -This enables a callback or coroutine to be run at intervals. The callable can -be specified to run forever, once only or a fixed number of times. `schedule` -is an asynchronous function. +This enables a response to be triggered at intervals. The response can be +specified to occur forever, once only or a fixed number of times. `schedule` +is a coroutine and is typically run as a background task as follows: +```python +asyncio.create_task(schedule(foo, 'every 4 mins', hrs=None, mins=range(0, 60, 4))) +``` Positional args: - 1. `func` The callable (callback or coroutine) to run. Alternatively an - `Event` may be passed (see below). - 2. Any further positional args are passed to the callable. + 1. `func` This may be a callable (callback or coroutine) to run, a user defined + `Event` or an instance of a `Sequence`. + 2. Any further positional args are passed to the callable or the `Sequence`. Keyword-only args. Args 1..6 are [Time specifiers](./SCHEDULE.md#41-time-specifiers): a variety of data types @@ -125,65 +137,37 @@ the value returned by that run of the callable. Because `schedule` does not terminate promptly it is usually started with `asyncio.create_task`, as in the following example where a callback is scheduled at various times. The code below may be run by issuing -```python -import sched.asynctest -``` -This is the demo code. -```python -import asyncio as asyncio -from sched.sched import schedule -from time import localtime - -def foo(txt): # Demonstrate callback - yr, mo, md, h, m, s, wd = localtime()[:7] - fst = 'Callback {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}' - print(fst.format(txt, h, m, s, md, mo, yr)) +The event-based interface can be simpler than using callables: -async def bar(txt): # Demonstrate coro launch - yr, mo, md, h, m, s, wd = localtime()[:7] - fst = 'Coroutine {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}' - print(fst.format(txt, h, m, s, md, mo, yr)) - await asyncio.sleep(0) +The remainder of this section describes the asynchronous iterator interface as +this is the simplest to use. The other interfaces are discussed in +* [4.5 Callback interface](./SCHEDULE.md#45-callback-interface) +* [4.6 Event interface](./SCHEDULE.md#46-event-interface) -async def main(): - print('Asynchronous test running...') - asyncio.create_task(schedule(foo, 'every 4 mins', hrs=None, mins=range(0, 60, 4))) - asyncio.create_task(schedule(foo, 'every 5 mins', hrs=None, mins=range(0, 60, 5))) - # Launch a coroutine - asyncio.create_task(schedule(bar, 'every 3 mins', hrs=None, mins=range(0, 60, 3))) - # Launch a one-shot task - asyncio.create_task(schedule(foo, 'one shot', hrs=None, mins=range(0, 60, 2), times=1)) - await asyncio.sleep(900) # Quit after 15 minutes - -try: - asyncio.run(main()) -finally: - _ = asyncio.new_event_loop() -``` -The event-based interface can be simpler than using callables: +One or more `schedule` instances are collected in a `Sequence` object. This +supports the asynchronous iterator interface: ```python -import asyncio as asyncio -from sched.sched import schedule +import uasyncio as asyncio +from sched.sched import schedule, Sequence from time import localtime async def main(): print("Asynchronous test running...") - evt = asyncio.Event() - asyncio.create_task(schedule(evt, hrs=10, mins=range(0, 60, 4))) - while True: - await evt.wait() # Multiple tasks may wait on an Event - evt.clear() # It must be cleared. + seq = Sequence() # A Sequence comprises one or more schedule instances + asyncio.create_task(schedule(seq, 'every 4 mins', hrs=None, mins=range(0, 60, 4))) + asyncio.create_task(schedule(seq, 'every 5 mins', hrs=None, mins=range(0, 60, 5))) + asyncio.create_task(schedule(seq, 'every 3 mins', hrs=None, mins=range(0, 60, 3))) + # A one-shot trigger + asyncio.create_task(schedule(seq, 'one shot', hrs=None, mins=range(0, 60, 2), times=1)) + async for args in seq: yr, mo, md, h, m, s, wd = localtime()[:7] - print(f"Event {h:02d}:{m:02d}:{s:02d} on {md:02d}/{mo:02d}/{yr}") + print(f"Event {h:02d}:{m:02d}:{s:02d} on {md:02d}/{mo:02d}/{yr} args: {args}") try: asyncio.run(main()) finally: _ = asyncio.new_event_loop() ``` -See [tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#32-event). -Also [this doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/EVENTS.md) -for a discussion of event-based programming. ##### [Top](./SCHEDULE.md#0-contents) @@ -286,6 +270,81 @@ is to avoid scheduling the times in your region where this occurs (01.00.00 to ##### [Top](./SCHEDULE.md#0-contents) +## 4.5 Callback interface + +In this instance a user defined `callable` is passed as the first `schedule` arg. +A `callable` may be a function or a coroutine. It is possible for multiple +`schedule` instances to call the same callback, as in the example below. The +code is included in the library as `sched/asyntest.py` and may be run as below. +```python +import sched.asynctest +``` +This is the demo code. +```python +import uasyncio as asyncio +from sched.sched import schedule +from time import localtime + +def foo(txt): # Demonstrate callback + yr, mo, md, h, m, s, wd = localtime()[:7] + fst = 'Callback {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}' + print(fst.format(txt, h, m, s, md, mo, yr)) + +async def bar(txt): # Demonstrate coro launch + yr, mo, md, h, m, s, wd = localtime()[:7] + fst = 'Coroutine {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}' + print(fst.format(txt, h, m, s, md, mo, yr)) + await asyncio.sleep(0) + +async def main(): + print('Asynchronous test running...') + asyncio.create_task(schedule(foo, 'every 4 mins', hrs=None, mins=range(0, 60, 4))) + asyncio.create_task(schedule(foo, 'every 5 mins', hrs=None, mins=range(0, 60, 5))) + # Launch a coroutine + asyncio.create_task(schedule(bar, 'every 3 mins', hrs=None, mins=range(0, 60, 3))) + # Launch a one-shot task + asyncio.create_task(schedule(foo, 'one shot', hrs=None, mins=range(0, 60, 2), times=1)) + await asyncio.sleep(900) # Quit after 15 minutes + +try: + asyncio.run(main()) +finally: + _ = asyncio.new_event_loop() +``` +##### [Top](./SCHEDULE.md#0-contents) + +## 4.6 Event interface + +In this instance a user defined `Event` is passed as the first `schedule` arg. +It is possible for multiple `schedule` instances to trigger the same `Event`. +The user is responsible for clearing the `Event`. This interface has a drawback +in that extra positional args passed to `schedule` are lost. +```python +import uasyncio as asyncio +from sched.sched import schedule +from time import localtime + +async def main(): + print("Asynchronous test running...") + evt = asyncio.Event() + asyncio.create_task(schedule(evt, hrs=10, mins=range(0, 60, 4))) + while True: + await evt.wait() # Multiple tasks may wait on an Event + evt.clear() # It must be cleared. + yr, mo, md, h, m, s, wd = localtime()[:7] + print(f"Event {h:02d}:{m:02d}:{s:02d} on {md:02d}/{mo:02d}/{yr}") + +try: + asyncio.run(main()) +finally: + _ = asyncio.new_event_loop() +``` +See [tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#32-event). +Also [this doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/EVENTS.md) +for a discussion of event-based programming. + +##### [Top](./SCHEDULE.md#0-contents) + # 5. The cron object This is the core of the scheduler. Users of `asyncio` do not need to concern @@ -450,9 +509,9 @@ def wait_for(**kwargs): # 8. The simulate script -This enables the behaviour of sets of args to `schedule` to be rapidly checked. -The `sim` function should be adapted to reflect the application specifics. The -default is: +In `sched/simulate.py`. This enables the behaviour of sets of args to `schedule` +to be rapidly checked. The `sim` function should be adapted to reflect the +application specifics. The default is: ```python def sim(*args): set_time(*args) From 024b0802dcbdb301e89721b5d5ba71731a049691 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 24 Nov 2023 10:56:24 +0000 Subject: [PATCH 268/305] Schedule: Improve doc, code comments. --- v3/as_drivers/sched/sched.py | 3 ++- v3/docs/SCHEDULE.md | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/v3/as_drivers/sched/sched.py b/v3/as_drivers/sched/sched.py index e67a6df..7340f38 100644 --- a/v3/as_drivers/sched/sched.py +++ b/v3/as_drivers/sched/sched.py @@ -11,7 +11,8 @@ # uasyncio can't handle long delays so split into 1000s (1e6 ms) segments _MAXT = const(1000) -# Wait prior to a sequence start +# Wait prior to a sequence start: see +# https://github.com/peterhinch/micropython-async/blob/master/v3/docs/SCHEDULE.md#71-initialisation _PAUSE = const(2) diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index a86bd6e..6abe731 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -115,7 +115,9 @@ asyncio.create_task(schedule(foo, 'every 4 mins', hrs=None, mins=range(0, 60, 4) Positional args: 1. `func` This may be a callable (callback or coroutine) to run, a user defined `Event` or an instance of a `Sequence`. - 2. Any further positional args are passed to the callable or the `Sequence`. + 2. Any further positional args are passed to the callable or the `Sequence`; + these args can be used to enable the triggered object to determine the source + of the trigger. Keyword-only args. Args 1..6 are [Time specifiers](./SCHEDULE.md#41-time-specifiers): a variety of data types @@ -168,6 +170,9 @@ try: finally: _ = asyncio.new_event_loop() ``` +Note that the asynchronous iterator produces a `tuple` of the args passed to the +`schedule` that triggered it. This enables the code to determine the source of +the trigger. ##### [Top](./SCHEDULE.md#0-contents) From e815e9bf8638a1e54cc3b58cd810d679f49a5619 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 27 Nov 2023 16:44:07 +0000 Subject: [PATCH 269/305] auart.py: Add code comment re timeout. --- v3/as_demos/auart.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/v3/as_demos/auart.py b/v3/as_demos/auart.py index f00aa82..9119f41 100644 --- a/v3/as_demos/auart.py +++ b/v3/as_demos/auart.py @@ -3,22 +3,27 @@ # Copyright Peter Hinch 2017-2022 Released under the MIT license # Link X1 and X2 to test. +# We run with no UART timeout: UART read never blocks. import uasyncio as asyncio from machine import UART + uart = UART(4, 9600, timeout=0) + async def sender(): swriter = asyncio.StreamWriter(uart, {}) while True: - swriter.write('Hello uart\n') + swriter.write("Hello uart\n") await swriter.drain() await asyncio.sleep(2) + async def receiver(): sreader = asyncio.StreamReader(uart) while True: res = await sreader.readline() - print('Received', res) + print("Received", res) + async def main(): asyncio.create_task(sender()) @@ -26,13 +31,15 @@ async def main(): while True: await asyncio.sleep(1) + def test(): try: asyncio.run(main()) except KeyboardInterrupt: - print('Interrupted') + print("Interrupted") finally: asyncio.new_event_loop() - print('as_demos.auart.test() to run again.') + print("as_demos.auart.test() to run again.") + test() From 6a616bac97f986e58b134580a61d0fadb92b9276 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 11 Dec 2023 11:14:36 +0000 Subject: [PATCH 270/305] SCHEDULE.md: Add reference to astronomy doc. --- v3/docs/SCHEDULE.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/v3/docs/SCHEDULE.md b/v3/docs/SCHEDULE.md index 6abe731..ca4ff18 100644 --- a/v3/docs/SCHEDULE.md +++ b/v3/docs/SCHEDULE.md @@ -21,6 +21,8 @@ 8. [The simulate script](./SCHEDULE.md#8-the-simulate-script) Rapidly test sequences. Release note: +11th Dec 2023 Document astronomy module, allowing scheduling based on Sun and +Moon rise and set times. 23rd Nov 2023 Add asynchronous iterator interface. 3rd April 2023 Fix issue #100. Where an iterable is passed to `secs`, triggers must now be at least 10s apart (formerly 2s). @@ -48,6 +50,10 @@ also be used in synchronous code and an example is provided. It is cross-platform and has been tested on Pyboard, Pyboard D, ESP8266, ESP32 and the Unix build. +The `astronomy` module extends this to enable tasks to be scheduled at times +related to Sun and Moon rise and set times. This is documented +[here](https://github.com/peterhinch/micropython-samples/blob/master/astronomy/README.md). + # 2. Overview The `schedule` coroutine (`sched/sched.py`) is the interface for use with @@ -103,6 +109,12 @@ The following files are installed in the `sched` directory. The `crontest` script is only of interest to those wishing to adapt `cron.py`. It will run on any MicroPython target. +The [astronomy](https://github.com/peterhinch/micropython-samples/blob/master/astronomy/README.md) +module may be installed with +```bash +$ mpremote mip install "github:peterhinch/micropython-samples/astronomy" +``` + # 4. The schedule coroutine This enables a response to be triggered at intervals. The response can be From 36503b3e48cb6127bf9e4fbff8304c1a7c8cd668 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 27 Jan 2024 12:30:57 +0000 Subject: [PATCH 271/305] pushbutton.py: Fix iss 115. --- v3/docs/DRIVERS.md | 5 ++++- v3/primitives/pushbutton.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 226cda9..8d15569 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -423,11 +423,14 @@ Methods 1 - 4 may be called at any time. If `False` is passed for a callable, any existing callback will be disabled. If `None` is passed, a bound `Event` is created. See below for `Event` names. -Class attributes: +Class variables: 1. `debounce_ms` Debounce time in ms. Default 50. 2. `long_press_ms` Threshold time in ms for a long press. Default 1000. 3. `double_click_ms` Threshold time in ms for a double-click. Default 400. + If these variables are changed, it should be done prior to instantiating the + class. + A simple Pyboard demo: ```python from pyb import LED diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index 9a072ae..1543dbf 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -78,7 +78,7 @@ def _check(self, state): def _ddto(self): # Doubleclick timeout: no doubleclick occurred self._dblpend = False - if self._supp and not self._state: + if self._ff and self._supp and not self._state: if not self._ld or (self._ld and not self._ld()): launch(self._ff, self._fa) From d5edede2afb5f17e1988a5c6e429030600be60dc Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 27 Jan 2024 12:45:05 +0000 Subject: [PATCH 272/305] pushbutton.py: Fix iss 115. --- v3/docs/DRIVERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 8d15569..42f764a 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -429,7 +429,7 @@ Class variables: 3. `double_click_ms` Threshold time in ms for a double-click. Default 400. If these variables are changed, it should be done prior to instantiating the - class. + class. The double click time must be less than the long press time. A simple Pyboard demo: ```python From ccc21a076d1907eec889641598dc727bcc2cfd9a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 8 Feb 2024 13:53:58 +0000 Subject: [PATCH 273/305] events.py: Add ELO class. --- v3/docs/EVENTS.md | 121 ++++++++++++++++++++++++++++++-- v3/primitives/__init__.py | 7 ++ v3/primitives/events.py | 68 ++++++++++++++++-- v3/primitives/tests/elo_test.py | 100 ++++++++++++++++++++++++++ 4 files changed, 283 insertions(+), 13 deletions(-) create mode 100644 v3/primitives/tests/elo_test.py diff --git a/v3/docs/EVENTS.md b/v3/docs/EVENTS.md index 63337f4..7776fab 100644 --- a/v3/docs/EVENTS.md +++ b/v3/docs/EVENTS.md @@ -20,9 +20,10 @@ This document assumes familiarity with `asyncio`. See [official docs](http://doc 5.1 [Use of Delay_ms](./EVENTS.md#51-use-of-delay_ms) A retriggerable delay 5.2 [Long and very long button press](./EVENTS.md#52-long-and-very-long-button-press) 5.3 [Application example](./EVENTS.md#53-application-example) - 6. [Drivers](./EVENTS.md#6-drivers) Minimal Event-based drivers - 6.1 [ESwitch](./EVENTS.md#61-eswitch) Debounced switch - 6.2 [EButton](./EVENTS.md#62-ebutton) Debounced pushbutton with double and long press events + 6. [ELO class](./EVENTS.md#6-elo-class) Convert a coroutine or task to an event-like object. + 7. [Drivers](./EVENTS.md#7-drivers) Minimal Event-based drivers + 7.1 [ESwitch](./EVENTS.md#71-eswitch) Debounced switch + 7.2 [EButton](./EVENTS.md#72-ebutton) Debounced pushbutton with double and long press events [Appendix 1 Polling](./EVENTS.md#100-appendix-1-polling) @@ -61,6 +62,11 @@ Users only need to know the names of the bound `Event` instances. By contast there is no standard way to specify callbacks, to define the passing of callback arguments or to define how to retrieve their return values. +There are other ways to define an API without callbacks, notably the stream +mechanism and the use of asynchronous iterators with `async for`. This doc +discusses the `Event` based approach which is ideal for sporadic occurrences +such as responding to user input. + ###### [Contents](./EVENTS.md#0-contents) # 2. Rationale @@ -135,6 +141,10 @@ ELO examples are: | [Delay_ms][2m] | Y | Y | Y | Self-setting | | [WaitAll](./EVENTS.md#42-waitall) | Y | Y | N | See below | | [WaitAny](./EVENTS.md#41-waitany) | Y | Y | N | | +| [ELO instances](./EVENTS.md#44-elo-class) | Y | N | N | | + +The `ELO` class converts coroutines or `Task` instances to event-like objects, +allowing them to be included in the arguments of event based primitives. Drivers exposing `Event` instances include: @@ -316,19 +326,118 @@ async def foo(): else: # Normal outcome, process readings ``` +###### [Contents](./EVENTS.md#0-contents) + +# 6. ELO class + +This converts a task to an "event-like object", enabling tasks to be included in +`WaitAll` and `WaitAny` arguments. An `ELO` instance is a wrapper for a `Task` +instance and its lifetime is that of its `Task`. The constructor can take a +coroutine or a task as its first argument; in the former case the coro is +converted to a `Task`. + +#### Constructor args + +1. `coro` This may be a coroutine or a `Task` instance. +2. `*args` Positional args for a coroutine (ignored if a `Task` is passed). +3. `**kwargs` Keyword args for a coroutine (ignored if a `Task` is passed). + +If a coro is passed it is immediately converted to a `Task` and scheduled for +execution. + +#### Asynchronous method + +1. `wait` Pauses until the `Task` is complete or is cancelled. In the latter +case no exception is thrown. + +#### Synchronous method + +1. `__call__` Returns the instance's `Task`. If the instance's `Task` was +cancelled the `CancelledError` exception is returned. The function call operator +allows a running task to be accessed, e.g. for cancellation. It also enables return values to be +retrieved. + +#### Usage example + +In most use cases an `ELO` instance is a throw-away object which allows a coro +to participate in an event-based primitive: +```python +evt = asyncio.Event() +async def my_coro(t): + await asyncio.wait(t) + +async def foo(): # Puase until the event has been triggered and coro has completed + await WaitAll((evt, ELO(my_coro, 5))).wait() # Note argument passing +``` +#### Retrieving results + +A task may return a result on completion. This may be accessed by awaiting the +`ELO` instance's `Task`. A reference to the `Task` may be acquired with function +call syntax. The following code fragment illustrates usage. It assumes that +`task` has already been created, and that `my_coro` is a coroutine taking an +integer arg. There is an `EButton` instance `ebutton` and execution pauses until +tasks have run to completion and the button has been pressed. +```python +async def foo(): + elos = (ELO(my_coro, 5), ELO(task)) + events = (ebutton.press,) + await WaitAll(elos + events).wait() + for e in elos: # Retrieve results from each task + r = await e() # Works even though task has already completed + print(r) +``` +This works because it is valid to `await` a task which has already completed. +The `await` returns immediately with the result. If `WaitAny` were used an `ELO` +instance might contain a running task. In this case the line +```python +r = await e() +``` +would pause before returning the result. + +#### Cancellation + +The `Task` in `ELO` instance `elo` may be retrieved by issuing `elo()`. For +example the following will subject an `ELO` instance to a timeout: +```python +async def elo_timeout(elo, t): + await asyncio.sleep(t) + elo().cancel() # Retrieve the Task and cancel it + +async def foo(): + elo = ELO(my_coro, 5) + asyncio.create_task(elo_timeout(2)) + await WaitAll((elo, ebutton.press)).wait() # Until button press and ELO either finished or timed out +``` +If the `ELO` task is cancelled, `.wait` terminates; the exception is retained. +Thus `WaitAll` or `WaitAny` behaves as if the task had terminated normally. A +subsequent call to `elo()` will return the exception. In an application +where the task might return a result or be cancelled, the following may be used: +```python +async def foo(): + elos = (ELO(my_coro, 5), ELO(task)) + events = (ebutton.press,) + await WaitAll(elos + events).wait() + for e in elos: # Check each task + t = e() + if isinstance(t, asyncio.CancelledError): + # Handle exception + else: # Retrieve results + r = await t # Works even though task has already completed + print(r) +``` ###### [Contents](./EVENTS.md#0-contents) -# 6. Drivers +# 7. Drivers The following device drivers provide an `Event` based interface for switches and pushbuttons. -## 6.1 ESwitch +## 7.1 ESwitch This is now documented [here](./DRIVERS.md#31-eswitch-class). -## 6.2 EButton +## 7.2 EButton This is now documented [here](./DRIVERS.md#41-ebutton-class). diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py index 523a13a..5b09a57 100644 --- a/v3/primitives/__init__.py +++ b/v3/primitives/__init__.py @@ -11,6 +11,8 @@ async def _g(): pass + + type_coro = type(_g()) # If a callback is passed, run it and return. @@ -22,14 +24,18 @@ def launch(func, tup_args): res = asyncio.create_task(res) return res + def set_global_exception(): def _handle_exception(loop, context): import sys + sys.print_exception(context["exception"]) sys.exit() + loop = asyncio.get_event_loop() loop.set_exception_handler(_handle_exception) + _attrs = { "AADC": "aadc", "Barrier": "barrier", @@ -44,6 +50,7 @@ def _handle_exception(loop, context): "Switch": "switch", "WaitAll": "events", "WaitAny": "events", + "ELO": "events", "ESwitch": "events", "EButton": "events", "RingbufQueue": "ringbuf_queue", diff --git a/v3/primitives/events.py b/v3/primitives/events.py index 8fe436e..a66274b 100644 --- a/v3/primitives/events.py +++ b/v3/primitives/events.py @@ -1,6 +1,6 @@ # events.py Event based primitives -# Copyright (c) 2022 Peter Hinch +# Copyright (c) 2022-2024 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file import uasyncio as asyncio @@ -34,9 +34,10 @@ def event(self): return self.trig_event def clear(self): - for evt in (x for x in self.events if hasattr(x, 'clear')): + for evt in (x for x in self.events if hasattr(x, "clear")): evt.clear() + # An Event-like class that can wait on an iterable of Event-like instances, # .wait pauses until all passed events have been set. class WaitAll: @@ -46,6 +47,7 @@ def __init__(self, events): async def wait(self): async def wt(event): await event.wait() + tasks = (asyncio.create_task(wt(event)) for event in self.events) try: await asyncio.gather(*tasks) @@ -54,15 +56,65 @@ async def wt(event): task.cancel() def clear(self): - for evt in (x for x in self.events if hasattr(x, 'clear')): + for evt in (x for x in self.events if hasattr(x, "clear")): evt.clear() + +# Convert to an event-like object: either a running task or a coro with args. +# Motivated by a suggestion from @sandyscott iss #116 +class ELO_x: + def __init__(self, coro, *args, **kwargs): + self._coro = coro + self._args = args + self._kwargs = kwargs + self._task = None # Current running task (or exception) + + async def wait(self): + cr = self._coro + istask = isinstance(cr, asyncio.Task) # Instantiated with a Task + if istask and isinstance(self._task, asyncio.CancelledError): + return # Previously awaited and was cancelled/timed out + self._task = cr if istask else asyncio.create_task(cr(*self._args, **self._kwargs)) + try: + await self._task + except asyncio.CancelledError as e: + self._task = e # Let WaitAll or WaitAny complete + + # User can retrieve task/coro results by awaiting .task() (even if task had + # run to completion). If task was cancelled CancelledError is returned. + # If .task() is called before .wait() returns None or result of prior .wait() + # Caller issues isinstance(task, CancelledError) + def task(self): + return self._task + + +# Convert to an event-like object: either a running task or a coro with args. +# Motivated by a suggestion from @sandyscott iss #116 +class ELO: + def __init__(self, coro, *args, **kwargs): + tsk = isinstance(coro, asyncio.Task) # Instantiated with a Task + self._task = coro if tsk else asyncio.create_task(coro(*args, **kwargs)) + + async def wait(self): + try: + await self._task + except asyncio.CancelledError as e: + self._task = e # Let WaitAll or WaitAny complete + + # User can retrieve task/coro results by awaiting elo() (even if task had + # run to completion). If task was cancelled CancelledError is returned. + # If .task() is called before .wait() returns None or result of prior .wait() + # Caller issues isinstance(task, CancelledError) + def __call__(self): + return self._task + + # Minimal switch class having an Event based interface class ESwitch: debounce_ms = 50 def __init__(self, pin, lopen=1): # Default is n/o switch returned to gnd - self._pin = pin # Should be initialised for input with pullup + self._pin = pin # Should be initialised for input with pullup self._lopen = lopen # Logic level in "open" state self.open = asyncio.Event() self.close = asyncio.Event() @@ -92,6 +144,7 @@ def deinit(self): self.open.clear() self.close.clear() + # Minimal pushbutton class having an Event based interface class EButton: debounce_ms = 50 # Attributes can be varied by user @@ -103,13 +156,14 @@ def __init__(self, pin, suppress=False, sense=None): self._supp = suppress self._sense = pin() if sense is None else sense self._state = self.rawstate() # Initial logical state - self._ltim = Delay_ms(duration = EButton.long_press_ms) - self._dtim = Delay_ms(duration = EButton.double_click_ms) + self._ltim = Delay_ms(duration=EButton.long_press_ms) + self._dtim = Delay_ms(duration=EButton.double_click_ms) self.press = asyncio.Event() # *** API *** self.double = asyncio.Event() self.long = asyncio.Event() self.release = asyncio.Event() # *** END API *** - self._tasks = [asyncio.create_task(self._poll(EButton.debounce_ms))] # Tasks run forever. Poll contacts + # Tasks run forever. Poll contacts + self._tasks = [asyncio.create_task(self._poll(EButton.debounce_ms))] self._tasks.append(asyncio.create_task(self._ltf())) # Handle long press if suppress: self._tasks.append(asyncio.create_task(self._dtf())) # Double timer diff --git a/v3/primitives/tests/elo_test.py b/v3/primitives/tests/elo_test.py new file mode 100644 index 0000000..8ebe4bd --- /dev/null +++ b/v3/primitives/tests/elo_test.py @@ -0,0 +1,100 @@ +# elo_test.py Test ELO class + +# Copyright (c) 2024 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +# from primitives.tests.elo_test import test +# test() + +import asyncio +from primitives import WaitAny, WaitAll, ELO + +evt = asyncio.Event() + + +def set_after(t): + async def ta(t): + await asyncio.sleep(t) + print("set") + evt.set() + evt.clear() + + asyncio.create_task(ta(t)) + + +def can_after(elo, t): + async def ca(elo, t): + await asyncio.sleep(t) + elo().cancel() + + asyncio.create_task(ca(elo, t)) + + +async def foo(t, n=42): + await asyncio.sleep(t) + return n + + +async def main(): + txt = """\x1b[32m +Expected output: + +Test cancellation. +Canned +Test return of value. +Result: 42 +Instantiate with running task +Result: 99 +Delayed return of value. +Result: 88 +\x1b[39m +""" + print(txt) + entries = (evt, elo := ELO(foo, 5)) + print("Test cancellation.") + can_after(elo, 1) + await WaitAny(entries).wait() + task = elo() + if isinstance(task, asyncio.CancelledError): + print("Canned") + + print("Test return of value.") + entries = (evt, elo := ELO(foo, 5)) + await WaitAny(entries).wait() + res = await elo() + print(f"Result: {res}") + + print("Instantiate with running task") + elo = ELO(task := asyncio.create_task(foo(3, 99))) + await WaitAny((elo, evt)).wait() + res = await task + print(f"Result: {res}") + + print("Delayed return of value.") + entries = (evt, elo := ELO(foo, 5, 88)) + await WaitAny(entries).wait() + set_after(1) # Early exit + res = await elo() # Pause until complete + print(f"Result: {res}") + + +def tests(): + txt = """ +\x1b[32m +Issue: +from primitives.tests.elo_test import test +test() +\x1b[39m +""" + print(txt) + + +def test(): + try: + asyncio.run(main()) + finally: + asyncio.new_event_loop() + tests() + + +tests() From 99421dcceefe8f039a1776bb1fc68f87ed085b91 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 8 Feb 2024 15:23:48 +0000 Subject: [PATCH 274/305] TUTORIAL: Remove task groups. A wish unfulfilled... --- v3/docs/TUTORIAL.md | 100 ++++++++------------------------------------ 1 file changed, 17 insertions(+), 83 deletions(-) diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index ed5bbc2..0caf44c 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -37,7 +37,6 @@ import uasyncio as asyncio      3.2.1 [Wait on multiple events](./TUTORIAL.md#321-wait-on-multiple-events) Pause until 1 of N events is set. 3.3 [Coordinating multiple tasks](./TUTORIAL.md#33-coordinating-multiple-tasks)      3.3.1 [gather](./TUTORIAL.md#331-gather) -      3.3.2 [TaskGroups](./TUTORIAL.md#332-taskgroups) Not yet in official build. 3.4 [Semaphore](./TUTORIAL.md#34-semaphore)      3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore) 3.5 [Queue](./TUTORIAL.md#35-queue) @@ -134,6 +133,10 @@ mip.install("github:peterhinch/micropython-async/v3/threadsafe") ``` For non-networked targets use `mpremote` as described in [the official docs](http://docs.micropython.org/en/latest/reference/packages.html#installing-packages-with-mpremote). +```bash +$ mpremote mip install github:peterhinch/micropython-async/v3/primitives +$ mpremote mip install github:peterhinch/micropython-async/v3/threadsafe +``` ###### [Main README](../README.md) @@ -276,7 +279,7 @@ line `main.py` and runs forever. ## 2.2 Coroutines and Tasks -The fundmental building block of `asyncio` is a coro. This is defined with +The fundamental building block of `asyncio` is a coro. This is defined with `async def` and usually contains at least one `await` statement. This minimal example waits 1 second before printing a message: @@ -285,12 +288,16 @@ async def bar(): await asyncio.sleep(1) print('Done') ``` - -V3 `asyncio` introduced the concept of a `Task`. A `Task` instance is created -from a coro by means of the `create_task` method, which causes the coro to be -scheduled for execution and returns a `Task` instance. In many cases, coros and -tasks are interchangeable: the official docs refer to them as `awaitable`, for -the reason that either of them may be the target of an `await`. Consider this: +Just as a function does nothing until called, a coro does nothing until awaited +or converted to a `Task`. The `create_task` method takes a coro as its argument +and returns a `Task` instance, which is scheduled for execution. In +```python +async def foo(): + await coro +``` +`coro` is run with `await` pausing until `coro` has completed. Sometimes coros +and tasks are interchangeable: the CPython docs refer to them as `awaitable`, +because either may be the target of an `await`. Consider this: ```python import asyncio @@ -856,79 +863,6 @@ async def main(): asyncio.run(main()) ``` -### 3.3.2 TaskGroups - -The `TaskGroup` class is unofficially provided by -[this PR](https://github.com/micropython/micropython/pull/8791). It is well -suited to applications where one or more of a group of tasks is subject to -runtime exceptions. A `TaskGroup` is instantiated in an asynchronous context -manager. The `TaskGroup` instantiates member tasks. When all have run to -completion, the context manager terminates. Where `gather` is static, a task -group can be dynamic: a task in a group may spawn further group members. Return -values from member tasks cannot be retrieved. Results should be passed in other -ways such as via bound variables, queues etc. - -An exception in a member task not trapped by that task is propagated to the -task that created the `TaskGroup`. All tasks in the `TaskGroup` then terminate -in an orderly fashion: cleanup code in any `finally` clause will run. When all -cleanup code has completed, the context manager completes, and execution passes -to an exception handler in an outer scope. - -If a member task is cancelled in code, that task terminates in an orderly way -but the other members continue to run. - -The following illustrates the basic salient points of using a `TaskGroup`: -```python -import asyncio -async def foo(n): - for x in range(10 + n): - print(f"Task {n} running.") - await asyncio.sleep(1 + n/10) - print(f"Task {n} done") - -async def main(): - async with asyncio.TaskGroup() as tg: # Context manager pauses until members terminate - for n in range(4): - tg.create_task(foo(n)) # tg.create_task() creates a member task - print("TaskGroup done") # All tasks have terminated - -asyncio.run(main()) -``` -This more complete example illustrates an exception which is not trapped by the -member task. Cleanup code on all members runs when the exception occurs, -followed by exception handling code in `main()`. -```python -import asyncio -fail = True # Set False to demo normal completion -async def foo(n): - print(f"Task {n} running...") - try: - for x in range(10 + n): - await asyncio.sleep(1 + n/10) - if n==0 and x==5 and fail: - raise OSError("Uncaught exception in task.") - print(f"Task {n} done") - finally: - print(f"Task {n} cleanup") - -async def main(): - try: - async with asyncio.TaskGroup() as tg: - for n in range(4): - tg.create_task(foo(n)) - print("TaskGroup done") # Does not get here if a task throws exception - except Exception as e: - print(f'TaskGroup caught exception: "{e}"') - finally: - print("TaskGroup finally") - -asyncio.run(main()) -``` -[This doc](https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/) -provides background on the theory behind task groups and how they can improve -program structure and reliablity. - -###### [Contents](./TUTORIAL.md#contents) ## 3.4 Semaphore @@ -2061,7 +1995,7 @@ asyncio.run(main()) ``` The `.readline` method will pause until `\n` is received. -###### StreamWriter write methods +##### StreamWriter write methods Writing to a `StreamWriter` occurs in two stages. The synchronous `.write` method concatenates data for later transmission. The asynchronous `.drain` @@ -2078,7 +2012,7 @@ following methods: `ioctl`, `read`, `readline` and `write`. See [Writing streaming device drivers](./TUTORIAL.md#64-writing-streaming-device-drivers) for details on how such drivers may be written in Python. -###### StreamReader read methods +##### StreamReader read methods The `StreamReader` read methods fall into two categories depending on whether they wait for a specific end condition. Thus `.readline` pauses until a newline From e7a47529bbfd7da902492db4cb85f992fd05b9c3 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 16 Feb 2024 13:55:24 +0000 Subject: [PATCH 275/305] Remove obsolete V2. Add StreamReader timeout demo and tutorial text. --- v2/DRIVERS.md | 309 ----- v2/FASTPOLL.md | 556 --------- v2/HD44780/README.md | 93 -- v2/HD44780/alcd.py | 108 -- v2/HD44780/alcdtest.py | 19 - v2/PRIMITIVES.md | 792 ------------- v2/README.md | 166 --- v2/TUTORIAL.md | 2031 -------------------------------- v2/UNDER_THE_HOOD.md | 377 ------ v2/aledflash.py | 32 - v2/apoll.py | 64 - v2/aqtest.py | 35 - v2/astests.py | 132 --- v2/aswitch.py | 231 ---- v2/asyn.py | 470 -------- v2/asyn_demos.py | 131 -- v2/asyntest.py | 426 ------- v2/auart.py | 25 - v2/auart_hd.py | 106 -- v2/awaitable.py | 32 - v2/benchmarks/call_lp.py | 43 - v2/benchmarks/latency.py | 123 -- v2/benchmarks/overdue.py | 40 - v2/benchmarks/priority_test.py | 78 -- v2/benchmarks/rate.py | 48 - v2/benchmarks/rate_esp.py | 50 - v2/benchmarks/rate_fastio.py | 48 - v2/cantest.py | 422 ------- v2/chain.py | 20 - v2/check_async_code.py | 206 ---- v2/client_server/heartbeat.py | 26 - v2/client_server/uclient.py | 51 - v2/client_server/userver.py | 64 - v2/fast_io/__init__.py | 377 ------ v2/fast_io/core.py | 462 -------- v2/fast_io/fast_can_test.py | 67 -- v2/fast_io/iorw_can.py | 140 --- v2/fast_io/iorw_to.py | 143 --- v2/fast_io/ms_timer.py | 33 - v2/fast_io/ms_timer_test.py | 45 - v2/fast_io/pin_cb.py | 47 - v2/fast_io/pin_cb_test.py | 68 -- v2/gps/LICENSE | 21 - v2/gps/README.md | 917 -------------- v2/gps/as_GPS.py | 614 ---------- v2/gps/as_GPS_time.py | 173 --- v2/gps/as_GPS_utils.py | 48 - v2/gps/as_rwGPS.py | 118 -- v2/gps/as_rwGPS_time.py | 237 ---- v2/gps/as_tGPS.py | 241 ---- v2/gps/ast_pb.py | 101 -- v2/gps/ast_pbrw.py | 173 --- v2/gps/astests.py | 178 --- v2/gps/astests_pyb.py | 150 --- v2/gps/log.kml | 128 -- v2/gps/log_kml.py | 77 -- v2/htu21d/README.md | 50 - v2/htu21d/htu21d_mc.py | 64 - v2/htu21d/htu_test.py | 34 - v2/i2c/README.md | 420 ------- v2/i2c/asi2c.py | 206 ---- v2/i2c/asi2c_i.py | 142 --- v2/i2c/i2c_esp.py | 69 -- v2/i2c/i2c_init.py | 81 -- v2/i2c/i2c_resp.py | 68 -- v2/io.py | 39 - v2/iorw.py | 101 -- v2/lowpower/README.md | 496 -------- v2/lowpower/current.png | Bin 9794 -> 0 bytes v2/lowpower/current1.png | Bin 10620 -> 0 bytes v2/lowpower/lp_uart.py | 53 - v2/lowpower/lpdemo.py | 75 -- v2/lowpower/mqtt_log.py | 63 - v2/lowpower/rtc_time.py | 166 --- v2/lowpower/rtc_time_cfg.py | 5 - v2/nec_ir/README.md | 133 --- v2/nec_ir/aremote.py | 124 -- v2/nec_ir/art.py | 47 - v2/nec_ir/art1.py | 60 - v2/roundrobin.py | 37 - v2/sock_nonblock.py | 110 -- v2/syncom_as/README.md | 242 ---- v2/syncom_as/main.py | 4 - v2/syncom_as/sr_init.py | 86 -- v2/syncom_as/sr_passive.py | 64 - v2/syncom_as/syncom.py | 246 ---- v3/as_demos/stream_to.py | 74 ++ v3/docs/TUTORIAL.md | 43 +- 88 files changed, 114 insertions(+), 15200 deletions(-) delete mode 100644 v2/DRIVERS.md delete mode 100644 v2/FASTPOLL.md delete mode 100644 v2/HD44780/README.md delete mode 100644 v2/HD44780/alcd.py delete mode 100644 v2/HD44780/alcdtest.py delete mode 100644 v2/PRIMITIVES.md delete mode 100644 v2/README.md delete mode 100644 v2/TUTORIAL.md delete mode 100644 v2/UNDER_THE_HOOD.md delete mode 100644 v2/aledflash.py delete mode 100644 v2/apoll.py delete mode 100644 v2/aqtest.py delete mode 100644 v2/astests.py delete mode 100644 v2/aswitch.py delete mode 100644 v2/asyn.py delete mode 100644 v2/asyn_demos.py delete mode 100644 v2/asyntest.py delete mode 100644 v2/auart.py delete mode 100644 v2/auart_hd.py delete mode 100644 v2/awaitable.py delete mode 100644 v2/benchmarks/call_lp.py delete mode 100644 v2/benchmarks/latency.py delete mode 100644 v2/benchmarks/overdue.py delete mode 100644 v2/benchmarks/priority_test.py delete mode 100644 v2/benchmarks/rate.py delete mode 100644 v2/benchmarks/rate_esp.py delete mode 100644 v2/benchmarks/rate_fastio.py delete mode 100644 v2/cantest.py delete mode 100644 v2/chain.py delete mode 100755 v2/check_async_code.py delete mode 100644 v2/client_server/heartbeat.py delete mode 100644 v2/client_server/uclient.py delete mode 100644 v2/client_server/userver.py delete mode 100644 v2/fast_io/__init__.py delete mode 100644 v2/fast_io/core.py delete mode 100644 v2/fast_io/fast_can_test.py delete mode 100644 v2/fast_io/iorw_can.py delete mode 100644 v2/fast_io/iorw_to.py delete mode 100644 v2/fast_io/ms_timer.py delete mode 100644 v2/fast_io/ms_timer_test.py delete mode 100644 v2/fast_io/pin_cb.py delete mode 100644 v2/fast_io/pin_cb_test.py delete mode 100644 v2/gps/LICENSE delete mode 100644 v2/gps/README.md delete mode 100644 v2/gps/as_GPS.py delete mode 100644 v2/gps/as_GPS_time.py delete mode 100644 v2/gps/as_GPS_utils.py delete mode 100644 v2/gps/as_rwGPS.py delete mode 100644 v2/gps/as_rwGPS_time.py delete mode 100644 v2/gps/as_tGPS.py delete mode 100644 v2/gps/ast_pb.py delete mode 100644 v2/gps/ast_pbrw.py delete mode 100755 v2/gps/astests.py delete mode 100755 v2/gps/astests_pyb.py delete mode 100644 v2/gps/log.kml delete mode 100644 v2/gps/log_kml.py delete mode 100644 v2/htu21d/README.md delete mode 100644 v2/htu21d/htu21d_mc.py delete mode 100644 v2/htu21d/htu_test.py delete mode 100644 v2/i2c/README.md delete mode 100644 v2/i2c/asi2c.py delete mode 100644 v2/i2c/asi2c_i.py delete mode 100644 v2/i2c/i2c_esp.py delete mode 100644 v2/i2c/i2c_init.py delete mode 100644 v2/i2c/i2c_resp.py delete mode 100644 v2/io.py delete mode 100644 v2/iorw.py delete mode 100644 v2/lowpower/README.md delete mode 100644 v2/lowpower/current.png delete mode 100644 v2/lowpower/current1.png delete mode 100644 v2/lowpower/lp_uart.py delete mode 100644 v2/lowpower/lpdemo.py delete mode 100644 v2/lowpower/mqtt_log.py delete mode 100644 v2/lowpower/rtc_time.py delete mode 100644 v2/lowpower/rtc_time_cfg.py delete mode 100644 v2/nec_ir/README.md delete mode 100644 v2/nec_ir/aremote.py delete mode 100644 v2/nec_ir/art.py delete mode 100644 v2/nec_ir/art1.py delete mode 100644 v2/roundrobin.py delete mode 100644 v2/sock_nonblock.py delete mode 100644 v2/syncom_as/README.md delete mode 100644 v2/syncom_as/main.py delete mode 100644 v2/syncom_as/sr_init.py delete mode 100644 v2/syncom_as/sr_passive.py delete mode 100644 v2/syncom_as/syncom.py create mode 100644 v3/as_demos/stream_to.py diff --git a/v2/DRIVERS.md b/v2/DRIVERS.md deleted file mode 100644 index 3359123..0000000 --- a/v2/DRIVERS.md +++ /dev/null @@ -1,309 +0,0 @@ -# 1. Introduction - -Drivers for switches and pushbuttons are provided, plus a retriggerable delay -class. The switch and button drivers support debouncing. The switch driver -provides for running a callback or launching a coroutine (coro) on contact -closure and/or opening. - -The pushbutton driver extends this to support long-press and double-click -events. - -# 2. Modules - - 1. `aledflash.py` Flashes the four Pyboard LED's asynchronously for 10s. The - simplest uasyncio demo. Import it to run. - 2. `aswitch.py` This provides classes for interfacing switches and pushbuttons - and also a software retriggerable delay object. Pushbuttons are a - generalisation of switches providing logical rather than physical status along - with double-clicked and long pressed events. - 3. `astests.py` Test/demonstration programs for `aswitch.py`. - -# 3. Module aswitch.py - -This module provides the following classes: - - * `Switch` This supports debouncing a normally open switch connected between - a pin and ground. Can run callbacks or schedule coros on contact closure - and/or opening. - * `Pushbutton` A generalisation of `Switch` to support normally open or - normally closed switches connected to ground or 3V3. Can run callbacks or - schedule coros on double-click or long press events. - * `Delay_ms` A class providing a retriggerable delay measured in ms. Can be - used to run a callback or to schedule a coro. Its state can be tested by any - coro. - -The module `astests.py` provides examples of usage. In the following text the -term **function** implies a Python `callable`: namely a function, bound method, -coroutine or bound coroutine interchangeably. - -### Timing - -The `Switch` class relies on millisecond-level timing: callback functions must -be designed to terminate rapidly. This applies to all functions in the -application; coroutines should yield regularly. If these constraints are not -met, switch events can be missed. - -## 3.1 Switch class - -This assumes a normally open switch connected between a pin and ground. The pin -should be initialised as an input with a pullup. A **function** may be -specified to run on contact closure or opening; where the **function** is a -coroutine it will be scheduled for execution and will run asynchronously. -Debouncing is implicit: contact bounce will not cause spurious execution of -these functions. - -Constructor argument (mandatory): - - 1. `pin` The initialised Pin instance. - -Methods: - - 1. `close_func` Args: `func` (mandatory) a **function** to run on contact - closure. `args` a tuple of arguments for the **function** (default `()`) - 2. `open_func` Args: `func` (mandatory) a **function** to run on contact open. - `args` a tuple of arguments for the **function** (default `()`) - 3. `__call__` Call syntax e.g. `myswitch()` returns the physical debounced - state of the switch i.e. 0 if grounded, 1 if connected to `3V3`. - -Methods 1 and 2 should be called before starting the scheduler. - -Class attribute: - 1. `debounce_ms` Debounce time in ms. Default 50. - -```python -from pyb import LED -from machine import Pin -import uasyncio as asyncio -from aswitch import Switch - -async def pulse(led, ms): - led.on() - await asyncio.sleep_ms(ms) - led.off() - -async def my_app(): - await asyncio.sleep(60) # Dummy application code - -pin = Pin('X1', Pin.IN, Pin.PULL_UP) # Hardware: switch to gnd -red = LED(1) -sw = Switch(pin) -sw.close_func(pulse, (red, 1000)) # Note how coro and args are passed -loop = asyncio.get_event_loop() -loop.run_until_complete(my_app()) # Run main application code -``` - -## 3.2 Pushbutton class - -This can support normally open or normally closed switches, connected to `gnd` -(with a pullup) or to `3V3` (with a pull-down). The `Pin` object should be -initialised appropriately. The assumption is that on initialisation the button -is not pressed. - -The Pushbutton class uses logical rather than physical state: a button's state -is considered `True` if pressed, otherwise `False` regardless of its physical -implementation. - -**function** instances may be specified to run on button press, release, double -click or long press events; where the **function** is a coroutine it will be -scheduled for execution and will run asynchronously. - -Please see the note on timing in section 3. - -Constructor arguments: - - 1. `pin` Mandatory. The initialised Pin instance. - 2. `suppress` Default `False`. See 3.2.1 below. - -Methods: - - 1. `press_func` Args: `func` (mandatory) a **function** to run on button push. - `args` a tuple of arguments for the **function** (default `()`). - 2. `release_func` Args: `func` (mandatory) a **function** to run on button - release. `args` a tuple of arguments for the **function** (default `()`). - 3. `long_func` Args: `func` (mandatory) a **function** to run on long button - push. `args` a tuple of arguments for the **function** (default `()`). - 4. `double_func` Args: `func` (mandatory) a **function** to run on double - push. `args` a tuple of arguments for the **function** (default `()`). - 5. `__call__` Call syntax e.g. `mybutton()` Returns the logical debounced - state of the button (`True` corresponds to pressed). - 6. `rawstate()` Returns the logical instantaneous state of the button. There - is probably no reason to use this. - -Methods 1 - 4 should be called before starting the scheduler. - -Class attributes: - 1. `debounce_ms` Debounce time in ms. Default 50. - 2. `long_press_ms` Threshold time in ms for a long press. Default 1000. - 3. `double_click_ms` Threshold time in ms for a double click. Default 400. - -```python -from pyb import LED -from machine import Pin -import uasyncio as asyncio -from aswitch import Pushbutton - -def toggle(led): - led.toggle() - -async def my_app(): - await asyncio.sleep(60) # Dummy - -pin = Pin('X1', Pin.IN, Pin.PULL_UP) # Pushbutton to gnd -red = LED(1) -pb = Pushbutton(pin) -pb.press_func(toggle, (red,)) # Note how function and args are passed -loop = asyncio.get_event_loop() -loop.run_until_complete(my_app()) # Run main application code -``` - -An alternative Pushbutton class with lower RAM usage is available -[here](https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py). - -### 3.2.1 The suppress constructor argument - -When the button is pressed `press_func` runs immediately. This minimal latency -is ideal for applications such as games, but does imply that in the event of a -long press, both `press_func` and `long_func` run: `press_func` immediately and -`long_func` if the button is still pressed when the timer has elapsed. Similar -reasoning applies to the double click function. - -There can be a need for a **function** which runs if a button is pressed but -only if a doubleclick or long press function does not run. The soonest that the -absence of a long press can be detected is on button release. The absence of a -double click can only be detected when the double click timer times out without -a second press occurring. - -This **function** is the `release_func`. If the `suppress` constructor arg is -set, `release_func` will be launched as follows: - 1. If `double_func` does not exist on rapid button release. - 2. If `double_func` exists, after the expiration of the doubleclick timer. - 3. If `long_func` exists and the press duration causes `long_func` to be - launched, `release_func` will not be launched. - 4. If `double_func` exists and a double click occurs, `release_func` will not - be launched. - -## 3.3 Delay_ms class - -This implements the software equivalent of a retriggerable monostable or a -watchdog timer. It has an internal boolean `running` state. When instantiated -the `Delay_ms` instance does nothing, with `running` `False` until triggered. -Then `running` becomes `True` and a timer is initiated. This can be prevented -from timing out by triggering it again (with a new timeout duration). So long -as it is triggered before the time specified in the preceeding trigger it will -never time out. - -If it does time out the `running` state will revert to `False`. This can be -interrogated by the object's `running()` method. In addition a **function** can -be specified to the constructor. This will execute when a timeout occurs; where -the **function** is a coroutine it will be scheduled for execution and will run -asynchronously. - -Constructor arguments (defaults in brackets): - - 1. `func` The **function** to call on timeout (default `None`). - 2. `args` A tuple of arguments for the **function** (default `()`). - 3. `can_alloc` Boolean, default `True`. See below. - 4. `duration` Integer, default 1000ms. The default timer period where no value - is passed to the `trigger` method. - -Methods: - - 1. `trigger` optional argument `duration=0`. A timeout will occur after - `duration` ms unless retriggered. If no arg is passed the period will be that - of the `duration` passed to the constructor. See Class variable below. - 2. `stop` No argument. Cancels the timeout, setting the `running` status - `False`. The timer can be restarted by issuing `trigger` again. - 3. `running` No argument. Returns the running status of the object. - 4. `__call__` Alias for running. - -Class variable: - - 1. `verbose=False` If `True` a warning will be printed if a running timer is - retriggered with a time value shorter than the time currently outstanding. - Such an operation has no effect owing to the design of `uasyncio`. - -If the `trigger` method is to be called from an interrupt service routine the -`can_alloc` constructor arg should be `False`. This causes the delay object -to use a slightly less efficient mode which avoids RAM allocation when -`trigger` runs. - -In this example a 3 second timer starts when the button is pressed. If it is -pressed repeatedly the timeout will not be triggered. If it is not pressed for -3 seconds the timeout triggers and the LED lights. - -```python -from pyb import LED -from machine import Pin -import uasyncio as asyncio -from aswitch import Pushbutton, Delay_ms - -async def my_app(): - await asyncio.sleep(60) # Run for 1 minute - -pin = Pin('X1', Pin.IN, Pin.PULL_UP) # Pushbutton to gnd -red = LED(1) -pb = Pushbutton(pin) -d = Delay_ms(lambda led: led.on(), (red,)) -pb.press_func(d.trigger, (3000,)) # Note how function and args are passed -loop = asyncio.get_event_loop() -loop.run_until_complete(my_app()) # Run main application code -``` - -# 4. Module astests.py - -This provides demonstration/test functions for the `Switch` and `Pushbutton` -classes. They assume a switch or button wired between pin X1 and gnd. Tests may -be terminated by grounding X2. - -## 4.1 Function test_sw() - -This will flash the red LED on switch closure, and the green LED on opening -and demonstrates the scheduling of coroutines. See section 5 for a discussion -of its behaviour if the switch is toggled rapidly. - -## 4.2 Function test_swcb() - -Demonstrates the use of callbacks to toggle the red and green LED's. - -## 4.3 Function test_btn(lpmode=False) - -This will flash the red LED on button push, and the green LED on release. A -long press will flash the blue LED and a double-press the yellow one. - -Test the launching of coroutines and also the `suppress` constructor arg. - -It takes three optional positional boolean args: - 1. `Suppresss=False` If `True` sets the `suppress` constructor arg. - 2. `lf=True` Declare a long press coro. - 3. `df=true` Declare a double click coro. - -The note below on race conditions applies. - -## 4.4 Function test_btncb() - -Demonstrates the use of callbacks. Toggles the red, green, yellow and blue -LED's on press, release, double-press and long press respectively. - -# 5 Race conditions - -Note that in the tests such as test_sw() where coroutines are scheduled by -events and the switch is cycled rapidly the LED behaviour may seem surprising. -This is because each time the switch is closed a coro is launched to flash the -red LED; on each open event one is launched for the green LED. With rapid -cycling a new coro instance will commence while one is still running against -the same LED. This type of conflict over a resource is known as a race -condition: in this instance it leads to the LED behaving erratically. - -This is a hazard of asynchronous programming. In some situations it is -desirable to launch a new instance on each button press or switch closure, even -if other instances are still incomplete. In other cases it can lead to a race -condition, leading to the need to code an interlock to ensure that the desired -behaviour occurs. The programmer must define the desired behaviour. - -In the case of this test program it might be to ignore events while a similar -one is running, or to extend the timer to prolong the LED illumination. -Alternatively a subsequent button press might be required to terminate the -illumination. The "right" behaviour is application dependent. - -A further consequence of scheduling new coroutine instances when one or more -are already running is that the `uasyncio` queue can fill causing an exception. diff --git a/v2/FASTPOLL.md b/v2/FASTPOLL.md deleted file mode 100644 index 2439342..0000000 --- a/v2/FASTPOLL.md +++ /dev/null @@ -1,556 +0,0 @@ -# fast_io: A modified version of uasyncio - -This version is a "drop in" replacement for official `uasyncio`. Existing -applications should run under it unchanged and with essentially identical -performance except that task cancellation and timeouts are expedited "soon" -rather than being deferred until the task is next scheduled. - -"Priority" features are only enabled if the event loop is instantiated with -specific arguments. - -This version has the following features relative to official V2.0: - * Timeouts and task cancellation are handled promptly, rather than being - deferred until the coroutine is next scheduled. - * I/O can optionally be handled at a higher priority than other coroutines - [PR287](https://github.com/micropython/micropython-lib/pull/287). - * Tasks can yield with low priority, running when nothing else is pending. - * Callbacks can similarly be scheduled with low priority. - * A [bug](https://github.com/micropython/micropython/pull/3836#issuecomment-397317408) - whereby bidirectional devices such as UARTS can fail to handle concurrent - input and output is fixed. - * It is compatible with `rtc_time.py` for micro-power applications documented - [here](./lowpower/README.md). This is a Pyboard-only extension (including - Pyboard D). - * An assertion failure is produced if `create_task` or `run_until_complete` - is called with a generator function - [PR292](https://github.com/micropython/micropython-lib/pull/292). This traps - a common coding error which otherwise results in silent failure. - * The presence and version of the `fast_io` version can be tested at runtime. - * The presence of an event loop instance can be tested at runtime. - * `run_until_complete(coro())` now returns the value returned by `coro()` as - per CPython - [micropython-lib PR270](https://github.com/micropython/micropython-lib/pull/270). - * The `StreamReader` class now has a `readinto(buf, n=0)` method to enable - allocations to be reduced. - -Note that priority device drivers are written by using the officially supported -technique for writing stream I/O drivers. Code using such drivers will run -unchanged under the `fast_io` version. Using the fast I/O mechanism requires -adding just one line of code. This implies that if official `uasyncio` acquires -a means of prioritising I/O other than that in this version, application code -changes should be minimal. - -#### Changes incompatible with prior versions - -V0.24 -The `version` bound variable now returns a 2-tuple. - -Prior versions. -The high priority mechanism formerly provided in `asyncio_priority.py` was a -workround based on the view that stream I/O written in Python would remain -unsupported. This is now available so `asyncio_priority.py` is obsolete and -should be deleted from your system. The facility for low priority coros -formerly provided by `asyncio_priority.py` is now implemented. - -###### [Main README](./README.md) - -# Contents - - 1. [Installation](./FASTPOLL.md#1-installation) - 1.1 [Benchmarks](./FASTPOLL.md#11-benchmarks) Benchmark and demo programs. - 2. [Rationale](./FASTPOLL.md#2-rationale) - 2.1 [Latency](./FASTPOLL.md#21-latency) - 2.2 [Timing accuracy](./FASTPOLL.md#22-timing-accuracy) - 2.3 [Polling in uasyncio](./FASTPOLL.md#23-polling-in-usayncio) - 3. [The modified version](./FASTPOLL.md#3-the-modified-version) - 3.1 [Fast IO](./FASTPOLL.md#31-fast-io) - 3.2 [Low Priority](./FASTPOLL.md#32-low-priority) - 3.3 [Other Features](./FASTPOLL.md#33-other-features) - 3.3.1 [Version](./FASTPOLL.md#331-version) - 3.3.2 [Check event loop status](./FASTPOLL.md#332-check-event-loop-status) - 3.3.3 [StreamReader readinto method](./FASTPOLL.md#333-streamreader-readinto-method) - 3.4 [Low priority yield](./FASTPOLL.md#34-low-priority-yield) - 3.4.1 [Task Cancellation and Timeouts](./FASTPOLL.md#341-task-cancellation-and-timeouts) - 3.5 [Low priority callbacks](./FASTPOLL.md#35-low-priority-callbacks) - 4. [ESP Platforms](./FASTPOLL.md#4-esp-platforms) - 5. [Background](./FASTPOLL.md#4-background) - 6. [Performance](./FASTPOLL.md#6-performance) - -# 1. Installation - -The basic approach is to install and test `uasyncio` on the target hardware. -Replace `core.py` and `__init__.py` with the files in the `fast_io` directory. - -The current MicroPython release build (1.10) has `uasyncio` implemented as a -frozen module. The following options for installing `fast_io` exist: - - 1. Use a daily build, install `uasyncio` as per the tutorial then replace the - above files. - 2. Build the firmware with the `fast_io` version implemented as frozen - bytecode. - 3. Use a release build. Install as in 1. above. Then change the module search - order by modifying `sys.path`. The initial entry `''` specifies frozen - bytecode. If this is deleted and appended to the end, frozen files will only - be found if there is no match in the filesystem. - -```python -import sys -sys.path.append(sys.path.pop(0)) # Prefer modules in filesystem -``` - -See [ESP Platforms](./FASTPOLL.md#6-esp-platforms) for general comments on the -suitability of ESP platforms for systems requiring fast response. - -## 1.1 Benchmarks - -The following files demonstrate the performance gains offered by prioritisation -and the improvements to task cancellation and timeouts. They also show the use -of these features. Documentation is in the code. - -Tests and benchmarks to run against the official and `fast_io` versions: - * `benchmarks/latency.py` Shows the effect on latency with and without low - priority usage. - * `benchmarks/rate.py` Shows the frequency with which uasyncio schedules - minimal coroutines (coros). - * `benchmarks/rate_esp.py` As above for ESP32 and ESP8266. - * `fast_io/ms_timer.py` An I/O device driver providing a timer with higher - precision timing than `wait_ms()` when run under the `fast_io` version. - * `fast_io/ms_timer_test.py` Test/demo program for above. - * `fast_io/pin_cb.py` An I/O device driver which causes a pin state change to - trigger a callback. This is a driver, not an executable test program. - * `fast_io/pin_cb_test.py` Demo of above driver: illustrates performance gain - under `fast_io`. - -Tests requiring the current version of the `fast_io` fork: - * `benchmarks/rate_fastio.py` Measures the rate at which coros can be scheduled - if the fast I/O mechanism is used but no I/O is pending. - * `benchmarks/call_lp.py` Demo of low priority callbacks. - * `benchmarks/overdue.py` Demo of maximum overdue feature. - * `benchmarks/priority_test.py` Cancellation of low priority coros. - * `fast_io/fast_can_test.py` Demo of cancellation of paused tasks. - * `fast_io/iorw_can.py` Cancellation of task waiting on I/O. - * `fast_io/iorw_to.py` Timeouts applies to tasks waiting on I/O. - -# 2. Rationale - -MicroPython firmware now enables device drivers for stream devices to be -written in Python, via `uio.IOBase`. This mechanism can be applied to any -situation where a piece of hardware or an asynchronously set flag needs to be -polled. Such polling is efficient because it is handled in C using -`select.poll`, and because the coroutine accessing the device is descheduled -until polling succeeds. - -Unfortunately official `uasyncio` polls I/O with a relatively high degree of -latency. - -Applications may need to poll a hardware device or a flag set by an interrupt -service routine (ISR). An overrun may occur if the scheduling of the polling -coroutine (coro) is subject to excessive latency. Fast devices with interrupt -driven drivers (such as the UART) need to buffer incoming data during any -latency period. Lower latency reduces the buffer size requirement. - -Further, a coro issuing `await asyncio.sleep_ms(t)` may block for much longer -than `t` depending on the number and design of other coros which are pending -execution. Delays can easily exceed the nominal value by an order of magnitude. - -This variant mitigates this by providing a means of scheduling I/O at a higher -priority than other coros: if an I/O queue is specified, I/O devices are polled -on every iteration of the scheduler. This enables faster response to real time -events and also enables higher precision millisecond-level delays to be -realised. - -The variant also enables coros to yield control in a way which prevents them -from competing with coros which are ready for execution. Coros which have -yielded in a low priority fashion will not be scheduled until all "normal" -coros are waiting on a nonzero timeout. The benchmarks show that the -improvement in the accuracy of time delays can exceed two orders of magnitude. - -## 2.1 Latency - -Coroutines in uasyncio which are pending execution are scheduled in a "fair" -round-robin fashion. Consider these functions: - -```python -async def foo(): - while True: - yield - # code which takes 4ms to complete - -async def handle_isr(): - global isr_has_run - while True: - if isr_has_run: - # read and process data - isr_has_run = False - yield -``` - -Assume a hardware interrupt handler sets the `isr_has_run` flag, and that we -have ten instances of `foo()` and one instance of `handle_isr()`. When -`handle_isr()` issues `yield`, its execution will pause for 40ms while each -instance of `foo()` is scheduled and performs one iteration. This may be -unacceptable: it may be necessary to poll and respond to the flag at a rate -sufficient to avoid overruns. - -In this version `handle_isr()` would be rewritten as a stream device driver -which could be expected to run with latency of just over 4ms. - -Alternatively this latency may be reduced by enabling the `foo()` instances to -yield in a low priority manner. In the case where all coros other than -`handle_isr()` are low priority the latency is reduced to 300μs - a figure -of about double the inherent latency of uasyncio. - -The benchmark latency.py demonstrates this. Documentation is in the code; it -can be run against both official and priority versions. This measures scheduler -latency. Maximum application latency, measured relative to the incidence of an -asynchronous event, will be 300μs plus the worst-case delay between yields of -any one competing task. - -### 2.1.1 I/O latency - -The official version of `uasyncio` has even higher levels of latency for I/O -scheduling. In the above case of ten coros using 4ms of CPU time between zero -delay yields, the latency of an I/O driver would be 80ms. - -###### [Contents](./FASTPOLL.md#contents) - -## 2.2 Timing accuracy - -Consider these functions: - -```python -async def foo(): - while True: - await asyncio.sleep(0) - # code which takes 4ms to complete - -async def fast(): - while True: - # Code omitted - await asyncio.sleep_ms(15) - # Code omitted -``` - -Again assume ten instances of `foo()` and one of `fast()`. When `fast()` -issues `await asyncio.sleep_ms(15)` it will not see a 15ms delay. During the -15ms period `foo()` instances will be scheduled. When the delay elapses, -`fast()` will compete with pending `foo()` instances. - -This results in variable delays up to 55ms (10 tasks * 4ms + 15ms). A -`MillisecTimer` class is provided which uses stream I/O to achieve a relatively -high precision delay: - -```python -async def timer_test(n): - timer = ms_timer.MillisecTimer() - while True: - await timer(30) # More precise timing - # Code -``` - -The test program `fast_io/ms_timer_test.py` illustrates three instances of a -coro with a 30ms nominal timer delay, competing with ten coros which yield with -a zero delay between hogging the CPU for 10ms. Using normal scheduling the 30ms -delay is actually 300ms. With fast I/O it is 30-34ms. - -###### [Contents](./FASTPOLL.md#contents) - -## 2.3 Polling in uasyncio - -The asyncio library provides various mechanisms for polling a device or flag. -Aside from a polling loop these include awaitable classes and asynchronous -iterators. If an awaitable class's `__iter__()` method simply returns the state -of a piece of hardware, there is no performance gain over a simple polling -loop. - -This is because uasyncio schedules tasks which yield with a zero delay, -together with tasks which have become ready to run, in a "fair" round-robin -fashion. This means that a task waiting on a zero delay will be rescheduled -only after the scheduling of all other such tasks (including timed waits whose -time has elapsed). - -The `fast_io` version enables awaitable classes and asynchronous iterators to -run with lower latency by designing them to use the stream I/O mechanism. The -program `fast_io/ms_timer.py` provides an example. - -Practical cases exist where the `foo()` tasks are not time-critical: in such -cases the performance of time critical tasks may be enhanced by enabling -`foo()` to submit for rescheduling in a way which does not compete with tasks -requiring a fast response. In essence "slow" operations tolerate longer latency -and longer time delays so that fast operations meet their performance targets. -Examples are: - - * User interface code. A system with ten pushbuttons might have a coro running - on each. A GUI touch detector coro needs to check a touch against sequence of - objects. Both may tolerate 100ms of latency before users notice any lag. - * Networking code: a latency of 100ms may be dwarfed by that of the network. - * Mathematical code: there are cases where time consuming calculations may - take place which are tolerant of delays. Examples are statistical analysis, - sensor fusion and astronomical calculations. - * Data logging. - -###### [Contents](./FASTPOLL.md#contents) - -# 3. The modified version - -The `fast_io` version adds `ioq_len=0` and `lp_len=0` arguments to -`get_event_loop`. These determine the lengths of I/O and low priority queues. -The zero defaults cause the queues not to be instantiated, in which case the -scheduler operates as per the official version. If an I/O queue length > 0 is -provided, I/O performed by `StreamReader` and `StreamWriter` objects is -prioritised over other coros. If a low priority queue length > 0 is specified, -tasks have an option to yield in such a way to minimise their competition with -other tasks. - -Arguments to `get_event_loop()`: - 1. `runq_len=16` Length of normal queue. Default 16 tasks. - 2. `waitq_len=16` Length of wait queue. - 3. `ioq_len=0` Length of I/O queue. Default: no queue is created. - 4. `lp_len=0` Length of low priority queue. Default: no queue. - -###### [Contents](./FASTPOLL.md#contents) - -## 3.1 Fast IO - -Device drivers which are to be capable of running at high priority should be -written to use stream I/O: see -[Writing streaming device drivers](./TUTORIAL.md#64-writing-streaming-device-drivers). - -The `fast_io` version will schedule I/O whenever the `ioctl` reports a ready -status. This implies that devices which become ready very soon after being -serviced can hog execution. This is analogous to the case where an interrupt -service routine is called at an excessive frequency. - -This behaviour may be desired where short bursts of fast data are handled. -Otherwise drivers of such hardware should be designed to avoid hogging, using -techniques like buffering or timing. - -###### [Contents](./FASTPOLL.md#contents) - -## 3.2 Low Priority - -The low priority solution is based on the notion of "after" implying a time -delay which can be expected to be less precise than the asyncio standard calls. -The `fast_io` version adds the following awaitable instances: - - * `after(t)` Low priority version of `sleep(t)`. - * `after_ms(t)` Low priority version of `sleep_ms(t)`. - -It adds the following event loop methods: - - * `loop.call_after(t, callback, *args)` - * `loop.call_after_ms(t, callback, *args)` - * `loop.max_overdue_ms(t=None)` This sets the maximum time a low priority task - will wait before being scheduled. A value of 0 corresponds to no limit. The - default arg `None` leaves the period unchanged. Always returns the period - value. If there is no limit and a competing task runs a loop with a zero delay - yield, the low priority yield will be postponed indefinitely. - -See [Low priority callbacks](./FASTPOLL.md#35-low-priority-callbacks) - -###### [Contents](./FASTPOLL.md#contents) - -## 3.3 Other Features - -### 3.3.1 Version - -Variable: - * `version` Returns a 2-tuple. Current contents ('fast_io', '0.25'). Enables - the presence and realease state of this version to be determined at runtime. - -### 3.3.2 Check event loop status - -The way `uasyncio` works can lead to subtle bugs. The first call to -`get_event_loop` instantiates the event loop and determines the size of its -queues. Hence the following code will not behave as expected: -```python -import uasyncio as asyncio -bar = Bar() # Constructor calls get_event_loop() -# and renders these args inoperative -loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) -``` -CPython V3.7 provides a function `get_running_loop` which enables the current -loop to be retrieved, raising a `RuntimeError` if one has not been -instantiated. This is provided in `fast_io`. In the above sample the `Bar` -constructor can call `get_running_loop` to avoid inadvertently instantiating an -event loop with default args. - -Function: - * `get_running_loop` No arg. Returns the event loop or raises a `RuntimeError` - if one has not been instantiated. - -Function: - * `got_event_loop()` No arg. Returns a `bool`: `True` if the event loop has - been instantiated. This is retained for compatibility: `get_running_loop` is - preferred. - -### 3.3.3 StreamReader readinto method - -The purpose of this asynchronous method is to be a non-allocating complement to -the `StreamReader.read` method, enabling data to be read into a pre-existing -buffer. It assumes that the device driver providing the data has a `readinto` -method. - -`StreamReader.readinto(buf, n=0)` args: -`buf` the buffer to read into. -`n=0` the maximum number of bytes to read - default the buffer size. - -The method will pause (allowing other coros to run) until data is available. - -Available data will be placed in the buffer. The return value is the number of -bytes read. The default maximum number of bytes is limited to the buffer size, -otherwise to the value of `n`. - -This method calls the synchronous `readinto` method of the data source. This -may take one arg (the buffer) or two (the buffer followed by the maximum number -of bytes to read). If `StreamReader.readinto` is launched with a single arg, -the `readinto` method will receive that one arg. - -It is the reponsibility of the device `readinto` method to validate the args, -to populate the buffer and to return the number of bytes read. It should return -"immediately" with as much data as is available. It will only be called when -the `ioctl` method indicates that read data is ready. - -###### [Contents](./FASTPOLL.md#contents) - -## 3.4 Low priority yield - -Consider this code fragment: - -```python -import uasyncio as asyncio -loop = asyncio.get_event_loop(lp_len=16) - -async def foo(): - while True: - # Do something - await asyncio.after(1.5) # Wait a minimum of 1.5s - # code - await asyncio.after_ms(20) # Wait a minimum of 20ms -``` - -These `await` statements cause the coro to suspend execution for the minimum -time specified. Low priority coros run in a mutually "fair" round-robin fashion. -By default the coro will only be rescheduled when all "normal" coros are waiting -on a nonzero time delay. A "normal" coro is one that has yielded by any other -means. - -This behaviour can be overridden to limit the degree to which they can become -overdue. For the reasoning behind this consider this code: - -```python -import uasyncio as asyncio -loop = asyncio.get_event_loop(lp_len=16) - -async def foo(): - while True: - # Do something - await asyncio.after(0) -``` - -By default a coro yielding in this way will be re-scheduled only when there are -no "normal" coros ready for execution i.e. when all are waiting on a nonzero -delay. The implication of having this degree of control is that if a coro -issues: - -```python -while True: - await asyncio.sleep(0) - # Do something which does not yield to the scheduler -``` - -low priority tasks will never be executed. Normal coros must sometimes wait on -a non-zero delay to enable the low priority ones to be scheduled. This is -analogous to running an infinite loop without yielding. - -This behaviour can be modified by issuing: - -```python -loop = asyncio.get_event_loop(lp_len = 16) -loop.max_overdue_ms(1000) -``` - -In this instance a task which has yielded in a low priority manner will be -rescheduled in the presence of pending "normal" tasks if they cause a low -priority task to become overdue by more than 1s. - -### 3.4.1 Task Cancellation and Timeouts - -Tasks which yield in a low priority manner may be subject to timeouts or be -cancelled in the same way as normal tasks. See [Task cancellation](./TUTORIAL.md#521-task-cancellation) -and [Coroutines with timeouts](./TUTORIAL.md#522-coroutines-with-timeouts). - -###### [Contents](./FASTPOLL.md#contents) - -## 3.5 Low priority callbacks - -The following `EventLoop` methods enable callback functions to be scheduled -to run when all normal coros are waiting on a delay or when `max_overdue_ms` -has elapsed: - -`call_after(delay, callback, *args)` Schedule a callback with low priority. -Positional args: - 1. `delay` Minimum delay in seconds. May be a float or integer. - 2. `callback` The callback to run. - 3. `*args` Optional comma-separated positional args for the callback. - -The delay specifies a minimum period before the callback will run and may have -a value of 0. The period may be extended depending on other high and low -priority tasks which are pending execution. - -A simple demo of this is `benchmarks/call_lp.py`. Documentation is in the -code. - -`call_after_ms(delay, callback, *args)` Call with low priority. Positional -args: - 1. `delay` Integer. Minimum delay in millisecs before callback runs. - 2. `callback` The callback to run. - 3. `*args` Optional positional args for the callback. - -###### [Contents](./FASTPOLL.md#contents) - -# 4. ESP Platforms - -It should be noted that the response of the ESP8266 to hardware interrupts is -remarkably slow. This also appears to apply to ESP32 platforms. Consider -whether a response in the high hundreds of μs meets project requirements; also -whether a priority mechanism is needed on hardware with such poor realtime -performance. - -# 5. Background - -This has been discussed in detail -[in issue 2989](https://github.com/micropython/micropython/issues/2989). - -A further discussion on the subject of using the ioread mechanism to achieve -fast scheduling took place -[in issue 2664](https://github.com/micropython/micropython/issues/2664). - -Support was finally [added here](https://github.com/micropython/micropython/pull/3836). - -# 6. Performance - -The `fast_io` version is designed to enable existing applications to run -unchanged and to minimise the effect on raw scheduler performance in cases -where the priority functionality is unused. - -The benchmark `rate.py` measures the rate at which tasks can be scheduled; -`rate_fastio` is identical except it instantiates an I/O queue and a low -priority queue. The benchmarks were run on a Pyboard V1.1 under official -`uasyncio` V2 and under the current `fast_io` version V0.24. Results were as -follows: - -| Script | Uasyncio version | Period (100 coros) | Overhead | PBD | -|:------:|:----------------:|:------------------:|:--------:|:---:| -| rate | Official V2 | 156μs | 0% | 123μs | -| rate | fast_io | 162μs | 3.4% | 129μs | -| rate_fastio | fast_io | 206μs | 32% | 181μs | - -The last column shows times from a Pyboard D SF2W. - -If an I/O queue is instantiated I/O is polled on every scheduler iteration -(that is its purpose). Consequently there is a significant overhead. In -practice the overhead will increase with the number of I/O devices being -polled and will be determined by the efficiency of their `ioctl` methods. - -Timings for current `fast_io` V0.24 and the original version were identical. diff --git a/v2/HD44780/README.md b/v2/HD44780/README.md deleted file mode 100644 index 70ad222..0000000 --- a/v2/HD44780/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# 1. Driver for character-based LCD displays - -This driver is for displays based on the Hitachi HD44780 driver: these are -widely available, typically in 16 character x 2 rows format. - -###### [Main README](../README.md) - -# 2. Files - - * `alcd.py` Driver, includes connection details. - * `alcdtest.py` Test/demo script. - -# 3. Typical wiring - -The driver uses 4-bit mode to economise on pins and wiring. Pins are arbitrary -but this configuration was used in testing: - -| LCD |Board | -|:----:|:----:| -| Rs | Y1 | -| E | Y2 | -| D7 | Y3 | -| D6 | Y4 | -| D5 | Y5 | -| D4 | Y6 | - -# 4. LCD Class - -## 4.1 Constructor - -This takes the following positional args: - * `pinlist` A tuple of 6 strings, being the Pyboard pins used for signals - `Rs`, `E`, `D4`, `D5`, `D6`, `D7` e.g. `('Y1','Y2','Y6','Y5','Y4','Y3')`. - * `cols` The number of horizontal characters in the display (typically 16). - * `rows` Default 2. Number of rows in the display. - -## 4.2 Display updates - -The class has no public properties or methods. The display is represented as an -array of strings indexed by row. The row contents is replaced in its entirety, -replacing all previous contents regardless of length. This is illustrated by -the test program: - -```python -import uasyncio as asyncio -import utime as time -from alcd import LCD, PINLIST - -lcd = LCD(PINLIST, cols = 16) - -async def lcd_task(): - for secs in range(20, -1, -1): - lcd[0] = 'MicroPython {}'.format(secs) - lcd[1] = "{:11d}uS".format(time.ticks_us()) - await asyncio.sleep(1) - -loop = asyncio.get_event_loop() -loop.run_until_complete(lcd_task()) -``` - -The row contents may be read back by issuing - -```python -row0 = lcd[0] -``` - -# 5. Display Formatting - -The driver represents an LCD display as an array indexed by row. Assigning a -string to a row causes that row to be updated. To write text to a specific -column of the display it is recommended to use the Python string `format` -method. - -For example this function formats a string such that it is left-padded with -spaces to a given column and right-padded to the specified width (typically the -width of the display). Right padding is not necessary but is included to -illustrate how right-justified formatting can be achieved: - -```python -def print_at(st, col, width=16): - return '{:>{col}s}{:{t}s}'.format(st,'', col=col+len(st), t = width-(col+len(st))) -``` - -``` ->>> print_at('cat', 2) -' cat ' ->>> len(_) -16 ->>> -``` - -This use of the `format` method may be extended to achieve more complex -tabulated data layouts. diff --git a/v2/HD44780/alcd.py b/v2/HD44780/alcd.py deleted file mode 100644 index bce80e7..0000000 --- a/v2/HD44780/alcd.py +++ /dev/null @@ -1,108 +0,0 @@ -# LCD class for Micropython and uasyncio. -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license -# V1.0 13 May 2017 - -# Assumes an LCD with standard Hitachi HD44780 controller chip wired using four data lines -# Code has only been tested on two line LCD displays. - -# My code is based on this program written for the Raspberry Pi -# http://www.raspberrypi-spy.co.uk/2012/07/16x2-lcd-module-control-using-python/ -# HD44780 LCD Test Script for -# Raspberry Pi -# -# Author : Matt Hawkins -# Site : http://www.raspberrypi-spy.co.uk - -from machine import Pin -import utime as time -import uasyncio as asyncio - -# ********************************** GLOBAL CONSTANTS: TARGET BOARD PIN NUMBERS ************************************* - -# Supply board pin numbers as a tuple in order Rs, E, D4, D5, D6, D7 - -PINLIST = ('Y1','Y2','Y6','Y5','Y4','Y3') # As used in testing. - -# **************************************************** LCD CLASS **************************************************** -# Initstring: -# 0x33, 0x32: See flowchart P24 send 3,3,3,2 -# 0x28: Function set DL = 1 (4 bit) N = 1 (2 lines) F = 0 (5*8 bit font) -# 0x0C: Display on/off: D = 1 display on C, B = 0 cursor off, blink off -# 0x06: Entry mode set: ID = 1 increment S = 0 display shift?? -# 0x01: Clear display, set DDRAM address = 0 -# Original code had timing delays of 50uS. Testing with the Pi indicates that time.sleep() can't issue delays shorter -# than about 250uS. There also seems to be an error in the original code in that the datasheet specifies a delay of -# >4.1mS after the first 3 is sent. To simplify I've imposed a delay of 5mS after each initialisation pulse: the time to -# initialise is hardly critical. The original code worked, but I'm happier with something that complies with the spec. - -# Async version: -# No point in having a message queue: people's eyes aren't that quick. Just display the most recent data for each line. -# Assigning changed data to the LCD object sets a "dirty" flag for that line. The LCD's runlcd thread then updates the -# hardware and clears the flag - -# lcd_byte and lcd_nybble method use explicit delays. This is because execution -# time is short relative to general latency (on the order of 300μs). - -class LCD(object): # LCD objects appear as read/write lists - INITSTRING = b'\x33\x32\x28\x0C\x06\x01' - LCD_LINES = b'\x80\xC0' # LCD RAM address for the 1st and 2nd line (0 and 40H) - CHR = True - CMD = False - E_PULSE = 50 # Timing constants in uS - E_DELAY = 50 - def __init__(self, pinlist, cols, rows = 2): # Init with pin nos for enable, rs, D4, D5, D6, D7 - self.initialising = True - self.LCD_E = Pin(pinlist[1], Pin.OUT) # Create and initialise the hardware pins - self.LCD_RS = Pin(pinlist[0], Pin.OUT) - self.datapins = [Pin(pin_name, Pin.OUT) for pin_name in pinlist[2:]] - self.cols = cols - self.rows = rows - self.lines = [""] * self.rows - self.dirty = [False] * self.rows - for thisbyte in LCD.INITSTRING: - self.lcd_byte(thisbyte, LCD.CMD) - self.initialising = False # Long delay after first byte only - loop = asyncio.get_event_loop() - loop.create_task(self.runlcd()) - - def lcd_nybble(self, bits): # send the LS 4 bits - for pin in self.datapins: - pin.value(bits & 0x01) - bits >>= 1 - time.sleep_us(LCD.E_DELAY) # 50μs - self.LCD_E.value(True) # Toggle the enable pin - time.sleep_us(LCD.E_PULSE) - self.LCD_E.value(False) - if self.initialising: - time.sleep_ms(5) - else: - time.sleep_us(LCD.E_DELAY) # 50μs - - def lcd_byte(self, bits, mode): # Send byte to data pins: bits = data - self.LCD_RS.value(mode) # mode = True for character, False for command - self.lcd_nybble(bits >>4) # send high bits - self.lcd_nybble(bits) # then low ones - - def __setitem__(self, line, message): # Send string to display line 0 or 1 - # Strip or pad to width of display. - # Whould use "{0:{1}.{1}}".format("rats", 20) but - message = "%-*.*s" % (self.cols,self.cols,message) # computed format field sizes are unsupported - if message != self.lines[line]: # Only update LCD if data has changed - self.lines[line] = message # Update stored line - self.dirty[line] = True # Flag its non-correspondence with the LCD device - - def __getitem__(self, line): - return self.lines[line] - - async def runlcd(self): # Periodically check for changed text and update LCD if so - while(True): - for row in range(self.rows): - if self.dirty[row]: - msg = self[row] - self.lcd_byte(LCD.LCD_LINES[row], LCD.CMD) - for thisbyte in msg: - self.lcd_byte(ord(thisbyte), LCD.CHR) - await asyncio.sleep_ms(0) # Reshedule ASAP - self.dirty[row] = False - await asyncio.sleep_ms(20) # Give other coros a look-in diff --git a/v2/HD44780/alcdtest.py b/v2/HD44780/alcdtest.py deleted file mode 100644 index 2899c31..0000000 --- a/v2/HD44780/alcdtest.py +++ /dev/null @@ -1,19 +0,0 @@ -# alcdtest.py Test program for LCD class -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license -# runs for 20s -import uasyncio as asyncio -import utime as time -from alcd import LCD, PINLIST - -lcd = LCD(PINLIST, cols = 16) - -async def lcd_task(): - for secs in range(20, -1, -1): - lcd[0] = 'MicroPython {}'.format(secs) - lcd[1] = "{:11d}uS".format(time.ticks_us()) - await asyncio.sleep(1) - - -loop = asyncio.get_event_loop() -loop.run_until_complete(lcd_task()) diff --git a/v2/PRIMITIVES.md b/v2/PRIMITIVES.md deleted file mode 100644 index ca147d4..0000000 --- a/v2/PRIMITIVES.md +++ /dev/null @@ -1,792 +0,0 @@ -# 1. The asyn.py library - -This provides some simple synchronisation primitives, together with an API for -task monitoring and cancellation. Task cancellation requires usayncio V 1.7.1 -or higher. At the time of writing (7th Jan 2018) it requires a daily build of -MicroPython firmware or one built from source. - -The library is too large to run on the ESP8266 except as frozen bytecode. An -obvious workround is to produce a version with unused primitives removed. - -###### [Main README](./README.md) - -# Contents - - 1. [The asyn.py library](./PRIMITIVES.md#1-the-asyn.py-library) - 1.1 [Synchronisation Primitives](./PRIMITIVES.md#11-synchronisation-primitives) - 1.2 [Task control and monitoring](./PRIMITIVES.md#12-task-control-and-monitoring) - 2. [Modules](./PRIMITIVES.md#2-modules) - 3. [Synchronisation Primitives](./PRIMITIVES.md#3-synchronisation-primitives) - 3.1 [Function launch](./PRIMITIVES.md#31-function-launch) Launch a function or a coro interchangeably. - 3.2 [Class Lock](./PRIMITIVES.md#32-class-lock) Ensure exclusive access to a shared resource. - 3.2.1 [Definition](./PRIMITIVES.md#321-definition) - 3.3 [Class Event](./PRIMITIVES.md#33-class-event) Pause a coro until an event occurs. - 3.3.1 [Definition](./PRIMITIVES.md#331-definition) - 3.4 [Class Barrier](./PRIMITIVES.md#34-class-barrier) Pause multiple coros until all reach a given point. - 3.5 [Class Semaphore](./PRIMITIVES.md#35-class-semaphore) Limit number of coros which can access a resource. - 3.5.1 [Class BoundedSemaphore](./PRIMITIVES.md#351-class-boundedsemaphore) - 3.6 [Class Condition](./PRIMITIVES.md#36-class-condition) Control access to a shared reource. - 3.6.1 [Definition](./PRIMITIVES.md#361-definition) - 3.7 [Class Gather](./PRIMITIVES.md#37-class-gather) Synchronise and collect results from multiple coros. - 3.7.1 [Definition](./PRIMITIVES.md#371-definition) - 3.7.2 [Use with timeouts and cancellation](./PRIMITIVES.md#372-use-with-timeouts-and-cancellation) Demo of advanced usage of Gather. - 4. [Task Cancellation](./PRIMITIVES.md#4-task-cancellation) Methods of cancelling tasks and groups of tasks. - 4.1 [Coro sleep](./PRIMITIVES.md#41-coro-sleep) sleep() with reduced exception handling latency. - 4.2 [Class Cancellable](./PRIMITIVES.md#42-class-cancellable) Register tasks for cancellation. - 4.2.1 [Groups](./PRIMITIVES.md#421-groups) Group sets of tasks for cancellation. - 4.2.2 [Custom cleanup](./PRIMITIVES.md#422-custom-cleanup) - 4.3 [Class NamedTask](./PRIMITIVES.md#43-class-namedtask) Associate tasks with names for cancellation. - 4.3.1 [Latency and Barrier objects](./PRIMITIVES.md#431-latency-and-barrier-objects) - 4.3.2 [Custom cleanup](./PRIMITIVES.md#432-custom-cleanup) - -## 1.1 Synchronisation Primitives - -There is often a need to provide synchronisation between coros. A common -example is to avoid what are known as "race conditions" where multiple coros -compete to access a single resource. An example is provided in the `aswitch.py` -program and discussed in [the docs](./DRIVERS.md). Another hazard is the "deadly -embrace" where two coros wait on the other's completion. - -In simple applications these are often addressed with global flags. A more -elegant approach is to use synchronisation primitives. The module `asyn.py` -offers "micro" implementations of `Lock`, `Event`, `Barrier`, `Semaphore` and -`Condition` primitives, and a lightweight implementation of `asyncio.gather`. - -Another synchronisation issue arises with producer and consumer coros. The -producer generates data which the consumer uses. Asyncio provides the `Queue` -object. The producer puts data onto the queue while the consumer waits for its -arrival (with other coros getting scheduled for the duration). The `Queue` -guarantees that items are removed in the order in which they were received. As -this is a part of the uasyncio library its use is described in the [tutorial](./TUTORIAL.md). - -###### [Contents](./PRIMITIVES.md#contents) - -## 1.2 Task control and monitoring - -`uasyncio` does not implement the `Task` and `Future` classes of `asyncio`. -Instead it uses a 'micro' lightweight means of task cancellation. The `asyn.py` -module provides an API to simplify its use and to check on the running status -of coroutines which are subject to cancellation. - -# 2. Modules - -The following modules are provided: - * `asyn.py` The main library. - * `asyntest.py` Test/demo programs for the primitives. - * `asyn_demos.py` Minimal "get started" task cancellation demos. - * `cantest.py` Task cancellation tests. Examples of intercepting `StopTask`. - Intended to verify the library against future `uasyncio` changes. - -Import `asyn_demos.py` or `cantest.py` for a list of available tests. - -###### [Contents](./PRIMITIVES.md#contents) - -# 3. Synchronisation Primitives - -The primitives are intended for use only with `uasyncio`. They are `micro` in -design. They are not thread safe and hence are incompatible with the `_thread` -module and with interrupt handlers. - -## 3.1 Function launch - -This function accepts a function or coro as an argument, along with a tuple of -args. If the function is a callback it is executed with the supplied argumets. -If it is a coro, it is scheduled for execution. - -args: - * `func` Mandatory. a function or coro. These are provided 'as-is' i.e. not - using function call syntax. - * `tup_args` Optional. A tuple of arguments, default `()`. The args are - upacked when provided to the function. - -## 3.2 Class Lock - -This has now been superseded by the more efficient official version. - -At time of writing (18th Dec 2017) the official `Lock` class is not complete. -If a coro is subject to a [timeout](./TUTORIAL.md#44-coroutines-with-timeouts) -and the timeout is triggered while it is waiting on a lock, the timeout will be -ineffective. It will not receive the `TimeoutError` until it has acquired the -lock. - -The implementation in `asyn.py` avoids this limitation but at the cost of lower -efficiency. The remainder of this section describes this version. - -A lock guarantees unique access to a shared resource. The preferred way to use it -is via an asynchronous context manager. In the following code sample a `Lock` -instance `lock` has been created and is passed to all coros wishing to access -the shared resource. Each coro issues the following: - -```python -async def bar(lock): - async with lock: - # Access resource -``` - -While the coro `bar` is accessing the resource, other coros will pause at the -`async with lock` statement until the context manager in `bar()` is complete. - -Note that MicroPython had a bug in its implementation of asynchronous context -managers. This is fixed: if you build from source there is no problem. Alas the -fix was too late for release build V1.9.4. If using that build a `return` -statement should not be issued in the `async with` block. See note at end of -[this section](./TUTORIAL.md#43-asynchronous-context-managers). - -### 3.2.1 Definition - -Constructor: Optional argument `delay_ms` default 0. Sets a delay between -attempts to acquire the lock. In applications with coros needing frequent -scheduling a nonzero value will reduce the `Lock` object's CPU overhead at the -expense of latency. -Methods: - - * `locked` No args. Returns `True` if locked. - * `release` No args. Releases the lock. - * `acquire` No args. Coro which pauses until the lock has been acquired. Use - by executing `await lock.acquire()`. - -###### [Contents](./PRIMITIVES.md#contents) - -## 3.3 Class Event - -This provides a way for one or more coros to pause until another one flags them -to continue. An `Event` object is instantiated and passed to all coros using -it. Coros waiting on the event issue `await event`. Execution pauses -until a coro issues `event.set()`. `event.clear()` must then be issued. An -optional data argument may be passed to `event.set()` and retrieved by -`event.value()`. - -In the usual case where a single coro is awaiting the event this can be done -immediately after it is received: - -```python -async def eventwait(event): - await event - event.clear() -``` - -The coro raising the event may need to check that it has been serviced: - -```python -async def foo(event): - while True: - # Acquire data from somewhere - while event.is_set(): - await asyncio.sleep(1) # Wait for coro to respond - event.set() -``` - -If multiple coros are to wait on a single event, consider using a `Barrier` -object described below. This is because the coro which raised the event has no -way to determine whether all others have received it; determining when to clear -it down requires further synchronisation. One way to achieve this is with an -acknowledge event: - -```python -async def eventwait(event, ack_event): - await event - ack_event.set() -``` - -Example of this are in `event_test` and `ack_test` in asyntest.py. - -### 3.3.1 Definition - -Constructor: takes one optional integer argument. - * `delay_ms` default 0. While awaiting an event an internal flag is repeatedly - polled. Setting a finite polling interval reduces the task's CPU overhead at - the expense of increased latency. - -Synchronous Methods: - * `set` Initiates the event. Optional arg `data`: may be of any type, - sets the event's value. Default `None`. May be called in an interrupt context. - * `clear` No args. Clears the event, sets the value to `None`. - * `is_set` No args. Returns `True` if the event is set. - * `value` No args. Returns the value passed to `set`. - -Asynchronous Method: - * `wait` For CPython compatibility. Pause until event is set. The CPython - Event is not awaitable. - -The optional data value may be used to compensate for the latency in awaiting -the event by passing `loop.time()`. - -###### [Contents](./PRIMITIVES.md#contents) - -## 3.4 Class Barrier - -This enables multiple coros to rendezvous at a particular point. For example -producer and consumer coros can synchronise at a point where the producer has -data available and the consumer is ready to use it. At that point in time the -`Barrier` can optionally run a callback before releasing the barrier and -allowing all waiting coros to continue. - -Constructor. -Mandatory arg: -`participants` The number of coros which will use the barrier. -Optional args: -`func` Callback to run. Default `None`. -`args` Tuple of args for the callback. Default `()`. - -Public synchronous methods: - * `busy` No args. Returns `True` if at least one coro is waiting on the - barrier, or if at least one non-waiting coro has not triggered it. - * `trigger` No args. The barrier records that the coro has passed the critical - point. Returns "immediately". - -The callback can be a function or a coro. In most applications a function will -be used as this can be guaranteed to run to completion beore the barrier is -released. - -Participant coros issue `await my_barrier` whereupon execution pauses until all -other participants are also waiting on it. At this point any callback will run -and then each participant will re-commence execution. See `barrier_test` and -`semaphore_test` in `asyntest.py` for example usage. - -A special case of `Barrier` usage is where some coros are allowed to pass the -barrier, registering the fact that they have done so. At least one coro must -wait on the barrier. That coro will pause until all non-waiting coros have -passed the barrier, and all waiting coros have reached it. At that point all -waiting coros will resume. A non-waiting coro issues `barrier.trigger()` to -indicate that is has passed the critical point. - -This mechanism is used in the `Cancellable` and `NamedTask` classes to register -the fact that a coro has responded to cancellation. Using a non-waiting barrier -in a looping construct carries a fairly obvious hazard and is normally to be -avoided. - -###### [Contents](./PRIMITIVES.md#contents) - -## 3.5 Class Semaphore - -A semaphore limits the number of coros which can access a resource. It can be -used to limit the number of instances of a particular coro which can run -concurrently. It performs this using an access counter which is initialised by -the constructor and decremented each time a coro acquires the semaphore. - -Constructor: Optional arg `value` default 1. Number of permitted concurrent -accesses. - -Synchronous method: - * `release` No args. Increments the access counter. - -Asynchronous method: - * `acquire` No args. If the access counter is greater than 0, decrements it - and terminates. Otherwise waits for it to become greater than 0 before - decrementing it and terminating. - -The easiest way to use it is with a context manager: - -```python -async def foo(sema): - async with sema: - # Limited access here -``` - -There is a difference between a `Semaphore` and a `Lock`. A `Lock` -instance is owned by the coro which locked it: only that coro can release it. A -`Semaphore` can be released by any coro which acquired it. - -### 3.5.1 Class BoundedSemaphore - -This works identically to the `Semaphore` class except that if the `release` -method causes the access counter to exceed its initial value, a `ValueError` -is raised. - -###### [Contents](./PRIMITIVES.md#contents) - -## 3.6 Class Condition - -A `Condition` instance enables controlled access to a shared resource. In -typical applications a number of tasks wait for the resource to be available. -Once this occurs access can be controlled both by the number of tasks and by -means of a `Lock`. - -A task waiting on a `Condition` instance will pause until another task issues -`condition.notify(n)` or `condition.notify_all()`. If the number of tasks -waiting on the condition exceeds `n`, only `n` tasks will resume. A `Condition` -instance has a `Lock` as a member. A task will only resume when it has acquired -the lock. User code may release the lock as required by the application logic. - -Typical use of the class is in a synchronous context manager: - -```python - with await cond: - cond.notify(2) # Notify 2 tasks -``` - -```python - with await cond: - await cond.wait() - # Has been notified and has access to the locked resource - # Resource has been unocked by context manager -``` -### 3.6.1 Definition - -Constructor: Optional arg `lock=None`. A `Lock` instance may be specified, -otherwise the `Condition` instantiates its own. - -Synchronous methods: - * `locked` No args. Returns the state of the `Lock` instance. - * `release` No args. Release the `Lock`. A `RuntimeError` will occur if the - `Lock` is not locked. - * `notify` Arg `n=1`. Notify `n` tasks. The `Lock` must be acquired before - issuing `notify` otherwise a `RuntimeError` will occur. - * `notify_all` No args. Notify all tasks. The `Lock` must be acquired before - issuing `notify_all` otherwise a `RuntimeError` will occur. - -Asynchronous methods: - * `acquire` No args. Pause until the `Lock` is acquired. - * `wait` No args. Await notification and the `Lock`. The `Lock` must be - acquired before issuing `wait` otherwise a `RuntimeError` will occur. The - sequence is as follows: - The `Lock` is released. - The task pauses until another task issues `notify`. - It continues to pause until the `Lock` has been re-acquired when execution - resumes. - * `wait_for` Arg: `predicate` a callback returning a `bool`. The task pauses - until a notification is received and an immediate test of `predicate()` - returns `True`. - -###### [Contents](./PRIMITIVES.md#contents) - -## 3.7 Class Gather - -This aims to replicate some of the functionality of `asyncio.gather` in a -'micro' form. The user creates a list of `Gatherable` tasks and then awaits a -`Gather` object. When the last task to complete terminates, this will return a -list of results returned by the tasks. Timeouts may be assigned to individual -tasks. - -```python -async def foo(n): - await asyncio.sleep(n) - return n * n - -async def bar(x, y, rats): # Example coro: note arg passing - await asyncio.sleep(1) - return x * y * rats - -gatherables = [asyn.Gatherable(foo, n) for n in range(4)] -gatherables.append(asyn.Gatherable(bar, 7, 8, rats=77)) -gatherables.append(asyn.Gatherable(rats, 0, timeout=5)) -res = await asyn.Gather(gatherables) -``` - -The result `res` is a 6 element list containing the result of each of the 6 -coros. These are ordered by the position of the coro in the `gatherables` list. -This is as per `asyncio.gather()`. - -See `asyntest.py` function `gather_test()`. - -### 3.7.1 Definition - -The `Gatherable` class has no user methods. The constructor takes a coro by -name followed by any positional or keyword arguments for the coro. If an arg -`timeout` is provided it should have an integer or float value: this is taken -to be the timeout for the coro in seconds. Note that timeout is subject to the -latency discussed in [Coroutines with timeouts](./TUTORIAL.md#44-coroutines-with-timeouts). -A way to reduce this is to use `asyn.sleep()` in such coros. - -The `Gather` class has no user methods. The constructor takes one mandatory -arg: a list of `Gatherable` instances. - -`Gather` instances are awaitable. An `await` on an instance will terminate when -the last member task completes or times out. It returns a list whose length -matches the length of the list of `Gatherable` instances. Each element contains -the return value of the corresponding `Gatherable` instance. Each return value -may be of any type. - -### 3.7.2 Use with timeouts and cancellation - -The following complete example illustrates the use of `Gather` with tasks which -are subject to cancellation or timeout. - -```python -import uasyncio as asyncio -import asyn - -async def barking(n): - print('Start normal coro barking()') - for _ in range(6): - await asyncio.sleep(1) - print('Done barking.') - return 2 * n - -async def foo(n): - print('Start timeout coro foo()') - try: - while True: - await asyncio.sleep(1) - n += 1 - except asyncio.TimeoutError: - print('foo timeout.') - return n - -@asyn.cancellable -async def bar(n): - print('Start cancellable bar()') - try: - while True: - await asyncio.sleep(1) - n += 1 - except asyn.StopTask: - print('bar stopped.') - return n - -async def do_cancel(): - await asyncio.sleep(5.5) - await asyn.Cancellable.cancel_all() - -async def main(loop): - bar_task = asyn.Cancellable(bar, 70) # Note args here - gatherables = [asyn.Gatherable(barking, 21), - asyn.Gatherable(foo, 10, timeout=7.5), - asyn.Gatherable(bar_task)] - loop.create_task(do_cancel()) - res = await asyn.Gather(gatherables) - print('Result: ', res) # Expect [42, 17, 75] - -loop = asyncio.get_event_loop() -loop.run_until_complete(main(loop)) -``` - -###### [Contents](./PRIMITIVES.md#contents) - -# 4. Task Cancellation - -All current `uasyncio` versions have a `cancel(coro)` function. This works by -throwing an exception to the coro in a special way: cancellation is deferred -until the coro is next scheduled. This mechanism works with nested coros. - -There is a limitation with official `uasyncio` V2.0. In this version a coro -which is waiting on a `sleep()` or `sleep_ms()` or pending I/O will not get the -exception until it is next scheduled. This means that cancellation can take a -long time: there is often a need to be able to verify when this has occurred. - -This problem can now be circumvented in two ways both involving running -unofficial code. The solutions fix the problem by ensuring that the cancelled -coro is scheduled promptly. Assuming `my_coro` is coded normally the following -will ensure that cancellation is complete, even if `my_coro` is paused at the -time of cancellation: -```python -my_coro_instance = my_coro() -loop.add_task(my_coro_instance) -# Do something -asyncio.cancel(my_coro_instance) -await asyncio.sleep(0) -# The task is now cancelled -``` -The unofficial solutions are: - * To run the `fast_io` version of `uasyncio` presented her, with official - MicroPython firmware. - * To run [Paul Sokolovsky's Pycopy firmware fork](https://github.com/pfalcon/pycopy) - plus `uasyncio` V2.4 from - [Paul Sokolovsky's library fork](https://github.com/pfalcon/micropython-lib) - -The following describes workrounds for those wishing to run official code (for -example the current realease build which includes `uasyncio` V2.0). There is -usually a need to establish when cancellation has occured: the classes and -decorators described below facilitate this. - -If a coro issues `await uasyncio.sleep(secs)` or `await uasyncio.sleep_ms(ms)` -scheduling will not occur until the time has elapsed. This introduces latency -into cancellation which matters in some use-cases. Other potential sources of -latency take the form of slow code. `uasyncio` V2.0 has no mechanism for -verifying when cancellation has actually occurred. The `asyn.py` library -provides solutions in the form of two classes. - -These are `Cancellable` and `NamedTask`. The `Cancellable` class allows the -creation of named groups of tasks which may be cancelled as a group; this -awaits completion of cancellation of all tasks in the group. - -The `NamedTask` class enables a task to be associated with a user supplied -name, enabling it to be cancelled and its status checked. Cancellation -optionally awaits confirmation of completion. - -For cases where cancellation latency is of concern `asyn.py` offers a `sleep` -function which provides a delay with reduced latency. - -## 4.1 Coro sleep - -Pause for a period as per `uasyncio.sleep` but with reduced exception handling -latency. - -The asynchronous `sleep` function takes two args: - * `t` Mandatory. Time in seconds. May be integer or float. - * `granularity` Optional integer >= 0, units ms. Default 100ms. Defines the - maximum latency. Small values reduce latency at cost of increased scheduler - workload. - -This repeatedly issues `uasyncio.sleep_ms(t)` where t <= `granularity`. - -## 4.2 Class Cancellable - -This class provides for cancellation of one or more tasks where it is necesary -to await confirmation that cancellation is complete. `Cancellable` instances -are anonymous coros which are members of a named group. They are capable of -being cancelled as a group. A typical use-case might take this form: - -```python -async def comms(): # Perform some communications task - while True: - await initialise_link() - try: - await do_communications() # Launches Cancellable tasks - except CommsError: - await asyn.Cancellable.cancel_all() - # All sub-tasks are now known to be stopped. They can be re-started - # with known initial state on next pass. -``` - -A `Cancellable` task is declared with the `@cancellable` decorator: - -```python -@asyn.cancellable -async def print_nums(num): - while True: - print(num) - num += 1 - await sleep(1) # asyn.sleep() allows fast response to exception -``` - -Positional or keyword arguments for the task are passed to the `Cancellable` -constructor as below. Note that the coro is passed not using function call -syntax. `Cancellable` tasks may be awaited or placed on the event loop: - -```python -await asyn.Cancellable(print_nums, 5) # single arg to print_nums. -loop = asyncio.get_event_loop() -loop.create_task(asyn.Cancellable(print_nums, 42)()) # Note () syntax. -``` -**NOTE** A coro declared with `@asyn.cancellable` must only be launched using -the above syntax options. Treating it as a conventional coro will result in -`tuple index out of range` errors or other failures. - -The following will cancel any tasks still running, pausing until cancellation -is complete: - -```python -await asyn.Cancellable.cancel_all() -``` - -Constructor mandatory args: - * `task` A coro passed by name i.e. not using function call syntax. - -Constructor optional positional args: - * Any further positional args are passed to the coro. - -Constructor optional keyword args: - * `group` Any Python object, typically integer or string. Default 0. See - Groups below. - * Further keyword args are passed to the coro. - -Public class method: - * `cancel_all` Asynchronous. - Optional args `group` default 0, `nowait` default `False`. - The `nowait` arg is for use by the `NamedTask` derived class. The default - value is assumed below. - The method cancels all instances in the specified group and awaits completion. - See Groups below. - The `cancel_all` method will complete when all `Cancellable` instances have - been cancelled or terminated naturally before `cancel_all` was launched. - Each coro will receive a `StopTask` exception when it is next scheduled. If - the coro is written using the `@cancellable` decorator this is handled - automatically. - It is possible to trap the `StopTask` exception: see 'Custom cleanup' below. - -Public bound method: - * `__call__` This returns the coro and is used to schedule the task using the - event loop `create_task()` method using function call syntax. - -The `asyn.StopTask` exception is an alias for `usayncio.CancelledError`. In my -view the name is more descriptive of its function. - -A complete minimal, example: -```python -import uasyncio as asyncio -import asyn - -@asyn.cancellable -async def print_nums(num): - while True: - print(num) - num += 1 - await asyn.sleep(1) # asyn.sleep() allows fast response to exception - -async def main(loop): - loop.create_task(asyn.Cancellable(print_nums, 42)()) # Note () syntax - await asyncio.sleep(5) - await asyn.Cancellable.cancel_all() - print('Task cancelled: delay 3 secs to prove it.') - await asyncio.sleep(3) - -loop = asyncio.get_event_loop() -loop.run_until_complete(main(loop)) -``` - -### 4.2.1 Groups - -`Cancellable` tasks may be assigned to groups, identified by a user supplied -Python object, typically an integer or string. By default tasks are assigned to -group 0. The `cancel_all` class method cancels all tasks in the specified -group. The 0 default ensures that this facility can be ignored if not required, -with `cancel_all` cancelling all `Cancellable` tasks. - -### 4.2.2 Custom cleanup - -A task created with the `cancellable` decorator can intercept the `StopTask` -exception to perform custom cleanup operations. This may be done as below: -```python -@asyn.cancellable -async def foo(): - while True: - try: - await sleep(1) # Main body of task - except asyn.StopTask: - # perform custom cleanup - return # Respond by quitting -``` -The following example returns `True` if it ends normally or `False` if -cancelled. -```python -@asyn.cancellable -async def bar(): - try: - await sleep(1) # Main body of task - except asyn.StopTask: - return False - else: - return True -``` -A complete minimal example: -```python -import uasyncio as asyncio -import asyn - -@asyn.cancellable -async def print_nums(num): - try: - while True: - print(num) - num += 1 - await asyn.sleep(1) # asyn.sleep() allows fast response to exception - except asyn.StopTask: - print('print_nums was cancelled') - -async def main(loop): - loop.create_task(asyn.Cancellable(print_nums, 42)()) # Note () syntax - await asyncio.sleep(5) - await asyn.Cancellable.cancel_all() - print('Task cancelled: delay 3 secs to prove it.') - await asyncio.sleep(3) - -loop = asyncio.get_event_loop() -loop.run_until_complete(main(loop)) -``` - -###### [Contents](./PRIMITIVES.md#contents) - -## 4.3 Class NamedTask - -A `NamedTask` instance is associated with a user-defined name such that the -name may outlive the task: a coro may end but the class enables its state to be -checked. It is a subclass of `Cancellable` and its constructor disallows -duplicate names: each instance of a coro must be assigned a unique name. - -A `NamedTask` coro is defined with the `@cancellable` decorator. - -```python -@cancellable -async def foo(arg1, arg2): - await asyn.sleep(1) - print('Task foo has ended.', arg1, arg2) -``` - -The `NamedTask` constructor takes the name, the coro, plus any user positional -or keyword args. The resultant instance can be scheduled in the usual ways: - -```python -await asyn.NamedTask('my foo', foo, 1, 2) # Pause until complete or killed -loop = asyncio.get_event_loop() # Or schedule and continue: -loop.create_task(asyn.NamedTask('my nums', foo, 10, 11)()) # Note () syntax. -``` - -Cancellation is performed with: - -```python -await asyn.NamedTask.cancel('my foo') -``` - -When cancelling a task there is no need to check if the task is still running: -if it has already completed, cancellation will have no effect. - -NamedTask Constructor. -Mandatory args: - * `name` Names may be any immutable type capable of being a dictionary index - e.g. integer or string. A `ValueError` will be raised if the name is already - assigned by a running coro. If multiple instances of a coro are to run - concurrently, each should be assigned a different name. - * `task` A coro passed by name i.e. not using function call syntax. - - Optional positional args: - * Any further positional args are passed to the coro. - - Optional keyword only args: - * `barrier` A `Barrier` instance may be passed. See below. - * Further keyword args are passed to the coro. - -Public class methods: - * `cancel` Asynchronous. - Mandatory arg: a coro name. - Optional boolean arg `nowait` default `True` - By default it will return soon. If `nowait` is `False` it will pause until the - coro has completed cancellation. - The named coro will receive a `StopTask` exception the next time it is - scheduled. If the `@namedtask` decorator is used this is transparent to the - user but the exception may be trapped for custom cleanup (see below). - `cancel` will return `True` if the coro was cancelled. It will return `False` - if the coro has already ended or been cancelled. - * `is_running` Synchronous. Arg: A coro name. Returns `True` if coro is queued - for scheduling, `False` if it has ended or been cancelled. - -Public bound method: - * `__call__` This returns the coro and is used to schedule the task using the - event loop `create_task()` method using function call syntax. - -### 4.3.1 Latency and Barrier objects - -It is possible to get confirmation of cancellation of an arbitrary set of -`NamedTask` instances by instantiating a `Barrier` and passing it to the -constructor of each member. This enables more complex synchronisation cases -than the normal method of using a group of `Cancellable` tasks. The approach is -described below. - -If a `Barrier` instance is passed to the `NamedTask` constructor, a task -performing cancellation can pause until a set of cancelled tasks have -terminated. The `Barrier` is constructed with the number of dependent tasks -plus one (the task which is to wait on it). It is passed to the constructor of -each dependent task and the cancelling task waits on it after cancelling all -dependent tasks. Each task being cancelled terminates 'immediately' subject -to latency. - -See examples in `cantest.py` e.g. `cancel_test2()`. - -### 4.3.2 Custom cleanup - -A coroutine to be used as a `NamedTask` can intercept the `StopTask` exception -if necessary. This might be done for cleanup or to return a 'cancelled' status. -The coro should have the following form: - -```python -@asyn.cancellable -async def foo(): - try: - await asyncio.sleep(1) # User code here - except asyn.StopTask: - return False # Cleanup code - else: - return True # Normal exit -``` - -###### [Contents](./PRIMITIVES.md#contents) diff --git a/v2/README.md b/v2/README.md deleted file mode 100644 index 163855a..0000000 --- a/v2/README.md +++ /dev/null @@ -1,166 +0,0 @@ -# 1. uasyncio V2 - -This repo also contains an optional `fast_io` variant of `uasyncio` V2. This -variant offers high I/O performance and also includes workrounds for many of -the bugs in V2. (Bugs properly fixed in V3.) - -## Reasons for running V2 - -In general I recommend V3, especially for new projects. It is better in every -respect bar one: the `fast_io` variant of V2 currently offers superior I/O -performance, relative both to V2 and V3. - -The main reason for running official V2 is that many existing libraries have -not yet been ported to V3. Some will run without change, but those using more -advanced features of `uasyncio` may not. - -## 1.1 Resources - - * [A tutorial](./TUTORIAL.md) An introductory tutorial on asynchronous - programming and the use of the `uasyncio` library. - * [Asynchronous device drivers](./DRIVERS.md). A module providing drivers for - devices such as switches and pushbuttons. - * [Synchronisation primitives](./PRIMITIVES.md). Provides commonly used - synchronisation primitives plus an API for task cancellation and monitoring. - * [A driver for an IR remote control](./nec_ir/README.md) This is intended as - an example of an asynchronous device driver. It decodes signals received from - infra red remote controls using the popular NEC protocol. - * [A driver for the HTU21D](./htu21d/README.md) temperature and humidity - sensor. This is intended to be portable across platforms and is another - example of an asynchronous device driver. - * [A driver for character LCD displays](./HD44780/README.md). A simple - asynchronous interface to displays based on the Hitachi HD44780 chip. - * [A driver for GPS modules](./gps/README.md) Runs a background task to read - and decode NMEA sentences, providing constantly updated position, course, - altitude and time/date information. - * [Communication using I2C slave mode.](./i2c/README.md) Enables a Pyboard to - to communicate with another MicroPython device using stream I/O. The Pyboard - achieves bidirectional communication with targets such as an ESP8266. - * [Communication between devices](./syncom_as/README.md) Enables MicroPython - boards to communicate without using a UART. This is hardware agnostic but - slower than the I2C version. - -## 1.2 The fast_io variant - -This comprises two parts. - 1. The [fast_io](./FASTPOLL.md) version of `uasyncio` is a "drop in" - replacement for the official version 2 providing bug fixes, additional - functionality and, in certain respects, higher performance. - 2. An optional extension module enabling the [fast_io](./FASTPOLL.md) version - to run with very low power draw. This is Pyboard-only including Pyboard D. - -Official `uasyncio` suffers from high levels of latency when scheduling I/O in -typical applications. It also has an issue which can cause bidirectional -devices such as UART's to block. The `fast_io` version fixes the bug. It also -provides a facility for reducing I/O latency which can substantially improve -the performance of stream I/O drivers. It provides other features aimed at -providing greater control over scheduling behaviour. - -To take advantage of the reduced latency device drivers should be written to -employ stream I/O. To operate at low latency they are simply run under the -`fast_io` version. The [tutorial](./TUTORIAL.md#64-writing-streaming-device-drivers) -has details of how to write streaming drivers. - -The current `fast_io` version 0.24 fixes an issue with task cancellation and -timeouts. In `uasyncio` version 2.0, where a coroutine is waiting on a -`sleep()` or on I/O, a timeout or cancellation is deferred until the coroutine -is next scheduled. This introduces uncertainty into when the coroutine is -stopped. - -## 1.2.1 A Pyboard-only low power module - -This is documented [here](./lowpower/README.md). In essence a Python file is -placed on the device which configures the `fast_io` version of `uasyncio` to -reduce power consumption at times when it is not busy. This provides a means of -using `uasyncio` in battery powered projects. This is decidedly experimental: -hopefully `uasyncio` V3 will introduce power saving in a less hacky manner. - -## 1.3 Under the hood - -[Under the hood](./UNDER_THE_HOOD.md) A guide to help understand the V2 -`uasyncio` code. For scheduler geeks and those wishing to modify `uasyncio`. - -## 1.4 Synchronisation Primitives - -All solutions listed below work with stock `uasyncio` V2 or `fast_io`. - -The CPython `asyncio` library supports these synchronisation primitives: - * `Lock` - * `Event` - * `gather` - * `Semaphore` and `BoundedSemaphore`. - * `Condition`. - * `Queue`. This was implemented by Paul Sokolvsky in `uasyncio.queues`. - -See [CPython docs](https://docs.python.org/3/library/asyncio-sync.html). - -The file `asyn.py` contains implementations of these, also - * `Barrier` An additional synchronisation primitive. - * Cancellation decorators and classes: these are workrounds for the bug where - in V2 cancellation does not occur promptly. - * Support for `gather`. - -The `Event` class in `asyn.py` provides a nonstandard option to supply a data -value to the `.set` method and to retrieve this with `.value`. It is also an -awaitable class. - -#### These are documented [here](./PRIMITIVES.md) - -## 1.5 Switches, Pushbuttons and Timeouts - -The file `aswitch.py` provides support for: - * `Delay_ms` A software retriggerable monostable or watchdog. - * `Switch` Debounced switch and pushbutton classes with callbacks. - * `Pushbutton` - -#### It is documented [here](./DRIVERS.md) - -# 2. Version 2.0 usage notes - -These notes are intended for users familiar with `asyncio` under CPython. - -The MicroPython language is based on CPython 3.4. The `uasyncio` library -supports a subset of the CPython 3.4 `asyncio` library with some V3.5 -extensions. In addition there are non-standard extensions to optimise services -such as millisecond level timing and task cancellation. Its design focus is on -high performance and scheduling is performed without RAM allocation. - -The `uasyncio` library supports the following Python 3.5 features: - - * `async def` and `await` syntax. - * Awaitable classes (using `__iter__` rather than `__await__`). - * Asynchronous context managers. - * Asynchronous iterators. - * Event loop methods `call_soon` and `call_later`. - * `sleep(seconds)`. - -It supports millisecond level timing with the following: - - * Event loop method `call_later_ms` - * uasyncio `sleep_ms(time)` - -`uasyncio` V2 supports coroutine timeouts and cancellation. - - * `wait_for(coro, t_secs)` runs `coro` with a timeout. - * `cancel(coro)` tags `coro` for cancellation when it is next scheduled. - -Classes `Task` and `Future` are not supported. - -## 2.1 Asynchronous I/O - -Asynchronous I/O (`StreamReader` and `StreamWriter` classes) support devices -with streaming drivers, such as UARTs and sockets. It is now possible to write -streaming device drivers in Python. - -## 2.2 Time values - -For timing asyncio uses floating point values of seconds. The `uasyncio.sleep` -method accepts floats (including sub-second values) or integers. Note that in -MicroPython the use of floats implies RAM allocation which incurs a performance -penalty. The design of `uasyncio` enables allocation-free scheduling. In -applications where performance is an issue, integers should be used and the -millisecond level functions (with integer arguments) employed where necessary. - -The `loop.time` method returns an integer number of milliseconds whereas -CPython returns a floating point number of seconds. `call_at` follows the -same convention. diff --git a/v2/TUTORIAL.md b/v2/TUTORIAL.md deleted file mode 100644 index 06b5b7b..0000000 --- a/v2/TUTORIAL.md +++ /dev/null @@ -1,2031 +0,0 @@ -# Application of uasyncio to hardware interfaces - -This tutorial is intended for users having varying levels of experience with -asyncio and includes a section for complete beginners. - -# Contents - - 0. [Introduction](./TUTORIAL.md#0-introduction) - 0.1 [Installing uasyncio on bare metal](./TUTORIAL.md#01-installing-uasyncio-on-bare-metal) - 1. [Cooperative scheduling](./TUTORIAL.md#1-cooperative-scheduling) - 1.1 [Modules](./TUTORIAL.md#11-modules) - 2. [uasyncio](./TUTORIAL.md#2-uasyncio) - 2.1 [Program structure: the event loop](./TUTORIAL.md#21-program-structure-the-event-loop) - 2.2 [Coroutines (coros)](./TUTORIAL.md#22-coroutines-coros) - 2.2.1 [Queueing a coro for scheduling](./TUTORIAL.md#221-queueing-a-coro-for-scheduling) - 2.2.2 [Running a callback function](./TUTORIAL.md#222-running-a-callback-function) - 2.2.3 [Notes](./TUTORIAL.md#223-notes) Coros as bound methods. Returning values. - 2.3 [Delays](./TUTORIAL.md#23-delays) - 3. [Synchronisation](./TUTORIAL.md#3-synchronisation) - 3.1 [Lock](./TUTORIAL.md#31-lock) - 3.1.1 [Locks and timeouts](./TUTORIAL.md#311-locks-and-timeouts) - 3.2 [Event](./TUTORIAL.md#32-event) - 3.2.1 [The event's value](./TUTORIAL.md#321-the-events-value) - 3.3 [Barrier](./TUTORIAL.md#33-barrier) - 3.4 [Semaphore](./TUTORIAL.md#34-semaphore) - 3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore) - 3.5 [Queue](./TUTORIAL.md#35-queue) - 3.6 [Other synchronisation primitives](./TUTORIAL.md#36-other-synchronisation-primitives) - 4. [Designing classes for asyncio](./TUTORIAL.md#4-designing-classes-for-asyncio) - 4.1 [Awaitable classes](./TUTORIAL.md#41-awaitable-classes) - 4.1.1 [Use in context managers](./TUTORIAL.md#411-use-in-context-managers) - 4.1.2 [Awaiting a coro](./TUTORIAL.md#412-awaiting-a-coro) - 4.2 [Asynchronous iterators](./TUTORIAL.md#42-asynchronous-iterators) - 4.3 [Asynchronous context managers](./TUTORIAL.md#43-asynchronous-context-managers) - 5. [Exceptions timeouts and cancellation](./TUTORIAL.md#5-exceptions-timeouts-and-cancellation) - 5.1 [Exceptions](./TUTORIAL.md#51-exceptions) - 5.2 [Cancellation and Timeouts](./TUTORIAL.md#52-cancellation-and-timeouts) - 5.2.1 [Task cancellation](./TUTORIAL.md#521-task-cancellation) - 5.2.2 [Coroutines with timeouts](./TUTORIAL.md#522-coroutines-with-timeouts) - 6. [Interfacing hardware](./TUTORIAL.md#6-interfacing-hardware) - 6.1 [Timing issues](./TUTORIAL.md#61-timing-issues) - 6.2 [Polling hardware with a coroutine](./TUTORIAL.md#62-polling-hardware-with-a-coroutine) - 6.3 [Using the stream mechanism](./TUTORIAL.md#63-using-the-stream-mechanism) - 6.3.1 [A UART driver example](./TUTORIAL.md#631-a-uart-driver-example) - 6.4 [Writing streaming device drivers](./TUTORIAL.md#64-writing-streaming-device-drivers) - 6.5 [A complete example: aremote.py](./TUTORIAL.md#65-a-complete-example-aremotepy) - A driver for an IR remote control receiver. - 6.6 [Driver for HTU21D](./TUTORIAL.md#66-htu21d-environment-sensor) A - temperature and humidity sensor. - 7. [Hints and tips](./TUTORIAL.md#7-hints-and-tips) - 7.1 [Program hangs](./TUTORIAL.md#71-program-hangs) - 7.2 [uasyncio retains state](./TUTORIAL.md#72-uasyncio-retains-state) - 7.3 [Garbage Collection](./TUTORIAL.md#73-garbage-collection) - 7.4 [Testing](./TUTORIAL.md#74-testing) - 7.5 [A common error](./TUTORIAL.md#75-a-common-error) This can be hard to find. - 7.6 [Socket programming](./TUTORIAL.md#76-socket-programming) - 7.6.1 [WiFi issues](./TUTORIAL.md#761-wifi-issues) - 7.7 [Event loop constructor args](./TUTORIAL.md#77-event-loop-constructor-args) - 8. [Notes for beginners](./TUTORIAL.md#8-notes-for-beginners) - 8.1 [Problem 1: event loops](./TUTORIAL.md#81-problem-1:-event-loops) - 8.2 [Problem 2: blocking methods](./TUTORIAL.md#8-problem-2:-blocking-methods) - 8.3 [The uasyncio approach](./TUTORIAL.md#83-the-uasyncio-approach) - 8.4 [Scheduling in uasyncio](./TUTORIAL.md#84-scheduling-in-uasyncio) - 8.5 [Why cooperative rather than pre-emptive?](./TUTORIAL.md#85-why-cooperative-rather-than-pre-emptive) - 8.6 [Communication](./TUTORIAL.md#86-communication) - 8.7 [Polling](./TUTORIAL.md#87-polling) - -###### [Main README](./README.md) - -# 0. Introduction - -Most of this document assumes some familiarity with asynchronous programming. -For those new to it an introduction may be found -[in section 7](./TUTORIAL.md#8-notes-for-beginners). - -The MicroPython `uasyncio` library comprises a subset of Python's `asyncio` -library. It is designed for use on microcontrollers. As such it has a small RAM -footprint and fast context switching with zero RAM allocation. This document -describes its use with a focus on interfacing hardware devices. The aim is to -design drivers in such a way that the application continues to run while the -driver is awaiting a response from the hardware. The application remains -responsive to events and to user interaction. - -Another major application area for asyncio is in network programming: many -guides to this may be found online. - -Note that MicroPython is based on Python 3.4 with minimal Python 3.5 additions. -Except where detailed below, `asyncio` features of versions >3.4 are -unsupported. As stated above it is a subset; this document identifies supported -features. - -This tutorial aims to present a consistent programming style compatible with -CPython V3.5 and above. - -## 0.1 Installing uasyncio on bare metal - -It is recommended to use MicroPython firmware V1.11 or later. On many platforms -no installation is necessary as `uasyncio` is compiled into the build. Test by -issuing -```python -import uasyncio -``` -at the REPL. - -The following instructions cover cases where modules are not pre-installed. The -`queues` and `synchro` modules are optional, but are required to run all the -examples below. - -#### Hardware with internet connectivity - -On hardware with an internet connection and running firmware V1.11 or greater -installation may be done using `upip`, which is pre-installed. After ensuring -that the device is connected to your network issue: -```python -import upip -upip.install('micropython-uasyncio') -upip.install('micropython-uasyncio.synchro') -upip.install('micropython-uasyncio.queues') -``` -Error meesages from `upip` are not too helpful. If you get an obscure error, -double check your internet connection. - -#### Hardware without internet connectivity (micropip) - -On hardware which lacks an internet connection (such as a Pyboard V1.x) the -easiest way is to run `micropip.py` on a PC to install to a directory of your -choice, then to copy the resultant directory structure to the target hardware. -The `micropip.py` utility runs under Python 3.2 or above and runs under Linux, -Windows and OSX. It may be found -[here](https://github.com/peterhinch/micropython-samples/tree/master/micropip). - -Typical invocation: -```bash -$ micropip.py install -p ~/rats micropython-uasyncio -$ micropip.py install -p ~/rats micropython-uasyncio.synchro -$ micropip.py install -p ~/rats micropython-uasyncio.queues -``` - -#### Hardware without internet connectivity (copy source) - -If `micropip.py` is not to be used the files should be copied from source. The -following instructions describe copying the bare minimum of files to a target -device, also the case where `uasyncio` is to be frozen into a compiled build as -bytecode. For the latest release compatible with official firmware -files must be copied from the official -[micropython-lib](https://github.com/micropython/micropython-lib). - -Clone the library to a PC with -```bash -$ git clone https://github.com/micropython/micropython-lib.git -``` -On the target hardware create a `uasyncio` directory (optionally under a -directory `lib`) and copy the following files to it: - * `uasyncio/uasyncio/__init__.py` - * `uasyncio.core/uasyncio/core.py` - * `uasyncio.synchro/uasyncio/synchro.py` - * `uasyncio.queues/uasyncio/queues.py` - -The `uasyncio` modules may be frozen as bytecode in the usual way, by placing -the `uasyncio` directory and its contents in the port's `modules` directory and -rebuilding. - -###### [Main README](./README.md) - -# 1. Cooperative scheduling - -The technique of cooperative multi-tasking is widely used in embedded systems. -It offers lower overheads than pre-emptive scheduling and avoids many of the -pitfalls associated with truly asynchronous threads of execution. - -###### [Contents](./TUTORIAL.md#contents) - -## 1.1 Modules - -The following modules are provided which may be copied to the target hardware. - -**Libraries** - - 1. [asyn.py](./asyn.py) Provides synchronisation primitives `Lock`, `Event`, - `Barrier`, `Semaphore`, `BoundedSemaphore`, `Condition` and `gather`. Provides - support for task cancellation via `NamedTask` and `Cancellable` classes. - 2. [aswitch.py](./aswitch.py) Provides classes for interfacing switches and - pushbuttons and also a software retriggerable delay object. Pushbuttons are a - generalisation of switches providing logical rather than physical status along - with double-clicked and long pressed events. - -**Demo Programs** - -The first two are the most immediately rewarding as they produce visible -results by accessing Pyboard hardware. - - 1. [aledflash.py](./aledflash.py) Flashes the four Pyboard LEDs asynchronously - for 10s. The simplest uasyncio demo. Import it to run. - 2. [apoll.py](./apoll.py) A device driver for the Pyboard accelerometer. - Demonstrates the use of a coroutine to poll a device. Runs for 20s. Import it - to run. Requires a Pyboard V1.x. - 3. [astests.py](./astests.py) Test/demonstration programs for the - [aswitch](./aswitch) module. - 4. [asyn_demos.py](./asyn_demos.py) Simple task cancellation demos. - 5. [roundrobin.py](./roundrobin.py) Demo of round-robin scheduling. Also a - benchmark of scheduling performance. - 6. [awaitable.py](./awaitable.py) Demo of an awaitable class. One way of - implementing a device driver which polls an interface. - 7. [chain.py](./chain.py) Copied from the Python docs. Demo of chaining - coroutines. - 8. [aqtest.py](./aqtest.py) Demo of uasyncio `Queue` class. - 9. [aremote.py](./aremote.py) Example device driver for NEC protocol IR remote - control. - 10. [auart.py](./auart.py) Demo of streaming I/O via a Pyboard UART. - 11. [auart_hd.py](./auart_hd.py) Use of the Pyboard UART to communicate with a - device using a half-duplex protocol. Suits devices such as those using the - 'AT' modem command set. - 12. [iorw.py](./iorw.py) Demo of a read/write device driver using the stream - I/O mechanism. - -**Test Programs** - - 1. [asyntest.py](./asyntest.py) Tests for the synchronisation primitives in - [asyn.py](./asyn.py). - 2. [cantest.py](./cantest.py) Task cancellation tests. - -**Utility** - - 1. [check_async_code.py](./check_async_code.py) A Python3 utility to locate a - particular coding error which can be hard to find. See - [para 7.5](./TUTORIAL.md#75-a-common-error). - -**Benchmarks** - -The `benchmarks` directory contains scripts to test and characterise the -uasyncio scheduler. See [this doc](./FASTPOLL.md). - -###### [Contents](./TUTORIAL.md#contents) - -# 2. uasyncio - -The asyncio concept is of cooperative multi-tasking based on coroutines, -referred to in this document as coros or tasks. - -###### [Contents](./TUTORIAL.md#contents) - -## 2.1 Program structure: the event loop - -Consider the following example: - -```python -import uasyncio as asyncio -async def bar(): - count = 0 - while True: - count += 1 - print(count) - await asyncio.sleep(1) # Pause 1s - -loop = asyncio.get_event_loop() -loop.create_task(bar()) # Schedule ASAP -loop.run_forever() -``` - -Program execution proceeds normally until the call to `loop.run_forever`. At -this point execution is controlled by the scheduler. A line after -`loop.run_forever` would never be executed. The scheduler runs `bar` -because this has been placed on the scheduler's queue by `loop.create_task`. -In this trivial example there is only one coro: `bar`. If there were others, -the scheduler would schedule them in periods when `bar` was paused. - -Most embedded applications have an event loop which runs continuously. The event -loop can also be started in a way which permits termination, by using the event -loop's `run_until_complete` method; this is mainly of use in testing. Examples -may be found in the [astests.py](./astests.py) module. - -The event loop instance is a singleton, instantiated by a program's first call -to `asyncio.get_event_loop()`. This takes two optional integer args being the -lengths of the two coro queues. Typically both will have the same value being -at least the number of concurrent coros in the application. The default of 16 -is usually sufficient. If using non-default values see -[Event loop constructor args](./TUTORIAL.md#77-event-loop-constructor-args). - -If a coro needs to call an event loop method (usually `create_task`), calling -`asyncio.get_event_loop()` (without args) will efficiently return it. - -###### [Contents](./TUTORIAL.md#contents) - -## 2.2 Coroutines (coros) - -A coro is instantiated as follows: - -```python -async def foo(delay_secs): - await asyncio.sleep(delay_secs) - print('Hello') -``` - -A coro can allow other coroutines to run by means of the `await coro` -statement. A coro usually contains a `await` statement. The `await` causes -the called `coro` to run to completion before execution passes to the next -instruction. - -Consider these lines of code: - -```python -await asyncio.sleep(delay_secs) -await asyncio.sleep(0) -``` - -The first causes the code to pause for the duration of the delay, with other -coros being scheduled for the duration. A delay of 0 causes any pending coros -to be scheduled in round-robin fashion before the following line is run. See -the `roundrobin.py` example. - -###### [Contents](./TUTORIAL.md#contents) - -### 2.2.1 Queueing a coro for scheduling - - * `EventLoop.create_task` Arg: the coro to run. The scheduler queues the - coro to run ASAP. The `create_task` call returns immediately. The coro - arg is specified with function call syntax with any required arguments passed. - * `EventLoop.run_until_complete` Arg: the coro to run. The scheduler queues - the coro to run ASAP. The coro arg is specified with function call syntax with - any required arguments passed. The `run_until_complete` call returns when - the coro terminates: this method provides a way of quitting the scheduler. - * `await` Arg: the coro to run, specified with function call syntax. Starts - the coro ASAP. The awaiting coro blocks until the awaited one has run to - completion. - -The above are compatible with CPython. Additional uasyncio methods are -discussed in 2.2.3 below. - -###### [Contents](./TUTORIAL.md#contents) - -### 2.2.2 Running a callback function - -Callbacks should be Python functions designed to complete in a short period of -time. This is because coroutines will have no opportunity to run for the -duration. - -The following `EventLoop` methods schedule callbacks: - - 1. `call_soon` Call as soon as possible. Args: `callback` the callback to - run, `*args` any positional args may follow separated by commas. - 2. `call_later` Call after a delay in secs. Args: `delay`, `callback`, - `*args` - 3. `call_later_ms` Call after a delay in ms. Args: `delay`, `callback`, - `*args`. - -```python -loop = asyncio.get_event_loop() -loop.call_soon(foo, 5) # Schedule callback 'foo' ASAP with an arg of 5. -loop.call_later(2, foo, 5) # Schedule after 2 seconds. -loop.call_later_ms(50, foo, 5) # Schedule after 50ms. -loop.run_forever() -``` - -###### [Contents](./TUTORIAL.md#contents) - -### 2.2.3 Notes - -A coro can contain a `return` statement with arbitrary return values. To -retrieve them issue: - -```python -result = await my_coro() -``` - -Coros may be bound methods. A coro must contain at least one `await` statement. - -###### [Contents](./TUTORIAL.md#contents) - -## 2.3 Delays - -Where a delay is required in a coro there are two options. For longer delays and -those where the duration need not be precise, the following should be used: - -```python -async def foo(delay_secs, delay_ms): - await asyncio.sleep(delay_secs) - print('Hello') - await asyncio.sleep_ms(delay_ms) -``` - -While these delays are in progress the scheduler will schedule other coros. -This is generally highly desirable, but it does introduce uncertainty in the -timing as the calling routine will only be rescheduled when the one running at -the appropriate time has yielded. The amount of latency depends on the design -of the application, but is likely to be on the order of tens or hundreds of ms; -this is discussed further in [Section 6](./TUTORIAL.md#6-interfacing-hardware). - -Very precise delays may be issued by using the `utime` functions `sleep_ms` -and `sleep_us`. These are best suited for short delays as the scheduler will -be unable to schedule other coros while the delay is in progress. - -###### [Contents](./TUTORIAL.md#contents) - -# 3 Synchronisation - -There is often a need to provide synchronisation between coros. A common -example is to avoid what are known as "race conditions" where multiple coros -compete to access a single resource. An example is provided in the -[astests.py](./astests.py) program and discussed in [the docs](./DRIVERS.md). -Another hazard is the "deadly embrace" where two coros each wait on the other's -completion. - -In simple applications communication may be achieved with global flags or bound -variables. A more elegant approach is to use synchronisation primitives. The -module -[asyn.py](https://github.com/peterhinch/micropython-async/blob/master/asyn.py) -offers "micro" implementations of `Event`, `Barrier`, `Semaphore` and -`Condition` primitives. These are for use only with asyncio. They are not -thread safe and should not be used with the `_thread` module or from an -interrupt handler except where mentioned. A `Lock` primitive is provided which -is an alternative to the official implementation. - -Another synchronisation issue arises with producer and consumer coros. The -producer generates data which the consumer uses. Asyncio provides the `Queue` -object. The producer puts data onto the queue while the consumer waits for its -arrival (with other coros getting scheduled for the duration). The `Queue` -guarantees that items are removed in the order in which they were received. -Alternatively a `Barrier` instance can be used if the producer must wait -until the consumer is ready to access the data. - -The following provides a brief overview of the primitives. Full documentation -may be found [here](./PRIMITIVES.md). - -###### [Contents](./TUTORIAL.md#contents) - -## 3.1 Lock - -This describes the use of the official `Lock` primitive. - -This guarantees unique access to a shared resource. In the following code -sample a `Lock` instance `lock` has been created and is passed to all coros -wishing to access the shared resource. Each coro attempts to acquire the lock, -pausing execution until it succeeds. - -```python -import uasyncio as asyncio -from uasyncio.synchro import Lock - -async def task(i, lock): - while 1: - await lock.acquire() - print("Acquired lock in task", i) - await asyncio.sleep(0.5) - lock.release() - -async def killer(): - await asyncio.sleep(10) - -loop = asyncio.get_event_loop() - -lock = Lock() # The global Lock instance - -loop.create_task(task(1, lock)) -loop.create_task(task(2, lock)) -loop.create_task(task(3, lock)) - -loop.run_until_complete(killer()) # Run for 10s -``` - -### 3.1.1 Locks and timeouts - -At time of writing (5th Jan 2018) the official `Lock` class is not complete. -If a coro is subject to a [timeout](./TUTORIAL.md#522-coroutines-with-timeouts) -and the timeout is triggered while it is waiting on a lock, the timeout will be -ineffective. It will not receive the `TimeoutError` until it has acquired the -lock. The same observation applies to task cancellation. - -The module [asyn.py](./asyn.py) offers a `Lock` class which works in these -situations [see docs](./PRIMITIVES.md#32-class-lock). It is significantly less -efficient than the official class but supports additional interfaces as per the -CPython version including context manager usage. - -###### [Contents](./TUTORIAL.md#contents) - -## 3.2 Event - -This provides a way for one or more coros to pause until another flags them to -continue. An `Event` object is instantiated and made accessible to all coros -using it: - -```python -import asyn -event = asyn.Event() -``` - -Coros waiting on the event issue `await event` whereupon execution pauses until -another issues `event.set()`. [Full details.](./PRIMITIVES.md#33-class-event) - -This presents a problem if `event.set()` is issued in a looping construct; the -code must wait until the event has been accessed by all waiting coros before -setting it again. In the case where a single coro is awaiting the event this -can be achieved by the receiving coro clearing the event: - -```python -async def eventwait(event): - await event - event.clear() -``` - -The coro raising the event checks that it has been serviced: - -```python -async def foo(event): - while True: - # Acquire data from somewhere - while event.is_set(): - await asyncio.sleep(1) # Wait for coro to respond - event.set() -``` - -Where multiple coros wait on a single event synchronisation can be achieved by -means of an acknowledge event. Each coro needs a separate event. - -```python -async def eventwait(event, ack_event): - await event - ack_event.set() -``` - -An example of this is provided in the `event_test` function in `asyntest.py`. -This is cumbersome. In most cases - even those with a single waiting coro - the -Barrier class below offers a simpler approach. - -An Event can also provide a means of communication between an interrupt handler -and a coro. The handler services the hardware and sets an event which is tested -in slow time by the coro. - -###### [Contents](./TUTORIAL.md#contents) - -### 3.2.1 The event's value - -The `event.set()` method can accept an optional data value of any type. A -coro waiting on the event can retrieve it by means of `event.value()`. Note -that `event.clear()` will set the value to `None`. A typical use for this -is for the coro setting the event to issue `event.set(utime.ticks_ms())`. Any -coro waiting on the event can determine the latency incurred, for example to -perform compensation for this. - -###### [Contents](./TUTORIAL.md#contents) - -## 3.3 Barrier - -This has two uses. Firstly it can cause a coro to pause until one or more other -coros have terminated. - -Secondly it enables multiple coros to rendezvous at a particular point. For -example producer and consumer coros can synchronise at a point where the -producer has data available and the consumer is ready to use it. At that point -in time the `Barrier` can run an optional callback before the barrier is -released and all waiting coros can continue. [Full details.](./PRIMITIVES.md#34-class-barrier) - -The callback can be a function or a coro. In most applications a function is -likely to be used: this can be guaranteed to run to completion before the -barrier is released. - -An example is the `barrier_test` function in `asyntest.py`. In the code -fragment from that program: - -```python -import asyn - -def callback(text): - print(text) - -barrier = asyn.Barrier(3, callback, ('Synch',)) - -async def report(): - for i in range(5): - print('{} '.format(i), end='') - await barrier -``` - -multiple instances of `report` print their result and pause until the other -instances are also complete and waiting on `barrier`. At that point the -callback runs. On its completion the coros resume. - -###### [Contents](./TUTORIAL.md#contents) - -## 3.4 Semaphore - -A semaphore limits the number of coros which can access a resource. It can be -used to limit the number of instances of a particular coro which can run -concurrently. It performs this using an access counter which is initialised by -the constructor and decremented each time a coro acquires the semaphore. -[Full details.](./PRIMITIVES.md#35-class-semaphore) - -The easiest way to use it is with a context manager: - -```python -import asyn -sema = asyn.Semaphore(3) -async def foo(sema): - async with sema: - # Limited access here -``` -An example is the `semaphore_test` function in `asyntest.py`. - -###### [Contents](./TUTORIAL.md#contents) - -### 3.4.1 BoundedSemaphore - -This works identically to the `Semaphore` class except that if the `release` -method causes the access counter to exceed its initial value, a `ValueError` -is raised. [Full details.](./PRIMITIVES.md#351-class-boundedsemaphore) - -###### [Contents](./TUTORIAL.md#contents) - -## 3.5 Queue - -The `Queue` class is officially supported and the sample program `aqtest.py` -demonstrates its use. A queue is instantiated as follows: - -```python -from uasyncio.queues import Queue -q = Queue() -``` - -A typical producer coro might work as follows: - -```python -async def producer(q): - while True: - result = await slow_process() # somehow get some data - await q.put(result) # may pause if a size limited queue fills -``` - -and the consumer works along these lines: - -```python -async def consumer(q): - while True: - result = await(q.get()) # Will pause if q is empty - print('Result was {}'.format(result)) -``` - -The `Queue` class provides significant additional functionality in that the -size of queues may be limited and the status may be interrogated. The behaviour -on empty status and (where size is limited) the behaviour on full status may be -controlled. Documentation of this is in the code. - -###### [Contents](./TUTORIAL.md#contents) - -## 3.6 Other synchronisation primitives - -The [asyn.py](./asyn.py) library provides 'micro' implementations of CPython -capabilities, namely the [Condition class](./PRIMITIVES.md#36-class-condition) -and the [gather](./PRIMITIVES.md#37-class-gather) method. - -The `Condition` class enables a coro to notify other coros which are waiting on -a locked resource. Once notified they will access the resource and release the -lock in turn. The notifying coro can limit the number of coros to be notified. - -The CPython `gather` method enables a list of coros to be launched. When the -last has completed a list of results is returned. This 'micro' implementation -uses different syntax. Timeouts may be applied to any of the coros. - -###### [Contents](./TUTORIAL.md#contents) - -# 4 Designing classes for asyncio - -In the context of device drivers the aim is to ensure nonblocking operation. -The design should ensure that other coros get scheduled in periods while the -driver is waiting for the hardware. For example a task awaiting data arriving -on a UART or a user pressing a button should allow other coros to be scheduled -until the event occurs.. - -###### [Contents](./TUTORIAL.md#contents) - -## 4.1 Awaitable classes - -A coro can pause execution by waiting on an `awaitable` object. Under CPython -a custom class is made `awaitable` by implementing an `__await__` special -method. This returns a generator. An `awaitable` class is used as follows: - -```python -import uasyncio as asyncio - -class Foo(): - def __await__(self): - for n in range(5): - print('__await__ called') - yield from asyncio.sleep(1) # Other coros get scheduled here - return 42 - - __iter__ = __await__ # See note below - -async def bar(): - foo = Foo() # Foo is an awaitable class - print('waiting for foo') - res = await foo # Retrieve result - print('done', res) - -loop = asyncio.get_event_loop() -loop.run_until_complete(bar()) -``` - -Currently MicroPython doesn't support `__await__` -[issue #2678](https://github.com/micropython/micropython/issues/2678) and -`__iter__` must be used. The line `__iter__ = __await__` enables portability -between CPython and MicroPython. Example code may be found in the `Event`, -`Barrier`, `Cancellable` and `Condition` classes in [asyn.py](./asyn.py). - -### 4.1.1 Use in context managers - -Awaitable objects can be used in synchronous or asynchronous CM's by providing -the necessary special methods. The syntax is: - -```python -with await awaitable as a: # The 'as' clause is optional - # code omitted -async with awaitable as a: # Asynchronous CM (see below) - # do something -``` - -To achieve this the `__await__` generator should return `self`. This is passed -to any variable in an `as` clause and also enables the special methods to work. -See `asyn.Condition` and `asyntest.condition_test`, where the `Condition` class -is awaitable and may be used in a synchronous CM. - -###### [Contents](./TUTORIAL.md#contents) - -### 4.1.2 Awaiting a coro - -The Python language requires that `__await__` is a generator function. In -MicroPython generators and coroutines are identical, so the solution is to use -`yield from coro(args)`. - -This tutorial aims to offer code portable to CPython 3.5 or above. In CPython -coroutines and generators are distinct. CPython coros have an `__await__` -special method which retrieves a generator. This is portable: - -```python -up = False # Running under MicroPython? -try: - import uasyncio as asyncio - up = True # Or can use sys.implementation.name -except ImportError: - import asyncio - -async def times_two(n): # Coro to await - await asyncio.sleep(1) - return 2 * n - -class Foo(): - def __await__(self): - res = 1 - for n in range(5): - print('__await__ called') - if up: # MicroPython - res = yield from times_two(res) - else: # CPython - res = yield from times_two(res).__await__() - return res - - __iter__ = __await__ - -async def bar(): - foo = Foo() # foo is awaitable - print('waiting for foo') - res = await foo # Retrieve value - print('done', res) - -loop = asyncio.get_event_loop() -loop.run_until_complete(bar()) -``` - -Note that, in `__await__`, `yield from asyncio.sleep(1)` is allowed by CPython. -I haven't yet established how this is achieved. - -###### [Contents](./TUTORIAL.md#contents) - -## 4.2 Asynchronous iterators - -These provide a means of returning a finite or infinite sequence of values -and could be used as a means of retrieving successive data items as they arrive -from a read-only device. An asynchronous iterable calls asynchronous code in -its `next` method. The class must conform to the following requirements: - - * It has an `__aiter__` method defined with `async def`and returning the - asynchronous iterator. - * It has an ` __anext__` method which is a coro - i.e. defined with - `async def` and containing at least one `await` statement. To stop - iteration it must raise a `StopAsyncIteration` exception. - -Successive values are retrieved with `async for` as below: - -```python -class AsyncIterable: - def __init__(self): - self.data = (1, 2, 3, 4, 5) - self.index = 0 - - async def __aiter__(self): - return self - - async def __anext__(self): - data = await self.fetch_data() - if data: - return data - else: - raise StopAsyncIteration - - async def fetch_data(self): - await asyncio.sleep(0.1) # Other coros get to run - if self.index >= len(self.data): - return None - x = self.data[self.index] - self.index += 1 - return x - -async def run(): - ai = AsyncIterable() - async for x in ai: - print(x) -``` - -###### [Contents](./TUTORIAL.md#contents) - -## 4.3 Asynchronous context managers - -Classes can be designed to support asynchronous context managers. These are CM's -having enter and exit procedures which are coros. An example is the `Lock` -class described above. This has an `__aenter__` coro which is logically -required to run asynchronously. To support the asynchronous CM protocol its -`__aexit__` method also must be a coro, achieved by including -`await asyncio.sleep(0)`. Such classes are accessed from within a coro with -the following syntax: - -```python -async def bar(lock): - async with lock: - print('bar acquired lock') -``` - -As with normal context managers an exit method is guaranteed to be called when -the context manager terminates, whether normally or via an exception. To -achieve this the special methods `__aenter__` and `__aexit__` must be -defined, both being coros waiting on a coro or `awaitable` object. This example -comes from the `Lock` class: - -```python - async def __aenter__(self): - await self.acquire() # a coro defined with async def - return self - - async def __aexit__(self, *args): - self.release() # A conventional method - await asyncio.sleep_ms(0) -``` - -If the `async with` has an `as variable` clause the variable receives the -value returned by `__aenter__`. - -To ensure correct behaviour firmware should be V1.9.10 or later. - -###### [Contents](./TUTORIAL.md#contents) - -# 5 Exceptions timeouts and cancellation - -These topics are related: `uasyncio` enables the cancellation of tasks, and the -application of a timeout to a task, by throwing an exception to the task in a -special way. - -## 5.1 Exceptions - -Where an exception occurs in a coro, it should be trapped either in that coro -or in a coro which is awaiting its completion. This ensures that the exception -is not propagated to the scheduler. If this occurred the scheduler would stop -running, passing the exception to the code which started the scheduler. -Consequently, to avoid stopping the scheduler, coros launched with -`loop.create_task()` must trap any exceptions internally. - -Using `throw` or `close` to throw an exception to a coro is unwise. It subverts -`uasyncio` by forcing the coro to run, and possibly terminate, when it is still -queued for execution. - -There is a "gotcha" illustrated by this code sample. If allowed to run to -completion it works as expected. - -```python -import uasyncio as asyncio -async def foo(): - await asyncio.sleep(3) - print('About to throw exception.') - 1/0 - -async def bar(): - try: - await foo() - except ZeroDivisionError: - print('foo was interrupted by zero division') # Happens - raise # Force shutdown to run by propagating to loop. - except KeyboardInterrupt: - print('foo was interrupted by ctrl-c') # NEVER HAPPENS - raise - -async def shutdown(): - print('Shutdown is running.') # Happens in both cases - await asyncio.sleep(1) - print('done') - -loop = asyncio.get_event_loop() -try: - loop.run_until_complete(bar()) -except ZeroDivisionError: - loop.run_until_complete(shutdown()) -except KeyboardInterrupt: - print('Keyboard interrupt at loop level.') - loop.run_until_complete(shutdown()) -``` - -However issuing a keyboard interrupt causes the exception to go to the event -loop. This is because `uasyncio.sleep` causes execution to be transferred to -the event loop. Consequently applications requiring cleanup code in response to -a keyboard interrupt should trap the exception at the event loop level. - -###### [Contents](./TUTORIAL.md#contents) - -## 5.2 Cancellation and Timeouts - -As stated above, these features work by throwing an exception to a task in a -special way, using a MicroPython specific coro method `pend_throw`. The way -this works is version dependent. In official `uasyncio` V2.0 the exception is -not processed until the task is next scheduled. This imposes latency if the -task is waiting on a `sleep` or on I/O. Timeouts may extend beyond their -nominal period. Task cancelling other tasks cannot determine when cancellation -is complete. - -There is currently a wokround and two solutions. - * Workround: the `asyn` library provides means of waiting on cancellation of - tasks or groups of tasks. See [Task Cancellation](./PRIMITIVES.md#4-task-cancellation). - * [Paul Sokolovsky's library fork](https://github.com/pfalcon/micropython-lib) - provides `uasyncio` V2.4, but this requires his - [Pycopy](https://github.com/pfalcon/micropython) firmware. - * The [fast_io](./FASTPOLL.md) fork of `uasyncio` solves this in Python (in a - less elegant manner) and runs under official firmware. - -The exception hierarchy used here is `Exception-CancelledError-TimeoutError`. - -## 5.2.1 Task cancellation - -`uasyncio` provides a `cancel(coro)` function. This works by throwing an -exception to the coro using `pend_throw`. This works with nested coros. Usage -is as follows: -```python -async def foo(): - while True: - # do something every 10 secs - await asyncio.sleep(10) - -async def bar(loop): - foo_instance = foo() # Create a coroutine instance - loop.create_task(foo_instance) - # code omitted - asyncio.cancel(foo_instance) -``` -If this example is run against `uasyncio` V2.0, when `bar` issues `cancel` it -will not take effect until `foo` is next scheduled. There is thus a latency of -up to 10s in the cancellation of `foo`. Another source of latency would arise -if `foo` waited on I/O. Where latency arises, `bar` cannot determine whether -`foo` has yet been cancelled. This matters in some use-cases. - -Using the Paul Sokolovsky fork or `fast_io` a simple `sleep(0)` suffices: -```python -async def foo(): - while True: - # do something every 10 secs - await asyncio.sleep(10) - -async def bar(loop): - foo_instance = foo() # Create a coroutine instance - loop.create_task(foo_instance) - # code omitted - asyncio.cancel(foo_instance) - await asyncio.sleep(0) - # Task is now cancelled -``` -This would also work in `uasyncio` V2.0 if `foo` (and any coros awaited by -`foo`) never issued `sleep` or waited on I/O. - -Behaviour which may surprise the unwary arises when a coro to be cancelled is -awaited rather than being launched by `create_task`. Consider this fragment: - -```python -async def foo(): - while True: - # do something every 10 secs - await asyncio.sleep(10) - -async def foo_runner(foo_instance): - await foo_instance - print('This will not be printed') - -async def bar(loop): - foo_instance = foo() - loop.create_task(foo_runner(foo_instance)) - # code omitted - asyncio.cancel(foo_instance) -``` -When `foo` is cancelled it is removed from the scheduler's queue; because it -lacks a `return` statement the calling routine `foo_runner` never resumes. It -is recommended always to trap the exception in the outermost scope of a -function subject to cancellation: -```python -async def foo(): - try: - while True: - await asyncio.sleep(10) - await my_coro - except asyncio.CancelledError: - return -``` -In this instance `my_coro` does not need to trap the exception as it will be -propagated to the calling coro and trapped there. - -**Note** It is bad practice to issue the `close` or `throw` methods of a -de-scheduled coro. This subverts the scheduler by causing the coro to execute -code even though descheduled. This is likely to have unwanted consequences. - -###### [Contents](./TUTORIAL.md#contents) - -## 5.2.2 Coroutines with timeouts - -Timeouts are implemented by means of `uasyncio` methods `.wait_for()` and -`.wait_for_ms()`. These take as arguments a coroutine and a timeout in seconds -or ms respectively. If the timeout expires a `TimeoutError` will be thrown to -the coro using `pend_throw`. This exception must be trapped, either by the coro -or its caller. This is for the reason discussed above: if a coro times out it -is descheduled. Unless it traps the error and returns the only way the caller -can proceed is by trapping the exception itself. - -Where the exception is trapped by the coro, I have experienced obscure failures -if the exception is not trapped in the outermost scope as below: -```python -import uasyncio as asyncio - -async def forever(): - try: - print('Starting') - while True: - await asyncio.sleep_ms(300) - print('Got here') - except asyncio.TimeoutError: - print('Got timeout') # And return - -async def foo(): - await asyncio.wait_for(forever(), 5) - await asyncio.sleep(2) - -loop = asyncio.get_event_loop() -loop.run_until_complete(foo()) -``` -Alternatively it may be trapped by the caller: -```python -import uasyncio as asyncio - -async def forever(): - print('Starting') - while True: - await asyncio.sleep_ms(300) - print('Got here') - -async def foo(): - try: - await asyncio.wait_for(forever(), 5) - except asyncio.TimeoutError: - pass - print('Timeout elapsed.') - await asyncio.sleep(2) - -loop = asyncio.get_event_loop() -loop.run_until_complete(foo()) -``` - -#### Uasyncio V2.0 note - -This does not apply to the Paul Sokolovsky fork or to `fast_io`. - -If the coro issues `await asyncio.sleep(t)` where `t` is a long delay, the coro -will not be rescheduled until `t` has elapsed. If the timeout has elapsed -before the `sleep` is complete the `TimeoutError` will occur when the coro is -scheduled - i.e. when `t` has elapsed. In real time and from the point of view -of the calling coro, its response to the `TimeoutError` will be delayed. - -If this matters to the application, create a long delay by awaiting a short one -in a loop. The coro `asyn.sleep` [supports this](./PRIMITIVES.md#41-coro-sleep). - -###### [Contents](./TUTORIAL.md#contents) - -# 6 Interfacing hardware - -At heart all interfaces between `uasyncio` and external asynchronous events -rely on polling. Hardware requiring a fast response may use an interrupt. But -the interface between the interrupt service routine (ISR) and a user coro will -be polled. For example the ISR might trigger an `Event` or set a global flag, -while a coroutine awaiting the outcome polls the object each time it is -scheduled. - -Polling may be effected in two ways, explicitly or implicitly. The latter is -performed by using the `stream I/O` mechanism which is a system designed for -stream devices such as UARTs and sockets. At its simplest explicit polling may -consist of code like this: - -```python -async def poll_my_device(): - global my_flag # Set by device ISR - while True: - if my_flag: - my_flag = False - # service the device - await asyncio.sleep(0) -``` - -In place of a global, an instance variable, an `Event` object or an instance of -an awaitable class might be used. Explicit polling is discussed -further [below](./TUTORIAL.md#62-polling-hardware-with-a-coroutine). - -Implicit polling consists of designing the driver to behave like a stream I/O -device such as a socket or UART, using `stream I/O`. This polls devices using -Python's `select.poll` system: because the polling is done in C it is faster -and more efficient than explicit polling. The use of `stream I/O` is discussed -[here](./TUTORIAL.md#63-using-the-stream-mechanism). - -Owing to its efficiency implicit polling benefits most fast I/O device drivers: -streaming drivers can be written for many devices not normally considered as -streaming devices [section 6.4](./TUTORIAL.md#64-writing-streaming-device-drivers). - -###### [Contents](./TUTORIAL.md#contents) - -## 6.1 Timing issues - -Both explicit and implicit polling are currently based on round-robin -scheduling. Assume I/O is operating concurrently with N user coros each of -which yields with a zero delay. When I/O has been serviced it will next be -polled once all user coros have been scheduled. The implied latency needs to be -considered in the design. I/O channels may require buffering, with an ISR -servicing the hardware in real time from buffers and coroutines filling or -emptying the buffers in slower time. - -The possibility of overrun also needs to be considered: this is the case where -something being polled by a coroutine occurs more than once before the coro is -actually scheduled. - -Another timing issue is the accuracy of delays. If a coro issues - -```python - await asyncio.sleep_ms(t) - # next line -``` - -the scheduler guarantees that execution will pause for at least `t`ms. The -actual delay may be greater depending on the system state when `t` expires. -If, at that time, all other coros are waiting on nonzero delays, the next line -will immediately be scheduled. But if other coros are pending execution (either -because they issued a zero delay or because their time has also elapsed) they -may be scheduled first. This introduces a timing uncertainty into the `sleep()` -and `sleep_ms()` functions. The worst-case value for this overrun may be -calculated by summing, for every other coro, the worst-case execution time -between yielding to the scheduler. - -The [fast_io](./FASTPOLL.md) version of `uasyncio` in this repo provides a way -to ensure that stream I/O is polled on every iteration of the scheduler. It is -hoped that official `uasyncio` will adopt code to this effect in due course. - -###### [Contents](./TUTORIAL.md#contents) - -## 6.2 Polling hardware with a coroutine - -This is a simple approach, but is most appropriate to hardware which may be -polled at a relatively low rate. This is primarily because polling with a short -(or zero) polling interval may cause the coro to consume more processor time -than is desirable. - -The example `apoll.py` demonstrates this approach by polling the Pyboard -accelerometer at 100ms intervals. It performs some simple filtering to ignore -noisy samples and prints a message every two seconds if the board is not moved. - -Further examples may be found in `aswitch.py` which provides drivers for -switch and pushbutton devices. - -An example of a driver for a device capable of reading and writing is shown -below. For ease of testing Pyboard UART 4 emulates the notional device. The -driver implements a `RecordOrientedUart` class, where data is supplied in -variable length records consisting of bytes instances. The object appends a -delimiter before sending and buffers incoming data until the delimiter is -received. This is a demo and is an inefficient way to use a UART compared to -stream I/O. - -For the purpose of demonstrating asynchronous transmission we assume the -device being emulated has a means of checking that transmission is complete -and that the application requires that we wait on this. Neither assumption is -true in this example but the code fakes it with `await asyncio.sleep(0.1)`. - -Link pins X1 and X2 to run. - -```python -import uasyncio as asyncio -from pyb import UART - -class RecordOrientedUart(): - DELIMITER = b'\0' - def __init__(self): - self.uart = UART(4, 9600) - self.data = b'' - - def __iter__(self): # Not __await__ issue #2678 - data = b'' - while not data.endswith(self.DELIMITER): - yield from asyncio.sleep(0) # Necessary because: - while not self.uart.any(): - yield from asyncio.sleep(0) # timing may mean this is never called - data = b''.join((data, self.uart.read(self.uart.any()))) - self.data = data - - async def send_record(self, data): - data = b''.join((data, self.DELIMITER)) - self.uart.write(data) - await self._send_complete() - - # In a real device driver we would poll the hardware - # for completion in a loop with await asyncio.sleep(0) - async def _send_complete(self): - await asyncio.sleep(0.1) - - def read_record(self): # Synchronous: await the object before calling - return self.data[0:-1] # Discard delimiter - -async def run(): - foo = RecordOrientedUart() - rx_data = b'' - await foo.send_record(b'A line of text.') - for _ in range(20): - await foo # Other coros are scheduled while we wait - rx_data = foo.read_record() - print('Got: {}'.format(rx_data)) - await foo.send_record(rx_data) - rx_data = b'' - -loop = asyncio.get_event_loop() -loop.run_until_complete(run()) -``` - -###### [Contents](./TUTORIAL.md#contents) - -## 6.3 Using the stream mechanism - -This can be illustrated using a Pyboard UART. The following code sample -demonstrates concurrent I/O on one UART. To run, link Pyboard pins X1 and X2 -(UART Txd and Rxd). - -```python -import uasyncio as asyncio -from pyb import UART -uart = UART(4, 9600) - -async def sender(): - swriter = asyncio.StreamWriter(uart, {}) - while True: - await swriter.awrite('Hello uart\n') - await asyncio.sleep(2) - -async def receiver(): - sreader = asyncio.StreamReader(uart) - while True: - res = await sreader.readline() - print('Received', res) - -loop = asyncio.get_event_loop() -loop.create_task(sender()) -loop.create_task(receiver()) -loop.run_forever() -``` - -The supporting code may be found in `__init__.py` in the `uasyncio` library. -The mechanism works because the device driver (written in C) implements the -following methods: `ioctl`, `read`, `readline` and `write`. See -[Writing streaming device drivers](./TUTORIAL.md#64-writing-streaming-device-drivers) -for details on how such drivers may be written in Python. - -A UART can receive data at any time. The stream I/O mechanism checks for pending -incoming characters whenever the scheduler has control. When a coro is running -an interrupt service routine buffers incoming characters; these will be removed -when the coro yields to the scheduler. Consequently UART applications should be -designed such that coros minimise the time between yielding to the scheduler to -avoid buffer overflows and data loss. This can be ameliorated by using a larger -UART read buffer or a lower baudrate. Alternatively hardware flow control will -provide a solution if the data source supports it. - -### 6.3.1 A UART driver example - -The program [auart_hd.py](./auart_hd.py) illustrates a method of communicating -with a half duplex device such as one responding to the modem 'AT' command set. -Half duplex means that the device never sends unsolicited data: its -transmissions are always in response to a command from the master. - -The device is emulated, enabling the test to be run on a Pyboard with two wire -links. - -The (highly simplified) emulated device responds to any command by sending four -lines of data with a pause between each, to simulate slow processing. - -The master sends a command, but does not know in advance how many lines of data -will be returned. It starts a retriggerable timer, which is retriggered each -time a line is received. When the timer times out it is assumed that the device -has completed transmission, and a list of received lines is returned. - -The case of device failure is also demonstrated. This is done by omitting the -transmission before awaiting a response. After the timeout an empty list is -returned. See the code comments for more details. - -###### [Contents](./TUTORIAL.md#contents) - -## 6.4 Writing streaming device drivers - -The `stream I/O` mechanism is provided to support I/O to stream devices. Its -typical use is to support streaming I/O devices such as UARTs and sockets. The -mechanism may be employed by drivers of any device which needs to be polled: -the polling is delegated to the scheduler which uses `select` to schedule the -handlers for any devices which are ready. This is more efficient than running -multiple coros each polling a device, partly because `select` is written in C -but also because the coroutine performing the polling is descheduled until the -`poll` object returns a ready status. - -A device driver capable of employing the stream I/O mechanism may support -`StreamReader`, `StreamWriter` instances or both. A readable device must -provide at least one of the following methods. Note that these are synchronous -methods. The `ioctl` method (see below) ensures that they are only called if -data is available. The methods should return as fast as possible with as much -data as is available. - -`readline()` Return as many characters as are available up to and including any -newline character. Required if you intend to use `StreamReader.readline()` -`read(n)` Return as many characters as are available but no more than `n`. -Required to use `StreamReader.read()` or `StreamReader.readexactly()` - -A writeable driver must provide this synchronous method: -`write` Args `buf`, `off`, `sz`. Arguments: -`buf` is the buffer to write. -`off` is the offset into the buffer of the first character to write. -`sz` is the requested number of characters to write. -It should return immediately. The return value is the number of characters -actually written (may well be 1 if the device is slow). The `ioctl` method -ensures that this is only called if the device is ready to accept data. - -All devices must provide an `ioctl` method which polls the hardware to -determine its ready status. A typical example for a read/write driver is: - -```python -import io -MP_STREAM_POLL_RD = const(1) -MP_STREAM_POLL_WR = const(4) -MP_STREAM_POLL = const(3) -MP_STREAM_ERROR = const(-1) - -class MyIO(io.IOBase): - # Methods omitted - def ioctl(self, req, arg): # see ports/stm32/uart.c - ret = MP_STREAM_ERROR - if req == MP_STREAM_POLL: - ret = 0 - if arg & MP_STREAM_POLL_RD: - if hardware_has_at_least_one_char_to_read: - ret |= MP_STREAM_POLL_RD - if arg & MP_STREAM_POLL_WR: - if hardware_can_accept_at_least_one_write_character: - ret |= MP_STREAM_POLL_WR - return ret -``` - -The following is a complete awaitable delay class: - -```python -import uasyncio as asyncio -import utime -import io -MP_STREAM_POLL_RD = const(1) -MP_STREAM_POLL = const(3) -MP_STREAM_ERROR = const(-1) - -class MillisecTimer(io.IOBase): - def __init__(self): - self.end = 0 - self.sreader = asyncio.StreamReader(self) - - def __iter__(self): - await self.sreader.readline() - - def __call__(self, ms): - self.end = utime.ticks_add(utime.ticks_ms(), ms) - return self - - def readline(self): - return b'\n' - - def ioctl(self, req, arg): - ret = MP_STREAM_ERROR - if req == MP_STREAM_POLL: - ret = 0 - if arg & MP_STREAM_POLL_RD: - if utime.ticks_diff(utime.ticks_ms(), self.end) >= 0: - ret |= MP_STREAM_POLL_RD - return ret -``` - -which may be used as follows: - -```python -async def timer_test(n): - timer = ms_timer.MillisecTimer() - await timer(30) # Pause 30ms -``` - -With official `uasyncio` this confers no benefit over `await asyncio.sleep_ms()`. -Using [fast_io](./FASTPOLL.md) it offers much more precise delays under the -common usage pattern where coros await a zero delay. - -It is possible to use I/O scheduling to associate an event with a callback. -This is more efficient than a polling loop because the coro doing the polling -is descheduled until `ioctl` returns a ready status. The following runs a -callback when a pin changes state. - -```python -import uasyncio as asyncio -import io -MP_STREAM_POLL_RD = const(1) -MP_STREAM_POLL = const(3) -MP_STREAM_ERROR = const(-1) - -class PinCall(io.IOBase): - def __init__(self, pin, *, cb_rise=None, cbr_args=(), cb_fall=None, cbf_args=()): - self.pin = pin - self.cb_rise = cb_rise - self.cbr_args = cbr_args - self.cb_fall = cb_fall - self.cbf_args = cbf_args - self.pinval = pin.value() - self.sreader = asyncio.StreamReader(self) - loop = asyncio.get_event_loop() - loop.create_task(self.run()) - - async def run(self): - while True: - await self.sreader.read(1) - - def read(self, _): - v = self.pinval - if v and self.cb_rise is not None: - self.cb_rise(*self.cbr_args) - return b'\n' - if not v and self.cb_fall is not None: - self.cb_fall(*self.cbf_args) - return b'\n' - - def ioctl(self, req, arg): - ret = MP_STREAM_ERROR - if req == MP_STREAM_POLL: - ret = 0 - if arg & MP_STREAM_POLL_RD: - v = self.pin.value() - if v != self.pinval: - self.pinval = v - ret = MP_STREAM_POLL_RD - return ret -``` - -Once again with official `uasyncio` latency can be high. Depending on -application design the [fast_io](./FASTPOLL.md) version can greatly reduce -this. - -The demo program `iorw.py` illustrates a complete example. Note that, at the -time of writing there is a bug in `uasyncio` which prevents this from working. -See [this GitHub thread](https://github.com/micropython/micropython/pull/3836#issuecomment-397317408). -There are two solutions. A workround is to write two separate drivers, one -read-only and the other write-only. Alternatively the -[fast_io](./FASTPOLL.md) version addresses this. - -In the official `uasyncio` I/O is scheduled quite infrequently. See -[see this GitHub RFC](https://github.com/micropython/micropython/issues/2664). -The [fast_io](./FASTPOLL.md) version addresses this issue. - -###### [Contents](./TUTORIAL.md#contents) - -## 6.5 A complete example: aremote.py - -See [aremote.py](./nec_ir/aremote.py) documented [here](./nec_ir/README.md). -The demo provides a complete device driver example: a receiver/decoder for an -infra red remote controller. The following notes are salient points regarding -its `asyncio` usage. - -A pin interrupt records the time of a state change (in μs) and sets an event, -passing the time when the first state change occurred. A coro waits on the -event, yields for the duration of a data burst, then decodes the stored data -before calling a user-specified callback. - -Passing the time to the `Event` instance enables the coro to compensate for -any `asyncio` latency when setting its delay period. - -###### [Contents](./TUTORIAL.md#contents) - -## 6.6 HTU21D environment sensor - -This chip provides accurate measurements of temperature and humidity. The -driver is documented [here](./htu21d/README.md). It has a continuously running -task which updates `temperature` and `humidity` bound variables which may be -accessed "instantly". - -The chip takes on the order of 120ms to acquire both data items. The driver -works asynchronously by triggering the acquisition and using -`await asyncio.sleep(t)` prior to reading the data. This allows other coros to -run while acquisition is in progress. - -# 7 Hints and tips - -###### [Contents](./TUTORIAL.md#contents) - -## 7.1 Program hangs - -Hanging usually occurs because a task has blocked without yielding: this will -hang the entire system. When developing it is useful to have a coro which -periodically toggles an onboard LED. This provides confirmation that the -scheduler is running. - -###### [Contents](./TUTORIAL.md#contents) - -## 7.2 uasyncio retains state - -When running programs using `uasyncio` at the REPL, issue a soft reset -(ctrl-D) between runs. This is because `uasyncio` retains state between runs -which can lead to confusing behaviour. - -###### [Contents](./TUTORIAL.md#contents) - -## 7.3 Garbage Collection - -You may want to consider running a coro which issues: - -```python - gc.collect() - gc.threshold(gc.mem_free() // 4 + gc.mem_alloc()) -``` - -This assumes `import gc` has been issued. The purpose of this is discussed -[here](http://docs.micropython.org/en/latest/pyboard/reference/constrained.html) -in the section on the heap. - -###### [Contents](./TUTORIAL.md#contents) - -## 7.4 Testing - -It's advisable to test that a device driver yields control when you intend it -to. This can be done by running one or more instances of a dummy coro which -runs a loop printing a message, and checking that it runs in the periods when -the driver is blocking: - -```python -async def rr(n): - while True: - print('Roundrobin ', n) - await asyncio.sleep(0) -``` - -As an example of the type of hazard which can occur, in the `RecordOrientedUart` -example above the `__await__` method was originally written as: - -```python - def __await__(self): - data = b'' - while not data.endswith(self.DELIMITER): - while not self.uart.any(): - yield from asyncio.sleep(0) - data = b''.join((data, self.uart.read(self.uart.any()))) - self.data = data -``` - -In testing this hogged execution until an entire record was received. This was -because `uart.any()` always returned a nonzero quantity. By the time it was -called, characters had been received. The solution was to yield execution in -the outer loop: - -```python - def __await__(self): - data = b'' - while not data.endswith(self.DELIMITER): - yield from asyncio.sleep(0) # Necessary because: - while not self.uart.any(): - yield from asyncio.sleep(0) # timing may mean this is never called - data = b''.join((data, self.uart.read(self.uart.any()))) - self.data = data -``` - -It is perhaps worth noting that this error would not have been apparent had -data been sent to the UART at a slow rate rather than via a loopback test. -Welcome to the joys of realtime programming. - -###### [Contents](./TUTORIAL.md#contents) - -## 7.5 A common error - -If a function or method is defined with `async def` and subsequently called as -if it were a regular (synchronous) callable, MicroPython does not issue an -error message. This is [by design](https://github.com/micropython/micropython/issues/3241). -It typically leads to a program silently failing to run correctly: - -```python -async def foo(): - # code -loop.create_task(foo) # Case 1: foo will never run -foo() # Case 2: Likewise. -``` - -I have [a PR](https://github.com/micropython/micropython-lib/pull/292) which -proposes a fix for case 1. The [fast_io](./FASTPOLL.md) version implements -this. - -The script [check_async_code.py](./check_async_code.py) attempts to locate -instances of questionable use of coros. It is intended to be run on a PC and -uses Python3. It takes a single argument, a path to a MicroPython sourcefile -(or `--help`). It is designed for use on scripts written according to the -guidelines in this tutorial, with coros declared using `async def`. - -Note it is somewhat crude and intended to be used on a syntactically correct -file which is silently failing to run. Use a tool such as `pylint` for general -syntax checking (`pylint` currently misses this error). - -The script produces false positives. This is by design: coros are first class -objects; you can pass them to functions and can store them in data structures. -Depending on the program logic you may intend to store the function or the -outcome of its execution. The script can't deduce the intent. It aims to ignore -cases which appear correct while identifying other instances for review. -Assume `foo` is a coro declared with `async def`: - -```python -loop.run_until_complete(foo()) # No warning -bar(foo) # These lines will warn but may or may not be correct -bar(foo()) -z = (foo,) -z = (foo(),) -foo() # Will warn: is surely wrong. -``` - -I find it useful as-is but improvements are always welcome. - -###### [Contents](./TUTORIAL.md#contents) - -## 7.6 Socket programming - -There are two basic approaches to socket programming under `uasyncio`. By -default sockets block until a specified read or write operation completes. -`uasyncio` supports blocking sockets by using `select.poll` to prevent them -from blocking the scheduler. In most cases it is simplest to use this -mechanism. Example client and server code may be found in the `client_server` -directory. The `userver` application uses `select.poll` explicitly to poll -the server socket. The client sockets use it implicitly in that the `uasyncio` -stream mechanism employs it. - -Note that `socket.getaddrinfo` currently blocks. The time will be minimal in -the example code but if a DNS lookup is required the blocking period could be -substantial. - -The second approach to socket programming is to use nonblocking sockets. This -adds complexity but is necessary in some applications, notably where -connectivity is via WiFi (see below). - -At the time of writing (March 2019) support for TLS on nonblocking sockets is -under development. Its exact status is unknown (to me). - -The use of nonblocking sockets requires some attention to detail. If a -nonblocking read is performed, because of server latency, there is no guarantee -that all (or any) of the requested data is returned. Likewise writes may not -proceed to completion. - -Hence asynchronous read and write methods need to iteratively perform the -nonblocking operation until the required data has been read or written. In -practice a timeout is likely to be required to cope with server outages. - -A further complication is that the ESP32 port had issues which required rather -unpleasant hacks for error-free operation. I have not tested whether this is -still the case. - -The file [sock_nonblock.py](./sock_nonblock.py) illustrates the sort of -techniques required. It is not a working demo, and solutions are likely to be -application dependent. - -### 7.6.1 WiFi issues - -The `uasyncio` stream mechanism is not good at detecting WiFi outages. I have -found it necessary to use nonblocking sockets to achieve resilient operation -and client reconnection in the presence of outages. - -[This doc](https://github.com/peterhinch/micropython-samples/blob/master/resilient/README.md) -describes issues I encountered in WiFi applications which keep sockets open for -long periods, and outlines a solution. - -[This repo](https://github.com/peterhinch/micropython-mqtt.git) offers a -resilent asynchronous MQTT client which ensures message integrity over WiFi -outages. [This repo](https://github.com/peterhinch/micropython-iot.git) -provides a simple asynchronous full-duplex serial channel between a wirelessly -connected client and a wired server with guaranteed message delivery. - -###### [Contents](./TUTORIAL.md#contents) - -## 7.7 Event loop constructor args - -A subtle bug can arise if you need to instantiate the event loop with non -default values. Instantiation should be performed before running any other -`asyncio` code. This is because the code may acquire the event loop. In -doing so it initialises it to the default values: - -```python -import uasyncio as asyncio -import some_module -bar = some_module.Bar() # Constructor calls get_event_loop() -# and renders these args inoperative -loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) -``` - -Given that importing a module can run code the safest way is to instantiate -the event loop immediately after importing `uasyncio`. - -```python -import uasyncio as asyncio -loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) -import some_module -bar = some_module.Bar() # The get_event_loop() call is now safe -``` - -My preferred approach to this is as follows. If writing modules for use by -other programs avoid running `uasyncio` code on import. Write functions and -methods to expect the event loop as an arg. Then ensure that only the top level -application calls `get_event_loop`: - -```python -import uasyncio as asyncio -import my_module # Does not run code on loading -loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) -bar = my_module.Bar(loop) -``` - -Ref [this issue](https://github.com/micropython/micropython-lib/issues/295). - -###### [Contents](./TUTORIAL.md#contents) - -# 8 Notes for beginners - -These notes are intended for those new to asynchronous code. They start by -outlining the problems which schedulers seek to solve, and give an overview of -the `uasyncio` approach to a solution. - -[Section 8.5](./TUTORIAL.md#85-why-cooperative-rather-than-pre-emptive) -discusses the relative merits of `uasyncio` and the `_thread` module and why -you may prefer use cooperative (`uasyncio`) over pre-emptive (`_thread`) -scheduling. - -###### [Contents](./TUTORIAL.md#contents) - -## 8.1 Problem 1: event loops - -A typical firmware application runs continuously and is required to respond to -external events. These might include a voltage change on an ADC, the arrival of -a hard interrupt, a character arriving on a UART, or data being available on a -socket. These events occur asynchronously and the code must be able to respond -regardless of the order in which they occur. Further the application may be -required to perform time-dependent tasks such as flashing LED's. - -The obvious way to do this is with an event loop. The following is not -practical code but serves to illustrate the general form of an event loop. - -```python -def event_loop(): - led_1_time = 0 - led_2_time = 0 - switch_state = switch.state() # Current state of a switch - while True: - time_now = utime.time() - if time_now >= led_1_time: # Flash LED #1 - led1.toggle() - led_1_time = time_now + led_1_period - if time_now >= led_2_time: # Flash LED #2 - led2.toggle() - led_2_time = time_now + led_2_period - # Handle LEDs 3 upwards - - if switch.value() != switch_state: - switch_state = switch.value() - # do something - if uart.any(): - # handle UART input -``` - -This works for simple examples but event loops rapidly become unwieldy as the -number of events increases. They also violate the principles of object oriented -programming by lumping much of the program logic in one place rather than -associating code with the object being controlled. We want to design a class -for an LED capable of flashing which could be put in a module and imported. An -OOP approach to flashing an LED might look like this: - -```python -import pyb -class LED_flashable(): - def __init__(self, led_no): - self.led = pyb.LED(led_no) - - def flash(self, period): - while True: - self.led.toggle() - # somehow wait for period but allow other - # things to happen at the same time -``` - -A cooperative scheduler such as `uasyncio` enables classes such as this to be -created. - -###### [Contents](./TUTORIAL.md#contents) - -## 8.2 Problem 2: blocking methods - -Assume you need to read a number of bytes from a socket. If you call -`socket.read(n)` with a default blocking socket it will "block" (i.e. fail to -return) until `n` bytes have been received. During this period the application -will be unresponsive to other events. - -With `uasyncio` and a non-blocking socket you can write an asynchronous read -method. The task requiring the data will (necessarily) block until it is -received but during that period other tasks will be scheduled enabling the -application to remain responsive. - -## 8.3 The uasyncio approach - -The following class provides for an LED which can be turned on and off, and -which can also be made to flash at an arbitrary rate. A `LED_async` instance -has a `run` method which can be considered to run continuously. The LED's -behaviour can be controlled by methods `on()`, `off()` and `flash(secs)`. - -```python -import pyb -import uasyncio as asyncio - -class LED_async(): - def __init__(self, led_no): - self.led = pyb.LED(led_no) - self.rate = 0 - loop = asyncio.get_event_loop() - loop.create_task(self.run()) - - async def run(self): - while True: - if self.rate <= 0: - await asyncio.sleep_ms(200) - else: - self.led.toggle() - await asyncio.sleep_ms(int(500 / self.rate)) - - def flash(self, rate): - self.rate = rate - - def on(self): - self.led.on() - self.rate = 0 - - def off(self): - self.led.off() - self.rate = 0 -``` - -Note that `on()`, `off()` and `flash()` are conventional synchronous methods. -They change the behaviour of the LED but return immediately. The flashing -occurs "in the background". This is explained in detail in the next section. - -The class conforms with the OOP principle of keeping the logic associated with -the device within the class. Further, the way `uasyncio` works ensures that -while the LED is flashing the application can respond to other events. The -example below flashes the four Pyboard LED's at different rates while also -responding to the USR button which terminates the program. - -```python -import pyb -import uasyncio as asyncio -from led_async import LED_async # Class as listed above - -async def killer(): - sw = pyb.Switch() - while not sw.value(): - await asyncio.sleep_ms(100) - -leds = [LED_async(n) for n in range(1, 4)] -for n, led in enumerate(leds): - led.flash(0.7 + n/4) -loop = asyncio.get_event_loop() -loop.run_until_complete(killer()) -``` - -In contrast to the event loop example the logic associated with the switch is -in a function separate from the LED functionality. Note the code used to start -the scheduler: - -```python -loop = asyncio.get_event_loop() -loop.run_until_complete(killer()) # Execution passes to coroutines. - # It only continues here once killer() terminates, when the - # scheduler has stopped. -``` - -###### [Contents](./TUTORIAL.md#contents) - -## 8.4 Scheduling in uasyncio - -Python 3.5 and MicroPython support the notion of an asynchronous function, -also known as a coroutine (coro) or task. A coro must include at least one -`await` statement. - -```python -async def hello(): - for _ in range(10): - print('Hello world.') - await asyncio.sleep(1) -``` - -This function prints the message ten times at one second intervals. While the -function is paused pending the time delay asyncio will schedule other tasks, -providing an illusion of concurrency. - -When a coro issues `await asyncio.sleep_ms()` or `await asyncio.sleep()` the -current task pauses: it is placed on a queue which is ordered on time due, and -execution passes to the task at the top of the queue. The queue is designed so -that even if the specified sleep is zero other due tasks will run before the -current one is resumed. This is "fair round-robin" scheduling. It is common -practice to issue `await asyncio.sleep(0)` in loops to ensure a task doesn't -hog execution. The following shows a busy-wait loop which waits for another -task to set the global `flag`. Alas it monopolises the CPU preventing other -coros from running: - -```python -async def bad_code(): - global flag - while not flag: - pass - flag = False - # code omitted -``` - -The problem here is that while the `flag` is `False` the loop never yields to -the scheduler so no other task will get to run. The correct approach is: - -```python -async def good_code(): - global flag - while not flag: - await asyncio.sleep(0) - flag = False - # code omitted -``` - -For the same reason it's bad practice to issue delays like `utime.sleep(1)` -because that will lock out other tasks for 1s; use `await asyncio.sleep(1)`. -Note that the delays implied by `uasyncio` methods `sleep` and `sleep_ms` can -overrun the specified time. This is because while the delay is in progress -other tasks will run. When the delay period completes, execution will not -resume until the running task issues `await` or terminates. A well-behaved coro -will always issue `await` at regular intervals. Where a precise delay is -required, especially one below a few ms, it may be necessary to use -`utime.sleep_us(us)`. - -###### [Contents](./TUTORIAL.md#contents) - -## 8.5 Why cooperative rather than pre-emptive? - -The initial reaction of beginners to the idea of cooperative multi-tasking is -often one of disappointment. Surely pre-emptive is better? Why should I have to -explicitly yield control when the Python virtual machine can do it for me? - -When it comes to embedded systems the cooperative model has two advantages. -Firstly, it is lightweight. It is possible to have large numbers of coroutines -because unlike descheduled threads, paused coroutines contain little state. -Secondly it avoids some of the subtle problems associated with pre-emptive -scheduling. In practice cooperative multi-tasking is widely used, notably in -user interface applications. - -To make a case for the defence a pre-emptive model has one advantage: if -someone writes - -```python -for x in range(1000000): - # do something time consuming -``` - -it won't lock out other threads. Under cooperative schedulers the loop must -explicitly yield control every so many iterations e.g. by putting the code in -a coro and periodically issuing `await asyncio.sleep(0)`. - -Alas this benefit of pre-emption pales into insignificance compared to the -drawbacks. Some of these are covered in the documentation on writing -[interrupt handlers](http://docs.micropython.org/en/latest/reference/isr_rules.html). -In a pre-emptive model every thread can interrupt every other thread, changing -data which might be used in other threads. It is generally much easier to find -and fix a lockup resulting from a coro which fails to yield than locating the -sometimes deeply subtle and rarely occurring bugs which can occur in -pre-emptive code. - -To put this in simple terms, if you write a MicroPython coroutine, you can be -sure that variables won't suddenly be changed by another coro: your coro has -complete control until it issues `await asyncio.sleep(0)`. - -Bear in mind that interrupt handlers are pre-emptive. This applies to both hard -and soft interrupts, either of which can occur at any point in your code. - -An eloquent discussion of the evils of threading may be found -[in threads are bad](https://glyph.twistedmatrix.com/2014/02/unyielding.html). - -###### [Contents](./TUTORIAL.md#contents) - -## 8.6 Communication - -In non-trivial applications coroutines need to communicate. Conventional Python -techniques can be employed. These include the use of global variables or -declaring coros as object methods: these can then share instance variables. -Alternatively a mutable object may be passed as a coro argument. - -Pre-emptive systems mandate specialist classes to achieve "thread safe" -communications; in a cooperative system these are seldom required. - -###### [Contents](./TUTORIAL.md#contents) - -## 8.7 Polling - -Some hardware devices such as the Pyboard accelerometer don't support -interrupts, and therefore must be polled (i.e. checked periodically). Polling -can also be used in conjunction with interrupt handlers: the interrupt handler -services the hardware and sets a flag. A coro polls the flag: if it's set it -handles the data and clears the flag. A better approach is to use an `Event`. - -###### [Contents](./TUTORIAL.md#contents) diff --git a/v2/UNDER_THE_HOOD.md b/v2/UNDER_THE_HOOD.md deleted file mode 100644 index 64a3fff..0000000 --- a/v2/UNDER_THE_HOOD.md +++ /dev/null @@ -1,377 +0,0 @@ -# uasyncio: Under the hood - -This document aims to explain the operation of `uasyncio` as I understand it. I -did not write the library so the information presented is a result of using it, -studying the code, experiment and inference. There may be errors, in which case -please raise an issue. None of this information is required to use the library: -it is intended to satisfy the curiosity of scheduler geeks or to help those -wishing to modify it. - -# 0. Contents - - 1. [Introduction](./UNDER_THE_HOOD.md#1-introduction) - 2. [Generators and coroutines](./UNDER_THE_HOOD.md#2-generators-and-coroutines) - 2.1 [pend_throw](./UNDER_THE_HOOD.md#21-pend_throw) - 3. [Coroutine yield types](./UNDER_THE_HOOD.md#3-coroutine-yield-types) - 3.1 [SysCall1 classes](./UNDER_THE_HOOD.md#31-syscall1-classes) - 4. [The EventLoop](./UNDER_THE_HOOD.md#4-the-eventloop) - 4.1 [Exceptions](./UNDER_THE_HOOD.md#41-exceptions) - 4.2 [Task Cancellation and Timeouts](./UNDER_THE_HOOD.md#42-task-cancellation-and-timeouts) - 5. [Stream I/O](./UNDER_THE_HOOD.md#5-stream-io) - 5.1 [StreamReader](./UNDER_THE_HOOD.md#51-streamreader) - 5.2 [StreamWriter](./UNDER_THE_HOOD.md#52-streamwriter) - 5.3 [PollEventLoop wait method](./UNDER_THE_HOOD.md#53-polleventloop-wait-method) - 6. [Modifying uasyncio](./UNDER_THE_HOOD.md#6-modifying-uasyncio) - 7. [Links](./UNDER_THE_HOOD.md#7-links) - -# 1. Introduction - -Where the versions differ, this explanation relates to the `fast_io` version. -Note that the code in `fast_io` contains additional comments to explain its -operation. The code the `fast_io` directory is also in -[my micropython-lib fork](https://github.com/peterhinch/micropython-lib.git), -`uasyncio-io-fast-and-rw` branch. - -This doc assumes a good appreciation of the use of `uasyncio`. An understanding -of Python generators is also essential, in particular the use of `yield from` -and an appreciation of the difference between a generator and a generator -function: - -```python -def gen_func(n): # gen_func is a generator function - while True: - yield n - n += 1 - -my_gen = gen_func(7) # my_gen is a generator -``` - -The code for the `fast_io` variant of `uasyncio` may be found in: - -``` -fast_io/__init__.py -fast_io/core.py -``` - -This has additional code comments to aid in its understanding. - -###### [Main README](./README.md) - -# 2. Generators and coroutines - -In MicroPython coroutines and generators are identical: this differs from -CPython. The knowledge that a coro is a generator is crucial to understanding -`uasyncio`'s operation. Consider this code fragment: - -```python -async def bar(): - await asyncio.sleep(1) - -async def foo(): - await bar() -``` - -In MicroPython the `async def` syntax allows a generator function to lack a -`yield` statement. Thus `bar` is a generator function, hence `bar()` returns a -generator. - -The `await bar()` syntax is equivalent to `yield from bar()`. So transferring -execution to the generator instantiated by `bar()` does not involve the -scheduler. `asyncio.sleep` is a generator function so `await asyncio.sleep(1)` -creates a generator and transfers execution to it via `yield from`. The -generator yields a value of 1000; this is passed to the scheduler to invoke the -delay by placing the coro onto a `timeq` (see below). - -## 2.1 pend_throw - -Generators in MicroPython have a nonstandard method `pend_throw`. The Python -`throw` method causes the generator immediately to run and to handle the passed -exception. `pend_throw` retains the exception until the generator (coroutine) -is next scheduled, when the exception is raised. In `fast_io` the task -cancellation and timeout mechanisms aim to ensure that the task is scheduled as -soon as possible to minimise latency. - -The `pend_throw` method serves a secondary purpose in `uasyncio`: to store -state in a coro which is paused pending execution. This works because the -object returned from `pend_throw` is that which was previously passed to it, or -`None` on the first call. - -```python -a = my_coro.pend_throw(42) -b = my_coro.pend_throw(None) # Coro can now safely be executed -``` -In the above instance `a` will be `None` if it was the first call to -`pend_throw` and `b` will be 42. This is used to determine if a paused task is -on a `timeq` or waiting on I/O. A task on a `timeq` will have an integer value, -being the `ID` of the task; one pending I/O will have `False`. - -If a coro is actually run, the only acceptable stored values are `None` or an -exception. The error "exception must be derived from base exception" indicates -an error in the scheduler whereby this constraint has not been satisfied. - -###### [Contents](./UNDER_THE_HOOD.md#0-contents) - -# 3. Coroutine yield types - -Because coroutines are generators it is valid to issue `yield` in a coroutine, -behaviour which would cause a syntax error in CPython. While explicitly issuing -`yield` in a user application is best avoided for CPython compatibility, it is -used internally in `uasyncio`. Further, because `await` is equivalent to -`yield from`, the behaviour of the scheduler in response to `yield` is crucial -to understanding its operation. - -Where a coroutine (perhaps at the end of a `yield from` chain) executes - -```python -yield some_object -``` - -the scheduler regains execution. This is because the scheduler passed execution -to the user coroutine with - -```python -ret = next(cb) -``` - -so `ret` contains the object yielded. Subsequent scheduler behaviour depends on -the type of that object. The following object types are handled: - - * `None` The coro is rescheduled and will run in round-robin fashion. - Hence `yield` is functionally equivalent to `await asyncio.sleep(0)`. - * An integer `N`: equivalent to `await asyncio.sleep_ms(N)`. - * `False` The coro terminates and is not rescheduled. - * A coro/generator: the yielded coro is scheduled. The coro which issued the - `yield` is rescheduled. - * A `SysCall1` instance. See below. - -## 3.1 SysCall1 classes - -The `SysCall1` constructor takes a single argument stored in `self.arg`. It is -effectively an abstract base class: only subclasses are instantiated. When a -coro yields a `SysCall1` instance, the scheduler's behaviour is determined by -the type of the object and the contents of its `.arg`. - -The following subclasses exist: - - * `SleepMs` `.arg` holds the delay in ms. Effectively a singleton with the - instance in `sleep_ms`. Its `.__call__` enables `await asyncio.sleep_ms(n)`. - * `StopLoop` Stops the scheduler. `.arg` is returned to the caller. - * `IORead` Causes an interface to be polled for data ready. `.arg` is the - interface. - * `IOWrite` Causes an interface to be polled for ready to accept data. `.arg` - is the interface. - * `IOReadDone` These stop polling of an interface (in `.arg`). - * `IOWriteDone` - -The `IO*` classes are for the exclusive use of `StreamReader` and `StreamWriter` -objects. - -###### [Contents](./UNDER_THE_HOOD.md#0-contents) - -# 4. The EventLoop - -The file `core.py` defines an `EventLoop` class which is subclassed by -`PollEventLoop` in `__init__.py`. The latter extends the base class to support -stream I/O. In particular `.wait()` is overridden in the subclass. - -The `fast_io` `EventLoop` maintains four queues, `.runq`, `.waitq`, `.lpq` and -`.ioq`. The latter two are only instantiated if specified to the -`get_event_loop` method. Official `uasyncio` does not have `.lpq` or `.ioq`. - -Tasks are appended to the bottom of the run queue and retrieved from the top; -in other words it is a First In First Out (FIFO) queue. The I/O queue is -similar. Tasks on `.waitq` and `.lpq` are sorted in order of the time when they -are to run, the task having the soonest time to run at the top. - -When a task issues `await asyncio.sleep(t)` or `await asyncio.sleep_ms(t)` and -t > 0 the task is placed on the wait queue. If t == 0 it is placed on the run -queue (by `.call_soon()`). Callbacks are placed on the queues in a similar way -to tasks. - -The following is a somewhat broad-brush explanation of an iteration of the -event loop's `run_forever()` method intended to aid in following the code. - -The method first checks the wait queue. Any tasks which have become due (or -overdue) are removed and placed on the run queue. - -The run queue is then processed. The number of tasks on it is determined: only -that number of tasks will be run. Because the run queue is FIFO this guarantees -that exactly those tasks which were on the queue at the start of processing -this queue will run (even when tasks are appended). - -The topmost task/callback is removed and run. If it is a callback the loop -iterates to the next entry. If it is a task, it runs then either yields or -raises an exception. If it yields, the return type is examined as described -above. If the task yields with a zero delay it will be appended to the run -queue, but as described above it will not be rescheduled in this pass through -the queue. If it yields a nonzero delay it will be added to `.waitq` (it has -already been removed from `.runq`). - -Once every task which was initially on the run queue has been scheduled, the -queue may or may not be empty depending on whether tasks yielded a zero delay. - -At the end of the outer loop a `delay` value is determined. This will be zero -if the run queue is not empty: tasks are ready for scheduling. If the run queue -is empty `delay` is determined from the time to run of the topmost (most -current) task on the wait queue. - -The `.wait()` method is called with this delay. If the delay is > 0 the -scheduler pauses for this period (polling I/O). On a zero delay I/O is checked -once: if nothing is pending it returns quickly. - -###### [Contents](./UNDER_THE_HOOD.md#0-contents) - -## 4.1 Exceptions - -There are two "normal" cases where tasks raise an exception: when the task is -complete (`StopIteration`) and when it is cancelled (`CancelledError`). In both -these cases the exception is trapped and the loop proceeds to the next item on -the run queue - the task is simply not rescheduled. - -If an unhandled exception occurs in a task this will be propagated to the -caller of `run_forever()` or `run_until_complete` a explained in the tutorial. - -## 4.2 Task Cancellation and Timeouts - -The `cancel` function uses `pend_throw` to pass a `CancelledError` to the coro -to be cancelled. The generator's `.throw` and `.close` methods cause the coro -to execute code immediately. This is incorrect behaviour for a de-scheduled -coro. The `.pend_throw` method causes the exception to be processed the next -time the coro is scheduled. - -In the `fast_io` version the `cancel` function puts the task onto `.runq` or -`.ioq` for "immediate" excecution. In the case where the task is on `.waitq` or -`.lpq` the task ID is added to a `set` `.canned`. When the task reaches the top -of the timeq it is ignored and removed from `.canned`. This Python approach is -less efficient than that in the Paul Sokolovsky fork, but his approach uses a -special version of the C `utimeq` object and so requires his firmware. - -Timeouts use a similar mechanism. - -###### [Contents](./UNDER_THE_HOOD.md#0-contents) - -# 5. Stream IO - -Stream I/O is an efficient way of polling stream devices using `select.poll`. -Device drivers for this mechanism must provide an `ioctl` method which reports -whether a read device has data ready, or whether a write device is capable of -accepting data. Stream I/O is handled via `StreamReader` and `StreamWriter` -instances (defined in `__init__.py`). - -## 5.1 StreamReader - -The class supports three read coros which work in a similar fashion. The coro -yields an `IORead` instance with the device to be polled as its arg. It is -rescheduled when `ioctl` has reported that some data is available. The coro -reads the device by calling the device driver's `read` or `readline` method. -If all available data has been read, the device's read methods must update the -status returned by its `ioctl` method. - -The `StreamReader` read coros iterate until the required data has been read, -when the coro yields `IOReadDone(object_to_poll)` before returning the data. If -during this process, `ioctl` reports that no data is available, the coro -yields `IORead(object_to_poll)`. This causes the coro to be descheduled until -data is again available. - -The mechanism which causes it to be rescheduled is discussed below (`.wait()`). - -When `IORead(object_to_poll)` is yielded the `EventLoop` calls `.add_reader()`. -This registers the device with `select.poll` as a reader, and saves the coro -for later rescheduling. - -The `PollEventLoop` maintains three dictionaries indexed by the `id` of the -object being polled. These are: - - * `rdobjmap` Value: the suspended read coro. - * `wrobjmap` Value: the suspended write coro (read and write coros may both be - in a suspended state). - * `flags` Value: bitmap of current poll flags. - -The `add_reader` method saves the coro in `.rdobjmap` and updates `.flags` and -the poll flags so that `ioctl` will respond to a `MP_STREAM_POLL_RD` query. - -When the `StreamReader` read method completes it yields -`IOReadDone(object_to_poll)`: this updates `.flags` and the poll flags so that -`ioctl` no longer responds to an `MP_STREAM_POLL_RD` query. - -## 5.2 StreamWriter - -This supports the `awrite` coro which works in a similar way to `StreamReader`, -yielding `IOWrite(object_to_poll)` until all data has been written, followed -by `IOWriteDone(object_to_poll)`. - -The mechanism is the same as for reading, except that when `ioctl` returns a -"ready" state for a writeable device it means the device is capable of writing -at least one character. - -## 5.3 PollEventLoop wait method - -When this is called the `Poll` instance is checked in a one-shot mode. In this -mode it will return either when `delay` has elapsed or when at least one device -is ready. - -The poller's `ipoll` method uses the iterator protocol to return successive -`(sock, ev)` tuples where `sock` is the device driver and `ev` is a bitmap of -read and write ready status for that device. The `.wait` method iterates -through each device requiring service. - -If the read bit is set (i.e. `ioctl` reported data available) the read coro is -retrieved from `.rdobjmap` and queued for scheduling. This is done via -`._call_io`: this puts the coro onto `.runq` or `.ioq` depending on whether an -I/O queue has been instantiated. - -Writing is handled similarly. - -###### [Contents](./UNDER_THE_HOOD.md#0-contents) - -# 6. Modifying uasyncio - -The library is designed to be extensible. By following these guidelines a -module can be constructed which alters the functionality of asyncio without the -need to change the official library. Such a module may be used where `uasyncio` -is implemented as frozen bytecode as in official release binaries. - -Assume that the aim is to alter the event loop. The module should issue - -```python -from uasyncio import * -``` - -The event loop should be subclassed from `PollEventLoop` (defined in -`__init__.py`). - -The event loop is instantiated by the first call to `get_event_loop()`: this -creates a singleton instance. This is returned by every call to -`get_event_loop()`. On the assumption that the constructor arguments for the -new class differ from those of the base class, the module will need to redefine -`get_event_loop()` along the following lines: - -```python -_event_loop = None # The singleton instance -_event_loop_class = MyNewEventLoopClass # The class, not an instance -def get_event_loop(args): - global _event_loop - if _event_loop is None: - _event_loop = _event_loop_class(args) # Instantiate once only - return _event_loop -``` - -###### [Contents](./UNDER_THE_HOOD.md#0-contents) - -# 7. Links - -Initial discussion of priority I/O scheduling [here](https://github.com/micropython/micropython/issues/2664). - -MicroPython PR enabling stream device drivers to be written in Python -[PR #3836: io.IOBase](https://github.com/micropython/micropython/pull/3836). -Includes discussion of the read/write bug. - -My outstanding uasyncio PR's: fast I/O -[PR #287](https://github.com/micropython/micropython-lib/pull/287) improved -error reporting -[PR #292](https://github.com/micropython/micropython-lib/pull/292). - -This caught my attention for usefulness and compliance with CPython: -[PR #270](https://github.com/micropython/micropython-lib/pull/270). - -###### [Main README](./README.md) diff --git a/v2/aledflash.py b/v2/aledflash.py deleted file mode 100644 index 420a0d4..0000000 --- a/v2/aledflash.py +++ /dev/null @@ -1,32 +0,0 @@ -# aledflash.py Demo/test program for MicroPython asyncio -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license -# Flashes the onboard LED's each at a different rate. Stops after ten seconds. -# Run on MicroPython board bare hardware - -import pyb -import uasyncio as asyncio - -async def killer(duration): - await asyncio.sleep(duration) - -async def toggle(objLED, time_ms): - while True: - await asyncio.sleep_ms(time_ms) - objLED.toggle() - -# TEST FUNCTION - -def test(duration): - loop = asyncio.get_event_loop() - duration = int(duration) - if duration > 0: - print("Flash LED's for {:3d} seconds".format(duration)) - leds = [pyb.LED(x) for x in range(1,5)] # Initialise all four on board LED's - for x, led in enumerate(leds): # Create a coroutine for each LED - t = int((0.2 + x/2) * 1000) - loop.create_task(toggle(leds[x], t)) - loop.run_until_complete(killer(duration)) - loop.close() - -test(10) diff --git a/v2/apoll.py b/v2/apoll.py deleted file mode 100644 index 9639a2c..0000000 --- a/v2/apoll.py +++ /dev/null @@ -1,64 +0,0 @@ -# Demonstration of a device driver using a coroutine to poll a device. -# Runs on Pyboard: displays results from the onboard accelerometer. -# Uses crude filtering to discard noisy data. - -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license - -import uasyncio as asyncio -import pyb -import utime as time - -class Accelerometer(object): - threshold_squared = 16 - def __init__(self, accelhw, timeout): - self.loop = asyncio.get_event_loop() - self.accelhw = accelhw - self.timeout = timeout - self.last_change = self.loop.time() - self.coords = [accelhw.x(), accelhw.y(), accelhw.z()] - - def dsquared(self, xyz): # Return the square of the distance between this and a passed - return sum(map(lambda p, q : (p-q)**2, self.coords, xyz)) # acceleration vector - - def poll(self): # Device is noisy. Only update if change exceeds a threshold - xyz = [self.accelhw.x(), self.accelhw.y(), self.accelhw.z()] - if self.dsquared(xyz) > Accelerometer.threshold_squared: - self.coords = xyz - self.last_change = self.loop.time() - return 0 - return time.ticks_diff(self.loop.time(), self.last_change) - - def vector(self): - return self.coords - - def timed_out(self): # Time since last change or last timeout report - if time.ticks_diff(self.loop.time(), self.last_change) > self.timeout: - self.last_change = self.loop.time() - return True - return False - -async def accel_coro(timeout = 2000): - loop = asyncio.get_event_loop() - accelhw = pyb.Accel() # Instantiate accelerometer hardware - await asyncio.sleep_ms(30) # Allow it to settle - accel = Accelerometer(accelhw, timeout) - while True: - result = accel.poll() - if result == 0: # Value has changed - x, y, z = accel.vector() - print("Value x:{:3d} y:{:3d} z:{:3d}".format(x, y, z)) - elif accel.timed_out(): # Report every 2 secs - print("Timeout waiting for accelerometer change") - await asyncio.sleep_ms(100) # Poll every 100ms - - -async def main(delay): - print('Testing accelerometer for {} secs. Move the Pyboard!'.format(delay)) - print('Test runs for 20s.') - await asyncio.sleep(delay) - print('Test complete!') - -loop = asyncio.get_event_loop() -loop.create_task(accel_coro()) -loop.run_until_complete(main(20)) diff --git a/v2/aqtest.py b/v2/aqtest.py deleted file mode 100644 index afb5ffb..0000000 --- a/v2/aqtest.py +++ /dev/null @@ -1,35 +0,0 @@ -# aqtest.py Demo/test program for MicroPython library micropython-uasyncio.queues -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license - -import uasyncio as asyncio - -from uasyncio.queues import Queue - -q = Queue() - -async def slow_process(): - await asyncio.sleep(2) - return 42 - -async def bar(): - print('Waiting for slow process.') - result = await slow_process() - print('Putting result onto queue') - await q.put(result) # Put result on q - -async def foo(): - print("Running foo()") - result = await(q.get()) - print('Result was {}'.format(result)) - -async def main(delay): - await asyncio.sleep(delay) - print("I've seen starships burn off the shoulder of Orion...") - print("Time to die...") - -print('Test takes 3 secs') -loop = asyncio.get_event_loop() -loop.create_task(foo()) -loop.create_task(bar()) -loop.run_until_complete(main(3)) diff --git a/v2/astests.py b/v2/astests.py deleted file mode 100644 index 0120be5..0000000 --- a/v2/astests.py +++ /dev/null @@ -1,132 +0,0 @@ -# Test/demo programs for the aswitch module. -# Tested on Pyboard but should run on other microcontroller platforms -# running MicroPython with uasyncio library. -# Author: Peter Hinch. -# Copyright Peter Hinch 2017-2018 Released under the MIT license. - -from machine import Pin -from pyb import LED -from aswitch import Switch, Pushbutton -import uasyncio as asyncio - -helptext = ''' -Test using switch or pushbutton between X1 and gnd. -Ground pin X2 to terminate test. -Soft reset (ctrl-D) after each test. - -''' -tests = ''' -Available tests: -test_sw Switch test -test_swcb Switch with callback -test_btn Pushutton launching coros -test_btncb Pushbutton launching callbacks -''' -print(tests) - -# Pulse an LED (coroutine) -async def pulse(led, ms): - led.on() - await asyncio.sleep_ms(ms) - led.off() - -# Toggle an LED (callback) -def toggle(led): - led.toggle() - -# Quit test by connecting X2 to ground -async def killer(): - pin = Pin('X2', Pin.IN, Pin.PULL_UP) - while pin.value(): - await asyncio.sleep_ms(50) - -# Test for the Switch class passing coros -def test_sw(): - s = ''' -close pulses green -open pulses red -''' - print('Test of switch scheduling coroutines.') - print(helptext) - print(s) - pin = Pin('X1', Pin.IN, Pin.PULL_UP) - red = LED(1) - green = LED(2) - sw = Switch(pin) - # Register coros to launch on contact close and open - sw.close_func(pulse, (green, 1000)) - sw.open_func(pulse, (red, 1000)) - loop = asyncio.get_event_loop() - loop.run_until_complete(killer()) - -# Test for the switch class with a callback -def test_swcb(): - s = ''' -close toggles red -open toggles green -''' - print('Test of switch executing callbacks.') - print(helptext) - print(s) - pin = Pin('X1', Pin.IN, Pin.PULL_UP) - red = LED(1) - green = LED(2) - sw = Switch(pin) - # Register a coro to launch on contact close - sw.close_func(toggle, (red,)) - sw.open_func(toggle, (green,)) - loop = asyncio.get_event_loop() - loop.run_until_complete(killer()) - -# Test for the Pushbutton class (coroutines) -# Pass True to test suppress -def test_btn(suppress=False, lf=True, df=True): - s = ''' -press pulses red -release pulses green -double click pulses yellow -long press pulses blue -''' - print('Test of pushbutton scheduling coroutines.') - print(helptext) - print(s) - pin = Pin('X1', Pin.IN, Pin.PULL_UP) - red = LED(1) - green = LED(2) - yellow = LED(3) - blue = LED(4) - pb = Pushbutton(pin, suppress) - pb.press_func(pulse, (red, 1000)) - pb.release_func(pulse, (green, 1000)) - if df: - print('Doubleclick enabled') - pb.double_func(pulse, (yellow, 1000)) - if lf: - print('Long press enabled') - pb.long_func(pulse, (blue, 1000)) - loop = asyncio.get_event_loop() - loop.run_until_complete(killer()) - -# Test for the Pushbutton class (callbacks) -def test_btncb(): - s = ''' -press toggles red -release toggles green -double click toggles yellow -long press toggles blue -''' - print('Test of pushbutton executing callbacks.') - print(helptext) - print(s) - pin = Pin('X1', Pin.IN, Pin.PULL_UP) - red = LED(1) - green = LED(2) - yellow = LED(3) - blue = LED(4) - pb = Pushbutton(pin) - pb.press_func(toggle, (red,)) - pb.release_func(toggle, (green,)) - pb.double_func(toggle, (yellow,)) - pb.long_func(toggle, (blue,)) - loop = asyncio.get_event_loop() - loop.run_until_complete(killer()) diff --git a/v2/aswitch.py b/v2/aswitch.py deleted file mode 100644 index 4269ce9..0000000 --- a/v2/aswitch.py +++ /dev/null @@ -1,231 +0,0 @@ -# aswitch.py Switch and pushbutton classes for asyncio -# Delay_ms A retriggerable delay class. Can schedule a coro on timeout. -# Switch Simple debounced switch class for normally open grounded switch. -# Pushbutton extend the above to support logical state, long press and -# double-click events -# Tested on Pyboard but should run on other microcontroller platforms -# running MicroPython and uasyncio. - -# The MIT License (MIT) -# -# Copyright (c) 2017 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import uasyncio as asyncio -import utime as time -# Remove dependency on asyn to save RAM: -# launch: run a callback or initiate a coroutine depending on which is passed. -async def _g(): - pass -type_coro = type(_g()) - -# If a callback is passed, run it and return. -# If a coro is passed initiate it and return. -# coros are passed by name i.e. not using function call syntax. -def launch(func, tup_args): - res = func(*tup_args) - if isinstance(res, type_coro): - loop = asyncio.get_event_loop() - loop.create_task(res) - - -class Delay_ms: - verbose = False - def __init__(self, func=None, args=(), can_alloc=True, duration=1000): - self.func = func - self.args = args - self.can_alloc = can_alloc - self.duration = duration # Default duration - self._tstop = None # Killer not running - self._running = False # Timer not running - self.loop = asyncio.get_event_loop() - if not can_alloc: - self.loop.create_task(self._run()) - - async def _run(self): - while True: - if not self._running: # timer not running - await asyncio.sleep_ms(0) - else: - await self._killer() - - def stop(self): - self._running = False - # If uasyncio is ever fixed we should cancel .killer - - def trigger(self, duration=0): # Update end time - self._running = True - if duration <= 0: - duration = self.duration - tn = time.ticks_add(time.ticks_ms(), duration) # new end time - self.verbose and self._tstop is not None and self._tstop > tn \ - and print("Warning: can't reduce Delay_ms time.") - # Start killer if can allocate and killer is not running - sk = self.can_alloc and self._tstop is None - # The following indicates ._killer is running: it will be - # started either here or in ._run - self._tstop = tn - if sk: # ._killer stops the delay when its period has elapsed - self.loop.create_task(self._killer()) - - def running(self): - return self._running - - __call__ = running - - async def _killer(self): - twait = time.ticks_diff(self._tstop, time.ticks_ms()) - while twait > 0: # Must loop here: might be retriggered - await asyncio.sleep_ms(twait) - if self._tstop is None: - break # Return if stop() called during wait - twait = time.ticks_diff(self._tstop, time.ticks_ms()) - if self._running and self.func is not None: - launch(self.func, self.args) # Timed out: execute callback - self._tstop = None # killer not running - self._running = False # timer is stopped - -class Switch: - debounce_ms = 50 - def __init__(self, pin): - self.pin = pin # Should be initialised for input with pullup - self._open_func = False - self._close_func = False - self.switchstate = self.pin.value() # Get initial state - loop = asyncio.get_event_loop() - loop.create_task(self.switchcheck()) # Thread runs forever - - def open_func(self, func, args=()): - self._open_func = func - self._open_args = args - - def close_func(self, func, args=()): - self._close_func = func - self._close_args = args - - # Return current state of switch (0 = pressed) - def __call__(self): - return self.switchstate - - async def switchcheck(self): - while True: - state = self.pin.value() - if state != self.switchstate: - # State has changed: act on it now. - self.switchstate = state - if state == 0 and self._close_func: - launch(self._close_func, self._close_args) - elif state == 1 and self._open_func: - launch(self._open_func, self._open_args) - # Ignore further state changes until switch has settled - await asyncio.sleep_ms(Switch.debounce_ms) - -# An alternative Pushbutton solution with lower RAM use is available here -# https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py -class Pushbutton: - debounce_ms = 50 - long_press_ms = 1000 - double_click_ms = 400 - def __init__(self, pin, suppress=False): - self.pin = pin # Initialise for input - self._supp = suppress - self._dblpend = False # Doubleclick waiting for 2nd click - self._dblran = False # Doubleclick executed user function - self._tf = False - self._ff = False - self._df = False - self._lf = False - self._ld = False # Delay_ms instance for long press - self._dd = False # Ditto for doubleclick - self.sense = pin.value() # Convert from electrical to logical value - self.state = self.rawstate() # Initial state - loop = asyncio.get_event_loop() - loop.create_task(self.buttoncheck()) # Thread runs forever - - def press_func(self, func, args=()): - self._tf = func - self._ta = args - - def release_func(self, func, args=()): - self._ff = func - self._fa = args - - def double_func(self, func, args=()): - self._df = func - self._da = args - - def long_func(self, func, args=()): - self._lf = func - self._la = args - - # Current non-debounced logical button state: True == pressed - def rawstate(self): - return bool(self.pin.value() ^ self.sense) - - # Current debounced state of button (True == pressed) - def __call__(self): - return self.state - - def _ddto(self): # Doubleclick timeout: no doubleclick occurred - self._dblpend = False - if self._supp and not self.state: - if not self._ld or (self._ld and not self._ld()): - launch(self._ff, self._fa) - - async def buttoncheck(self): - if self._lf: # Instantiate timers if funcs exist - self._ld = Delay_ms(self._lf, self._la) - if self._df: - self._dd = Delay_ms(self._ddto) - while True: - state = self.rawstate() - # State has changed: act on it now. - if state != self.state: - self.state = state - if state: # Button pressed: launch pressed func - if self._tf: - launch(self._tf, self._ta) - if self._lf: # There's a long func: start long press delay - self._ld.trigger(Pushbutton.long_press_ms) - if self._df: - if self._dd(): # Second click: timer running - self._dd.stop() - self._dblpend = False - self._dblran = True # Prevent suppressed launch on release - launch(self._df, self._da) - else: - # First click: start doubleclick timer - self._dd.trigger(Pushbutton.double_click_ms) - self._dblpend = True # Prevent suppressed launch on release - else: # Button release. Is there a release func? - if self._ff: - if self._supp: - d = self._ld - # If long delay exists, is running and doubleclick status is OK - if not self._dblpend and not self._dblran: - if (d and d()) or not d: - launch(self._ff, self._fa) - else: - launch(self._ff, self._fa) - if self._ld: - self._ld.stop() # Avoid interpreting a second click as a long push - self._dblran = False - # Ignore state changes until switch has settled - await asyncio.sleep_ms(Pushbutton.debounce_ms) diff --git a/v2/asyn.py b/v2/asyn.py deleted file mode 100644 index c87c175..0000000 --- a/v2/asyn.py +++ /dev/null @@ -1,470 +0,0 @@ -# asyn.py 'micro' synchronisation primitives for uasyncio -# Test/demo programs asyntest.py, barrier_test.py -# Provides Lock, Event, Barrier, Semaphore, BoundedSemaphore, Condition, -# NamedTask and Cancellable classes, also sleep coro. -# Updated 31 Dec 2017 for uasyncio.core V1.6 and to provide task cancellation. - -# The MIT License (MIT) -# -# Copyright (c) 2017 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# CPython 3.5 compatibility -# (ignore RuntimeWarning: coroutine '_g' was never awaited) - -try: - import uasyncio as asyncio -except ImportError: - import asyncio - - -async def _g(): - pass -type_coro = type(_g()) - -# If a callback is passed, run it and return. -# If a coro is passed initiate it and return. -# coros are passed by name i.e. not using function call syntax. -def launch(func, tup_args): - res = func(*tup_args) - if isinstance(res, type_coro): - loop = asyncio.get_event_loop() - loop.create_task(res) - - -# To access a lockable resource a coro should issue -# async with lock_instance: -# access the locked resource - -# Alternatively: -# await lock.acquire() -# try: -# do stuff with locked resource -# finally: -# lock.release -# Uses normal scheduling on assumption that locks are held briefly. -class Lock(): - def __init__(self, delay_ms=0): - self._locked = False - self.delay_ms = delay_ms - - def locked(self): - return self._locked - - async def __aenter__(self): - await self.acquire() - return self - - async def __aexit__(self, *args): - self.release() - await asyncio.sleep(0) - - async def acquire(self): - while True: - if self._locked: - await asyncio.sleep_ms(self.delay_ms) - else: - self._locked = True - break - - def release(self): - if not self._locked: - raise RuntimeError('Attempt to release a lock which has not been set') - self._locked = False - - -# A coro waiting on an event issues await event -# A coro rasing the event issues event.set() -# When all waiting coros have run -# event.clear() should be issued -class Event(): - def __init__(self, delay_ms=0): - self.delay_ms = delay_ms - self.clear() - - def clear(self): - self._flag = False - self._data = None - - async def wait(self): # CPython comptaibility - while not self._flag: - await asyncio.sleep_ms(self.delay_ms) - - def __await__(self): - while not self._flag: - await asyncio.sleep_ms(self.delay_ms) - - __iter__ = __await__ - - def is_set(self): - return self._flag - - def set(self, data=None): - self._flag = True - self._data = data - - def value(self): - return self._data - -# A Barrier synchronises N coros. Each issues await barrier. -# Execution pauses until all other participant coros are waiting on it. -# At that point the callback is executed. Then the barrier is 'opened' and -# execution of all participants resumes. - -# The nowait arg is to support task cancellation. It enables usage where one or -# more coros can register that they have reached the barrier without waiting -# for it. Any coros waiting normally on the barrier will pause until all -# non-waiting coros have passed the barrier and all waiting ones have reached -# it. The use of nowait promotes efficiency by enabling tasks which have been -# cancelled to leave the task queue as soon as possible. - -class Barrier(): - def __init__(self, participants, func=None, args=()): - self._participants = participants - self._func = func - self._args = args - self._reset(True) - - def __await__(self): - self._update() - if self._at_limit(): # All other threads are also at limit - if self._func is not None: - launch(self._func, self._args) - self._reset(not self._down) # Toggle direction to release others - return - - direction = self._down - while True: # Wait until last waiting thread changes the direction - if direction != self._down: - return - await asyncio.sleep_ms(0) - - __iter__ = __await__ - - def trigger(self): - self._update() - if self._at_limit(): # All other threads are also at limit - if self._func is not None: - launch(self._func, self._args) - self._reset(not self._down) # Toggle direction to release others - - def _reset(self, down): - self._down = down - self._count = self._participants if down else 0 - - def busy(self): - if self._down: - done = self._count == self._participants - else: - done = self._count == 0 - return not done - - def _at_limit(self): # Has count reached up or down limit? - limit = 0 if self._down else self._participants - return self._count == limit - - def _update(self): - self._count += -1 if self._down else 1 - if self._count < 0 or self._count > self._participants: - raise ValueError('Too many tasks accessing Barrier') - -# A Semaphore is typically used to limit the number of coros running a -# particular piece of code at once. The number is defined in the constructor. -class Semaphore(): - def __init__(self, value=1): - self._count = value - - async def __aenter__(self): - await self.acquire() - return self - - async def __aexit__(self, *args): - self.release() - await asyncio.sleep(0) - - async def acquire(self): - while self._count == 0: - await asyncio.sleep_ms(0) - self._count -= 1 - - def release(self): - self._count += 1 - -class BoundedSemaphore(Semaphore): - def __init__(self, value=1): - super().__init__(value) - self._initial_value = value - - def release(self): - if self._count < self._initial_value: - self._count += 1 - else: - raise ValueError('Semaphore released more than acquired') - -# Task Cancellation -try: - StopTask = asyncio.CancelledError # More descriptive name -except AttributeError: - raise OSError('asyn.py requires uasyncio V1.7.1 or above.') - -class TaskId(): - def __init__(self, taskid): - self.taskid = taskid - - def __call__(self): - return self.taskid - -# Sleep coro breaks up a sleep into shorter intervals to ensure a rapid -# response to StopTask exceptions. Only relevant to official uasyncio V2.0. -async def sleep(t, granularity=100): # 100ms default - if granularity <= 0: - raise ValueError('sleep granularity must be > 0') - t = int(t * 1000) # ms - if t <= granularity: - await asyncio.sleep_ms(t) - else: - n, rem = divmod(t, granularity) - for _ in range(n): - await asyncio.sleep_ms(granularity) - await asyncio.sleep_ms(rem) - -# Anonymous cancellable tasks. These are members of a group which is identified -# by a user supplied name/number (default 0). Class method cancel_all() cancels -# all tasks in a group and awaits confirmation. Confirmation of ending (whether -# normally or by cancellation) is signalled by a task calling the _stopped() -# class method. Handled by the @cancellable decorator. - - -class Cancellable(): - task_no = 0 # Generated task ID, index of tasks dict - tasks = {} # Value is [coro, group, barrier] indexed by integer task_no - - @classmethod - def _cancel(cls, task_no): - task = cls.tasks[task_no][0] - asyncio.cancel(task) - - @classmethod - async def cancel_all(cls, group=0, nowait=False): - tokill = cls._get_task_nos(group) - barrier = Barrier(len(tokill) + 1) # Include this task - for task_no in tokill: - cls.tasks[task_no][2] = barrier - cls._cancel(task_no) - if nowait: - barrier.trigger() - else: - await barrier - - @classmethod - def _is_running(cls, group=0): - tasks = cls._get_task_nos(group) - if tasks == []: - return False - for task_no in tasks: - barrier = cls.tasks[task_no][2] - if barrier is None: # Running, not yet cancelled - return True - if barrier.busy(): - return True - return False - - @classmethod - def _get_task_nos(cls, group): # Return task nos in a group - return [task_no for task_no in cls.tasks if cls.tasks[task_no][1] == group] - - @classmethod - def _get_group(cls, task_no): # Return group given a task_no - return cls.tasks[task_no][1] - - @classmethod - def _stopped(cls, task_no): - if task_no in cls.tasks: - barrier = cls.tasks[task_no][2] - if barrier is not None: # Cancellation in progress - barrier.trigger() - del cls.tasks[task_no] - - def __init__(self, gf, *args, group=0, **kwargs): - task = gf(TaskId(Cancellable.task_no), *args, **kwargs) - if task in self.tasks: - raise ValueError('Task already exists.') - self.tasks[Cancellable.task_no] = [task, group, None] - self.task_no = Cancellable.task_no # For subclass - Cancellable.task_no += 1 - self.task = task - - def __call__(self): - return self.task - - def __await__(self): # Return any value returned by task. - return (yield from self.task) - - __iter__ = __await__ - - -# @cancellable decorator - -def cancellable(f): - def new_gen(*args, **kwargs): - if isinstance(args[0], TaskId): # Not a bound method - task_id = args[0] - g = f(*args[1:], **kwargs) - else: # Task ID is args[1] if a bound method - task_id = args[1] - args = (args[0],) + args[2:] - g = f(*args, **kwargs) - try: - res = await g - return res - finally: - NamedTask._stopped(task_id) - return new_gen - -# The NamedTask class enables a coro to be identified by a user defined name. -# It constrains Cancellable to allow groups of one coro only. -# It maintains a dict of barriers indexed by name. -class NamedTask(Cancellable): - instances = {} - - @classmethod - async def cancel(cls, name, nowait=True): - if name in cls.instances: - await cls.cancel_all(group=name, nowait=nowait) - return True - return False - - @classmethod - def is_running(cls, name): - return cls._is_running(group=name) - - @classmethod - def _stopped(cls, task_id): # On completion remove it - name = cls._get_group(task_id()) # Convert task_id to task_no - if name in cls.instances: - instance = cls.instances[name] - barrier = instance.barrier - if barrier is not None: - barrier.trigger() - del cls.instances[name] - Cancellable._stopped(task_id()) - - def __init__(self, name, gf, *args, barrier=None, **kwargs): - if name in self.instances: - raise ValueError('Task name "{}" already exists.'.format(name)) - super().__init__(gf, *args, group=name, **kwargs) - self.barrier = barrier - self.instances[name] = self - - -# @namedtask -namedtask = cancellable # compatibility with old code - -# Condition class - -class Condition(): - def __init__(self, lock=None): - self.lock = Lock() if lock is None else lock - self.events = [] - - async def acquire(self): - await self.lock.acquire() - -# enable this syntax: -# with await condition [as cond]: - def __await__(self): - yield from self.lock.acquire() - return self - - __iter__ = __await__ - - def __enter__(self): - return self - - def __exit__(self, *_): - self.lock.release() - - def locked(self): - return self.lock.locked() - - def release(self): - self.lock.release() # Will raise RuntimeError if not locked - - def notify(self, n=1): # Caller controls lock - if not self.lock.locked(): - raise RuntimeError('Condition notify with lock not acquired.') - for _ in range(min(n, len(self.events))): - ev = self.events.pop() - ev.set() - - def notify_all(self): - self.notify(len(self.events)) - - async def wait(self): - if not self.lock.locked(): - raise RuntimeError('Condition wait with lock not acquired.') - ev = Event() - self.events.append(ev) - self.lock.release() - await ev - await self.lock.acquire() - assert ev not in self.events, 'condition wait assertion fail' - return True # CPython compatibility - - async def wait_for(self, predicate): - result = predicate() - while not result: - await self.wait() - result = predicate() - return result - -# Provide functionality similar to asyncio.gather() - -class Gather(): - def __init__(self, gatherables): - ncoros = len(gatherables) - self.barrier = Barrier(ncoros + 1) - self.results = [None] * ncoros - loop = asyncio.get_event_loop() - for n, gatherable in enumerate(gatherables): - loop.create_task(self.wrap(gatherable, n)()) - - def __iter__(self): - yield from self.barrier.__await__() - return self.results - - def wrap(self, gatherable, idx): - async def wrapped(): - coro, args, kwargs = gatherable() - try: - tim = kwargs.pop('timeout') - except KeyError: - self.results[idx] = await coro(*args, **kwargs) - else: - self.results[idx] = await asyncio.wait_for(coro(*args, **kwargs), tim) - self.barrier.trigger() - return wrapped - -class Gatherable(): - def __init__(self, coro, *args, **kwargs): - self.arguments = coro, args, kwargs - - def __call__(self): - return self.arguments diff --git a/v2/asyn_demos.py b/v2/asyn_demos.py deleted file mode 100644 index e4781b1..0000000 --- a/v2/asyn_demos.py +++ /dev/null @@ -1,131 +0,0 @@ -# asyn_demos.py Simple demos of task cancellation -# Test/demo of official asyncio library and official Lock class - -# The MIT License (MIT) -# -# Copyright (c) 2017 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import uasyncio as asyncio -import asyn - -def print_tests(): - st = '''Minimal demo programs of uasyncio task cancellation. -Issue ctrl-D to soft reset the board between test runs. -Available demos: -cancel_test() Demo of Cancellable tasks. -named_test() Demo of NamedTask. -method_test() Cancellable and NamedTask coros as bound methods. -''' - print('\x1b[32m') - print(st) - print('\x1b[39m') - -print_tests() - -# Cancellable task minimal example -@asyn.cancellable -async def print_nums(num): - while True: - print(num) - num += 1 - await asyn.sleep(1) - -@asyn.cancellable -async def add_one(num): - num += 1 - await asyn.sleep(1) - return num - -async def run_cancel_test(loop): - res = await asyn.Cancellable(add_one, 41) - print('Result: ', res) - loop.create_task(asyn.Cancellable(print_nums, res)()) - await asyn.sleep(7.5) - # Cancel any cancellable tasks still running - await asyn.Cancellable.cancel_all() - print('Done') - -def cancel_test(): - loop = asyncio.get_event_loop() - loop.run_until_complete(run_cancel_test(loop)) - -# NamedTask minimal example - -@asyn.cancellable -async def print_nums_named(num): - while True: - print(num) - num += 1 - await asyn.sleep(1) - -@asyn.cancellable -async def add_one_named(num): - num += 1 - await asyn.sleep(1) - return num - -async def run_named_test(loop): - res = await asyn.NamedTask('not cancelled', add_one_named, 99) - print('Result: ', res) - loop.create_task(asyn.NamedTask('print nums', print_nums_named, res)()) - await asyn.sleep(7.5) - asyn.NamedTask.cancel('not cancelled') # Nothing to do: task has finished - asyn.NamedTask.cancel('print nums') # Stop the continuously running task - print('Done') - -def named_test(): - loop = asyncio.get_event_loop() - loop.run_until_complete(run_named_test(loop)) - -# Tasks as bound methods - -class CanDemo(): - async def start(self, loop): - loop.create_task(asyn.Cancellable(self.foo, 1)()) # 3 instances in default group 0 - loop.create_task(asyn.Cancellable(self.foo, 2)()) - loop.create_task(asyn.Cancellable(self.foo, 3)()) - loop.create_task(asyn.NamedTask('my bar', self.bar, 4)()) - print('bar running status is', asyn.NamedTask.is_running('my bar')) - await asyncio.sleep(4.5) - await asyn.NamedTask.cancel('my bar') - print('bar instance scheduled for cancellation.') - await asyn.Cancellable.cancel_all() - print('foo instances have been cancelled.') - await asyncio.sleep(0.2) # Allow for 100ms latency in bar() - print('bar running status is', asyn.NamedTask.is_running('my bar')) - print('Done') - - @asyn.cancellable - async def foo(self, arg): - while True: - await asyn.sleep(1) - print('foo running, arg', arg) - - @asyn.cancellable - async def bar(self, arg): - while True: - await asyn.sleep(1) - print('bar running, arg', arg) - -def method_test(): - cantest = CanDemo() - loop = asyncio.get_event_loop() - loop.run_until_complete(cantest.start(loop)) diff --git a/v2/asyntest.py b/v2/asyntest.py deleted file mode 100644 index 26c874c..0000000 --- a/v2/asyntest.py +++ /dev/null @@ -1,426 +0,0 @@ -# asyntest.py Test/demo of the 'micro' Event, Barrier and Semaphore classes -# Test/demo of official asyncio library and official Lock class - -# The MIT License (MIT) -# -# Copyright (c) 2017-2018 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# CPython 3.5 compatibility -# (ignore RuntimeWarning: coroutine '_g' was never awaited) - -try: - import uasyncio as asyncio -except ImportError: - import asyncio - -import asyn - -def print_tests(): - st = '''Available functions: -print_tests() Print this list. -ack_test() Test event acknowledge. -event_test() Test Event and Lock objects. -barrier_test() Test the Barrier class. -semaphore_test(bounded=False) Test Semaphore or BoundedSemaphore. -condition_test(new=False) Test the Condition class. Set arg True for new uasyncio. -gather_test() Test the Gather class - -Recommended to issue ctrl-D after running each test. -''' - print('\x1b[32m') - print(st) - print('\x1b[39m') - -print_tests() - -def printexp(exp, runtime=0): - print('Expected output:') - print('\x1b[32m') - print(exp) - print('\x1b[39m') - if runtime: - print('Running (runtime = {}s):'.format(runtime)) - else: - print('Running (runtime < 1s):') - -# ************ Test Event class ************ -# Demo use of acknowledge event - -async def event_wait(event, ack_event, n): - await event - print('Eventwait {} got event with value {}'.format(n, event.value())) - ack_event.set() - -async def run_ack(): - loop = asyncio.get_event_loop() - event = asyn.Event() - ack1 = asyn.Event() - ack2 = asyn.Event() - count = 0 - while True: - loop.create_task(event_wait(event, ack1, 1)) - loop.create_task(event_wait(event, ack2, 2)) - event.set(count) - count += 1 - print('event was set') - await ack1 - ack1.clear() - print('Cleared ack1') - await ack2 - ack2.clear() - print('Cleared ack2') - event.clear() - print('Cleared event') - await asyncio.sleep(1) - -async def ack_coro(delay): - await asyncio.sleep(delay) - print("I've seen attack ships burn on the shoulder of Orion...") - print("Time to die...") - -def ack_test(): - printexp('''event was set -Eventwait 1 got event with value 0 -Eventwait 2 got event with value 0 -Cleared ack1 -Cleared ack2 -Cleared event -event was set -Eventwait 1 got event with value 1 -Eventwait 2 got event with value 1 - -... text omitted ... - -Eventwait 1 got event with value 9 -Eventwait 2 got event with value 9 -Cleared ack1 -Cleared ack2 -Cleared event -I've seen attack ships burn on the shoulder of Orion... -Time to die... -''', 10) - loop = asyncio.get_event_loop() - loop.create_task(run_ack()) - loop.run_until_complete(ack_coro(10)) - -# ************ Test Lock and Event classes ************ - -async def run_lock(n, lock): - print('run_lock {} waiting for lock'.format(n)) - await lock.acquire() - print('run_lock {} acquired lock'.format(n)) - await asyncio.sleep(1) # Delay to demo other coros waiting for lock - lock.release() - print('run_lock {} released lock'.format(n)) - -async def eventset(event): - print('Waiting 5 secs before setting event') - await asyncio.sleep(5) - event.set() - print('event was set') - -async def eventwait(event): - print('waiting for event') - await event - print('got event') - event.clear() - -async def run_event_test(): - print('Test Lock class') - loop = asyncio.get_event_loop() - lock = asyn.Lock() - loop.create_task(run_lock(1, lock)) - loop.create_task(run_lock(2, lock)) - loop.create_task(run_lock(3, lock)) - print('Test Event class') - event = asyn.Event() - loop.create_task(eventset(event)) - await eventwait(event) # run_event_test runs fast until this point - print('Event status {}'.format('Incorrect' if event.is_set() else 'OK')) - print('Tasks complete') - -def event_test(): - printexp('''Test Lock class -Test Event class -waiting for event -run_lock 1 waiting for lock -run_lock 1 acquired lock -run_lock 2 waiting for lock -run_lock 3 waiting for lock -Waiting 5 secs before setting event -run_lock 1 released lock -run_lock 2 acquired lock -run_lock 2 released lock -run_lock 3 acquired lock -run_lock 3 released lock -event was set -got event -Event status OK -Tasks complete -''', 5) - loop = asyncio.get_event_loop() - loop.run_until_complete(run_event_test()) - -# ************ Barrier test ************ - -async def killer(duration): - await asyncio.sleep(duration) - -def callback(text): - print(text) - -async def report(barrier): - for i in range(5): - print('{} '.format(i), end='') - await barrier - -def barrier_test(): - printexp('''0 0 0 Synch -1 1 1 Synch -2 2 2 Synch -3 3 3 Synch -4 4 4 Synch -''') - barrier = asyn.Barrier(3, callback, ('Synch',)) - loop = asyncio.get_event_loop() - for _ in range(3): - loop.create_task(report(barrier)) - loop.run_until_complete(killer(2)) - loop.close() - -# ************ Semaphore test ************ - -async def run_sema(n, sema, barrier): - print('run_sema {} trying to access semaphore'.format(n)) - async with sema: - print('run_sema {} acquired semaphore'.format(n)) - # Delay demonstrates other coros waiting for semaphore - await asyncio.sleep(1 + n/10) # n/10 ensures deterministic printout - print('run_sema {} has released semaphore'.format(n)) - barrier.trigger() - -async def run_sema_test(bounded): - num_coros = 5 - loop = asyncio.get_event_loop() - barrier = asyn.Barrier(num_coros + 1) - if bounded: - semaphore = asyn.BoundedSemaphore(3) - else: - semaphore = asyn.Semaphore(3) - for n in range(num_coros): - loop.create_task(run_sema(n, semaphore, barrier)) - await barrier # Quit when all coros complete - try: - semaphore.release() - except ValueError: - print('Bounded semaphore exception test OK') - -def semaphore_test(bounded=False): - if bounded: - exp = '''run_sema 0 trying to access semaphore -run_sema 0 acquired semaphore -run_sema 1 trying to access semaphore -run_sema 1 acquired semaphore -run_sema 2 trying to access semaphore -run_sema 2 acquired semaphore -run_sema 3 trying to access semaphore -run_sema 4 trying to access semaphore -run_sema 0 has released semaphore -run_sema 4 acquired semaphore -run_sema 1 has released semaphore -run_sema 3 acquired semaphore -run_sema 2 has released semaphore -run_sema 4 has released semaphore -run_sema 3 has released semaphore -Bounded semaphore exception test OK - -Exact sequence of acquisition may vary when 3 and 4 compete for semaphore.''' - else: - exp = '''run_sema 0 trying to access semaphore -run_sema 0 acquired semaphore -run_sema 1 trying to access semaphore -run_sema 1 acquired semaphore -run_sema 2 trying to access semaphore -run_sema 2 acquired semaphore -run_sema 3 trying to access semaphore -run_sema 4 trying to access semaphore -run_sema 0 has released semaphore -run_sema 3 acquired semaphore -run_sema 1 has released semaphore -run_sema 4 acquired semaphore -run_sema 2 has released semaphore -run_sema 3 has released semaphore -run_sema 4 has released semaphore - -Exact sequence of acquisition may vary when 3 and 4 compete for semaphore.''' - printexp(exp, 3) - loop = asyncio.get_event_loop() - loop.run_until_complete(run_sema_test(bounded)) - -# ************ Condition test ************ - -cond = asyn.Condition() -tim = 0 - -@asyn.cancellable -async def cond01(): - while True: - await asyncio.sleep(2) - with await cond: - cond.notify(2) # Notify 2 tasks - -@asyn.cancellable -async def cond03(): # Maintain a count of seconds - global tim - await asyncio.sleep(0.5) - while True: - await asyncio.sleep(1) - tim += 1 - -async def cond01_new(): - while True: - await asyncio.sleep(2) - with await cond: - cond.notify(2) # Notify 2 tasks - -async def cond03_new(): # Maintain a count of seconds - global tim - await asyncio.sleep(0.5) - while True: - await asyncio.sleep(1) - tim += 1 - -async def cond02(n, barrier): - with await cond: - print('cond02', n, 'Awaiting notification.') - await cond.wait() - print('cond02', n, 'triggered. tim =', tim) - barrier.trigger() - -def predicate(): - return tim >= 8 # 12 - -async def cond04(n, barrier): - with await cond: - print('cond04', n, 'Awaiting notification and predicate.') - await cond.wait_for(predicate) - print('cond04', n, 'triggered. tim =', tim) - barrier.trigger() - -async def cond_go(loop, new): - ntasks = 7 - barrier = asyn.Barrier(ntasks + 1) - if new: - t1 = asyncio.create_task(cond01_new()) - t3 = asyncio.create_task(cond03_new()) - else: - loop.create_task(asyn.Cancellable(cond01)()) - loop.create_task(asyn.Cancellable(cond03)()) - for n in range(ntasks): - loop.create_task(cond02(n, barrier)) - await barrier # All instances of cond02 have completed - # Test wait_for - barrier = asyn.Barrier(2) - loop.create_task(cond04(99, barrier)) - await barrier - # cancel continuously running coros. - if new: - t1.cancel() - t3.cancel() - await asyncio.sleep_ms(0) - else: - await asyn.Cancellable.cancel_all() - print('Done.') - -def condition_test(new=False): - printexp('''cond02 0 Awaiting notification. -cond02 1 Awaiting notification. -cond02 2 Awaiting notification. -cond02 3 Awaiting notification. -cond02 4 Awaiting notification. -cond02 5 Awaiting notification. -cond02 6 Awaiting notification. -cond02 5 triggered. tim = 1 -cond02 6 triggered. tim = 1 -cond02 3 triggered. tim = 3 -cond02 4 triggered. tim = 3 -cond02 1 triggered. tim = 5 -cond02 2 triggered. tim = 5 -cond02 0 triggered. tim = 7 -cond04 99 Awaiting notification and predicate. -cond04 99 triggered. tim = 9 -Done. -''', 13) - loop = asyncio.get_event_loop() - loop.run_until_complete(cond_go(loop, new)) - -# ************ Gather test ************ - -# Task with one positional arg. Demonstrate that result order depends on -# original list order not termination order. -async def gath01(n): - print('gath01', n, 'started') - await asyncio.sleep(3 - n/10) - print('gath01', n, 'done') - return n - -# Takes kwarg. This is last to terminate. -async def gath02(x, y, rats): - print('gath02 started') - await asyncio.sleep(7) - print('gath02 done') - return x * y, rats - -# Only quits on timeout -async def gath03(n): - print('gath03 started') - try: - while True: - await asyncio.sleep(1) - n += 1 - except asyncio.TimeoutError: - print('gath03 timeout') - return n - -async def gath_go(): - gatherables = [asyn.Gatherable(gath01, n) for n in range(4)] - gatherables.append(asyn.Gatherable(gath02, 7, 8, rats=77)) - gatherables.append(asyn.Gatherable(gath03, 0, timeout=5)) - res = await asyn.Gather(gatherables) - print(res) - -def gather_test(): - printexp('''gath01 0 started -gath01 1 started -gath01 2 started -gath01 3 started -gath02 started -gath03 started -gath01 3 done -gath01 2 done -gath01 1 done -gath01 0 done -gath03 timeout -gath02 done -[0, 1, 2, 3, (56, 77), 4] -''', 7) - loop = asyncio.get_event_loop() - loop.run_until_complete(gath_go()) diff --git a/v2/auart.py b/v2/auart.py deleted file mode 100644 index 8600529..0000000 --- a/v2/auart.py +++ /dev/null @@ -1,25 +0,0 @@ -# Test of uasyncio stream I/O using UART -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license -# Link X1 and X2 to test. - -import uasyncio as asyncio -from pyb import UART -uart = UART(4, 9600) - -async def sender(): - swriter = asyncio.StreamWriter(uart, {}) - while True: - await swriter.awrite('Hello uart\n') - await asyncio.sleep(2) - -async def receiver(): - sreader = asyncio.StreamReader(uart) - while True: - res = await sreader.readline() - print('Recieved', res) - -loop = asyncio.get_event_loop() -loop.create_task(sender()) -loop.create_task(receiver()) -loop.run_forever() diff --git a/v2/auart_hd.py b/v2/auart_hd.py deleted file mode 100644 index da2b33e..0000000 --- a/v2/auart_hd.py +++ /dev/null @@ -1,106 +0,0 @@ -# auart_hd.py -# Author: Peter Hinch -# Copyright Peter Hinch 2018 Released under the MIT license - -# Demo of running a half-duplex protocol to a device. The device never sends -# unsolicited messages. An example is a communications device which responds -# to AT commands. -# The master sends a message to the device, which may respond with one or more -# lines of data. The master assumes that the device has sent all its data when -# a timeout has elapsed. - -# In this test a physical device is emulated by the DEVICE class -# To test link X1-X4 and X2-X3 - -from pyb import UART -import uasyncio as asyncio -import aswitch - -# Dummy device waits for any incoming line and responds with 4 lines at 1 second -# intervals. -class DEVICE(): - def __init__(self, uart_no = 4): - self.uart = UART(uart_no, 9600) - self.loop = asyncio.get_event_loop() - self.swriter = asyncio.StreamWriter(self.uart, {}) - self.sreader = asyncio.StreamReader(self.uart) - loop = asyncio.get_event_loop() - loop.create_task(self._run()) - - async def _run(self): - responses = ['Line 1', 'Line 2', 'Line 3', 'Goodbye'] - while True: - res = await self.sreader.readline() - for response in responses: - await self.swriter.awrite("{}\r\n".format(response)) - # Demo the fact that the master tolerates slow response. - await asyncio.sleep_ms(300) - -# The master's send_command() method sends a command and waits for a number of -# lines from the device. The end of the process is signified by a timeout, when -# a list of lines is returned. This allows line-by-line processing. -# A special test mode demonstrates the behaviour with a non-responding device. If -# None is passed, no commend is sent. The master waits for a response which never -# arrives and returns an empty list. -class MASTER(): - def __init__(self, uart_no = 2, timeout=4000): - self.uart = UART(uart_no, 9600) - self.timeout = timeout - self.loop = asyncio.get_event_loop() - self.swriter = asyncio.StreamWriter(self.uart, {}) - self.sreader = asyncio.StreamReader(self.uart) - self.delay = aswitch.Delay_ms() - self.response = [] - loop = asyncio.get_event_loop() - loop.create_task(self._recv()) - - async def _recv(self): - while True: - res = await self.sreader.readline() - self.response.append(res) # Append to list of lines - self.delay.trigger(self.timeout) # Got something, retrigger timer - - async def send_command(self, command): - self.response = [] # Discard any pending messages - if command is None: - print('Timeout test.') - else: - await self.swriter.awrite("{}\r\n".format(command)) - print('Command sent:', command) - self.delay.trigger(self.timeout) # Re-initialise timer - while self.delay.running(): - await asyncio.sleep(1) # Wait for 4s after last msg received - return self.response - -async def test(): - print('This test takes 10s to complete.') - for cmd in ['Run', None]: - print() - res = await master.send_command(cmd) - # can use b''.join(res) if a single string is required. - if res: - print('Result is:') - for line in res: - print(line.decode('UTF8'), end='') - else: - print('Timed out waiting for result.') - -loop = asyncio.get_event_loop() -master = MASTER() -device = DEVICE() -loop.run_until_complete(test()) - -# Expected output -# >>> import auart_hd -# This test takes 10s to complete. -# -# Command sent: Run -# Result is: -# Line 1 -# Line 2 -# Line 3 -# Goodbye -# -# Timeout test. -# Timed out waiting for result. -# >>> diff --git a/v2/awaitable.py b/v2/awaitable.py deleted file mode 100644 index a9087f6..0000000 --- a/v2/awaitable.py +++ /dev/null @@ -1,32 +0,0 @@ -# awaitable.py Demo of an awaitable class -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license -# runs in CPython and MicroPython -# Trivial fix for MicroPython issue #2678 - -try: - import uasyncio as asyncio -except ImportError: - import asyncio - -class Hardware(object): - def __init__(self, count): - self.count = count - - def __await__(self): # Typical use, loop until an interface becomes ready. - while self.count: - print(self.count) - yield - self.count -= 1 - - __iter__ = __await__ # issue #2678 - -loop = asyncio.get_event_loop() - -hardware = Hardware(10) - -async def run(): - await hardware - print('Done') - -loop.run_until_complete(run()) diff --git a/v2/benchmarks/call_lp.py b/v2/benchmarks/call_lp.py deleted file mode 100644 index 813787f..0000000 --- a/v2/benchmarks/call_lp.py +++ /dev/null @@ -1,43 +0,0 @@ -# call_lp.py Demo of low priority callback. Author Peter Hinch July 2018. -# Requires fast_io version of core.py - -import pyb -import uasyncio as asyncio -try: - if not(isinstance(asyncio.version, tuple)): - raise AttributeError -except AttributeError: - raise OSError('This program requires uasyncio fast_io version V0.24 or above.') - -loop = asyncio.get_event_loop(lp_len=16) - -count = 0 -numbers = 0 - -async def report(): - await asyncio.after(2) - print('Callback executed {} times. Expected count 2000/20 = 100 times.'.format(count)) - print('Avg. of {} random numbers in range 0 to 1023 was {}'.format(count, numbers // count)) - -def callback(num): - global count, numbers - count += 1 - numbers += num // 2**20 # range 0 to 1023 - -def cb(arg): - print(arg) - -async def run_test(): - loop = asyncio.get_event_loop() - loop.call_after(1, cb, 'One second has elapsed.') # Test args - loop.call_after_ms(500, cb, '500ms has elapsed.') - print('Callbacks scheduled.') - while True: - loop.call_after(0, callback, pyb.rng()) # demo use of args - yield 20 # 20ms - -print('Test runs for 2 seconds') -loop = asyncio.get_event_loop() -loop.create_task(run_test()) -loop.run_until_complete(report()) - diff --git a/v2/benchmarks/latency.py b/v2/benchmarks/latency.py deleted file mode 100644 index 786cd22..0000000 --- a/v2/benchmarks/latency.py +++ /dev/null @@ -1,123 +0,0 @@ -# latency.py Benchmark for uasyncio. Author Peter Hinch July 2018. - -# This measures the scheduling latency of a notional device driver running in the -# presence of other coros. This can test asyncio_priority.py which incorporates -# the priority mechanism. (In the home directory of this repo). - -# When running the test that uses the priority mechanism the latency is 300us which -# is determined by the time it takes uasyncio to schedule a coro (see rate.py). -# This is because, when the priority() coro issues await device it is the only coro -# on the normal queue and it therefore is immediately scheduled. - -# When running the test without the priority mechanism, the latency is D*Nms where N -# is the number of instances of the foo() coro and D is the processing period of -# foo() in ms (2). This is because priority() will only be rescheduled after every -# foo() instance has run. - -# For compute-intensive tasks a yield every 2ms is reasonably efficient. A shorter -# period implies a significant proportion of CPU cycles being taken up in scheduling. - -import uasyncio as asyncio -lp_version = True -try: - if not(isinstance(asyncio.version, tuple)): - raise AttributeError -except AttributeError: - lp_version = False - -import pyb -import utime as time -import gc - -num_coros = (5, 10, 100, 200) -duration = 2 # Time to run for each number of coros -done = False - -tmax = 0 -tmin = 1000000 -dtotal = 0 -count = 0 -lst_tmax = [tmax] * len(num_coros) # Max, min and avg error values -lst_tmin = [tmin] * len(num_coros) -lst_sd = [0] * len(num_coros) - -class DummyDeviceDriver(): - def __iter__(self): - yield - -async def report(): - # Don't compromise results by executing too soon. Time round loop is duration + 1 - await after(1 + len(num_coros) * (duration + 1)) - print('Awaiting result...') - while not done: - await after_ms(1000) - s = 'Coros {:4d} Latency = {:6.2f}ms min. {:6.2f}ms max. {:6.2f}ms avg.' - for x, n in enumerate(num_coros): - print(s.format(n, lst_tmin[x] / 1000, lst_tmax[x] /1000, lst_sd[x] / 1000)) - -async def lp_task(delay): - await after_ms(0) # If running low priority get on LP queue ASAP - while True: - time.sleep_ms(delay) # Simulate processing - await after_ms(0) - -async def priority(): - global tmax, tmin, dtotal, count - device = DummyDeviceDriver() - while True: - await after(0) # Ensure low priority coros get to run - tstart = time.ticks_us() - await device # Measure the latency - delta = time.ticks_diff(time.ticks_us(), tstart) - tmax = max(tmax, delta) - tmin = min(tmin, delta) - dtotal += delta - count += 1 - -async def run_test(delay): - global done, tmax, tmin, dtotal, count - loop.create_task(priority()) - old_n = 0 - for n, n_coros in enumerate(num_coros): - print('{:4d} coros. Test for {}s'.format(n_coros, duration)) - for _ in range(n_coros - old_n): - loop.create_task(lp_task(delay)) - await asyncio.sleep(1) # ensure tasks are all on LP queue before we measure - gc.collect() # ensure gc doesn't cloud the issue - old_n = n_coros - tmax = 0 - tmin = 1000000 - dtotal = 0 - count = 0 - await asyncio.sleep(duration) - lst_tmin[n] = tmin - lst_tmax[n] = tmax - lst_sd[n] = dtotal / count - done = True - -def test(use_priority=True): - global after, after_ms, loop, lp_version - processing_delay = 2 # Processing time in low priority task (ms) - if use_priority and not lp_version: - print('To test priority mechanism you must use fast_io version of uasyncio.') - else: - ntasks = max(num_coros) + 10 #4 - if use_priority: - loop = asyncio.get_event_loop(ntasks, ntasks, 0, ntasks) - after = asyncio.after - after_ms = asyncio.after_ms - else: - lp_version = False - after = asyncio.sleep - after_ms = asyncio.sleep_ms - loop = asyncio.get_event_loop(ntasks, ntasks) - s = 'Testing latency of priority task with coros blocking for {}ms.' - print(s.format(processing_delay)) - if lp_version: - print('Using priority mechanism.') - else: - print('Not using priority mechanism.') - loop.create_task(run_test(processing_delay)) - loop.run_until_complete(report()) - -print('Issue latency.test() to test priority mechanism, latency.test(False) to test standard algo.') diff --git a/v2/benchmarks/overdue.py b/v2/benchmarks/overdue.py deleted file mode 100644 index a777f5e..0000000 --- a/v2/benchmarks/overdue.py +++ /dev/null @@ -1,40 +0,0 @@ -# overdue.py Test for "low priority" uasyncio. Author Peter Hinch April 2017. -import uasyncio as asyncio -try: - if not(isinstance(asyncio.version, tuple)): - raise AttributeError -except AttributeError: - raise OSError('This program requires uasyncio fast_io version V0.24 or above.') - -loop = asyncio.get_event_loop(lp_len=16) -ntimes = 0 - -async def lp_task(): - global ntimes - while True: - await asyncio.after_ms(100) - print('LP task runs.') - ntimes += 1 - -async def hp_task(): # Hog the scheduler - while True: - await asyncio.sleep_ms(0) - -async def report(): - global ntimes - loop.max_overdue_ms(1000) - loop.create_task(hp_task()) - loop.create_task(lp_task()) - print('First test runs for 10 secs. Max overdue time = 1s.') - await asyncio.sleep(10) - print('Low priority coro was scheduled {} times: (should be 9).'.format(ntimes)) - loop.max_overdue_ms(0) - ntimes = 0 - print('Second test runs for 10 secs. Default scheduling.') - print('Low priority coro should not be scheduled.') - await asyncio.sleep(10) - print('Low priority coro was scheduled {} times: (should be 0).'.format(ntimes)) - -loop = asyncio.get_event_loop() -loop.run_until_complete(report()) - diff --git a/v2/benchmarks/priority_test.py b/v2/benchmarks/priority_test.py deleted file mode 100644 index b6a4636..0000000 --- a/v2/benchmarks/priority_test.py +++ /dev/null @@ -1,78 +0,0 @@ -# priority_test.py -# Test/demo of task cancellation of low priority tasks -# Author: Peter Hinch -# Copyright Peter Hinch 2018 Released under the MIT license - -# Check availability of 'priority' version -import uasyncio as asyncio -try: - if not(isinstance(asyncio.version, tuple)): - raise AttributeError -except AttributeError: - raise OSError('This program requires uasyncio fast_io version V0.24 or above.') - -loop = asyncio.get_event_loop(lp_len=16) -import asyn - -def printexp(exp, runtime=0): - print('Expected output:') - print('\x1b[32m') - print(exp) - print('\x1b[39m') - if runtime: - print('Running (runtime = {}s):'.format(runtime)) - else: - print('Running (runtime < 1s):') - -@asyn.cancellable -async def foo(num): - print('Starting foo', num) - try: - await asyncio.after(1) - print('foo', num, 'ran to completion.') - except asyn.StopTask: - print('foo', num, 'was cancelled.') - -async def kill(task_name): - if await asyn.NamedTask.cancel(task_name): - print(task_name, 'will be cancelled when next scheduled') - else: - print(task_name, 'was not cancellable.') - -# Example of a task which cancels another -async def bar(): - await asyncio.sleep(1) - await kill('foo 0') # Will fail because it has completed - await kill('foo 1') - await kill('foo 3') # Will fail because not yet scheduled - -async def run_cancel_test(): - loop = asyncio.get_event_loop() - await asyn.NamedTask('foo 0', foo, 0) - loop.create_task(asyn.NamedTask('foo 1', foo, 1)()) - loop.create_task(bar()) - await asyncio.sleep(5) - await asyn.NamedTask('foo 2', foo, 2) - await asyn.NamedTask('foo 4', foo, 4) - loop.create_task(asyn.NamedTask('foo 3', foo, 3)()) - await asyncio.sleep(5) - -def test(): - printexp('''Starting foo 0 -foo 0 ran to completion. -Starting foo 1 -foo 0 was not cancellable. -foo 1 will be cancelled when next scheduled -foo 3 was not cancellable. -foo 1 was cancelled. -Starting foo 2 -foo 2 ran to completion. -Starting foo 4 -foo 4 ran to completion. -Starting foo 3 -foo 3 ran to completion. -''', 14) - loop = asyncio.get_event_loop() - loop.run_until_complete(run_cancel_test()) - -test() diff --git a/v2/benchmarks/rate.py b/v2/benchmarks/rate.py deleted file mode 100644 index 8b7ceb8..0000000 --- a/v2/benchmarks/rate.py +++ /dev/null @@ -1,48 +0,0 @@ -# rate.py Benchmark for uasyncio. Author Peter Hinch Feb 2018. -# Benchmark uasyncio round-robin scheduling performance -# This measures the rate at which uasyncio can schedule a minimal coro which -# mereley increments a global. - -# Outcome: 100 minimal coros are scheduled at an interval of ~156μs on official -# uasyncio V2. On fast_io version 0.1 (including low priority) at 162μs. -# fast_io overhead is < 4% - -import uasyncio as asyncio - -num_coros = (100, 200, 500, 1000) -iterations = [0, 0, 0, 0] -duration = 2 # Time to run for each number of coros -count = 0 -done = False - -async def report(): - while not done: - await asyncio.sleep(1) - for x, n in enumerate(num_coros): - print('Coros {:4d} Iterations/sec {:5d} Duration {:3d}us'.format( - n, int(iterations[x]/duration), int(duration*1000000/iterations[x]))) - -async def foo(): - global count - while True: - yield - count += 1 - -async def test(): - global count, done - old_n = 0 - for n, n_coros in enumerate(num_coros): - print('Testing {} coros for {}secs'.format(n_coros, duration)) - count = 0 - for _ in range(n_coros - old_n): - loop.create_task(foo()) - old_n = n_coros - await asyncio.sleep(duration) - iterations[n] = count - done = True - -ntasks = max(num_coros) + 2 -loop = asyncio.get_event_loop(ntasks, ntasks) -loop.create_task(test()) -loop.run_until_complete(report()) - diff --git a/v2/benchmarks/rate_esp.py b/v2/benchmarks/rate_esp.py deleted file mode 100644 index a2a54e4..0000000 --- a/v2/benchmarks/rate_esp.py +++ /dev/null @@ -1,50 +0,0 @@ -# rate_esp.py Benchmark for uasyncio. Author Peter Hinch April 2017. -# Benchmark uasyncio round-robin scheduling performance -# This measures the rate at which uasyncio can schedule a minimal coro which -# mereley increments a global. - -# Test for ESP8266. Times below at 160/80MHz -# Outcome: minimal coros are scheduled at an interval of ~1.2/1.76ms with 'yield' -# 1.7/2.5ms with 'await asyncio.sleep_ms(0)' - -import uasyncio as asyncio -from machine import freq -freq(80000000) - -num_coros = (10,) -iterations = [0,] -duration = 10 # Time to run for each number of coros -count = 0 -done = False - -async def report(): - while not done: - await asyncio.sleep(1) - for x, n in enumerate(num_coros): - print('Coros {:4d} Iterations/sec {:5d} Duration {:3d}us'.format( - n, int(iterations[x]/duration), int(duration*1000000/iterations[x]))) - -async def foo(): - global count - while True: - yield - count += 1 - -async def test(): - global count, done - old_n = 0 - for n, n_coros in enumerate(num_coros): - print('Testing {} coros for {}secs'.format(n_coros, duration)) - count = 0 - for _ in range(n_coros - old_n): - loop.create_task(foo()) - old_n = n_coros - await asyncio.sleep(duration) - iterations[n] = count - done = True - -ntasks = max(num_coros) + 2 -loop = asyncio.get_event_loop(ntasks, ntasks) -loop.create_task(test()) -loop.run_until_complete(report()) - diff --git a/v2/benchmarks/rate_fastio.py b/v2/benchmarks/rate_fastio.py deleted file mode 100644 index d1ce969..0000000 --- a/v2/benchmarks/rate_fastio.py +++ /dev/null @@ -1,48 +0,0 @@ -# rate_fastio.py Benchmark for uasyncio. Author Peter Hinch July 2018. -# This version tests the fast_io version when I/O is not pending. -# Benchmark uasyncio round-robin scheduling performance -# This measures the rate at which uasyncio can schedule a minimal coro which -# mereley increments a global. - -# This is identical to rate.py but instantiates io and lp queues -# Outcome: minimal coros are scheduled at an interval of ~206μs - -import uasyncio as asyncio - -num_coros = (100, 200, 500, 1000) -iterations = [0, 0, 0, 0] -duration = 2 # Time to run for each number of coros -count = 0 -done = False - -async def report(): - while not done: - await asyncio.sleep(1) - for x, n in enumerate(num_coros): - print('Coros {:4d} Iterations/sec {:5d} Duration {:3d}us'.format( - n, int(iterations[x]/duration), int(duration*1000000/iterations[x]))) - -async def foo(): - global count - while True: - yield - count += 1 - -async def test(): - global count, done - old_n = 0 - for n, n_coros in enumerate(num_coros): - print('Testing {} coros for {}secs'.format(n_coros, duration)) - count = 0 - for _ in range(n_coros - old_n): - loop.create_task(foo()) - old_n = n_coros - await asyncio.sleep(duration) - iterations[n] = count - done = True - -ntasks = max(num_coros) + 2 -loop = asyncio.get_event_loop(ntasks, ntasks, 6, 6) -loop.create_task(test()) -loop.run_until_complete(report()) - diff --git a/v2/cantest.py b/v2/cantest.py deleted file mode 100644 index e6cc43f..0000000 --- a/v2/cantest.py +++ /dev/null @@ -1,422 +0,0 @@ -# cantest.py Tests of task cancellation - -# The MIT License (MIT) -# -# Copyright (c) 2017-2018 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - - -import uasyncio as asyncio -import asyn -import utime as time - -def print_tests(): - st = '''Available functions: -test1() Basic NamedTask cancellation. -test2() Use of Barrier to synchronise NamedTask cancellation. Demo of latency. -test3() Cancellation of a NamedTask which has run to completion. -test4() Test of Cancellable class. -test5() Cancellable and NamedTask instances as bound methods. -test6() Test of NamedTask.is_running() and awaiting NamedTask cancellation. -Recommended to issue ctrl-D after running each test. -''' - print('\x1b[32m') - print(st) - print('\x1b[39m') - -print_tests() - -def printexp(exp, runtime=0): - print('Expected output:') - print('\x1b[32m') - print(exp) - print('\x1b[39m') - if runtime: - print('Running (runtime = {}s):'.format(runtime)) - else: - print('Running (runtime < 1s):') - -# cancel_test1() - -@asyn.cancellable -async def foo(num): - try: - await asyncio.sleep(4) - except asyn.StopTask: - print('foo was cancelled.') - return -1 - else: - return num + 42 - -async def kill(task_name): - res = await asyn.NamedTask.cancel(task_name) - if res: - print(task_name, 'will be cancelled when next scheduled') - else: - print(task_name, 'was not cancellable.') - -# Example of a task which cancels another -async def bar(): - await asyncio.sleep(1) - await kill('foo') - await kill('not me') # Will fail because not yet scheduled - -async def run_cancel_test1(): - loop = asyncio.get_event_loop() - loop.create_task(bar()) - res = await asyn.NamedTask('foo', foo, 5) - print(res, asyn.NamedTask.is_running('foo')) - res = await asyn.NamedTask('not me', foo, 0) # Runs to completion - print(res, asyn.NamedTask.is_running('not me')) - -def test1(): - printexp('''foo will be cancelled when next scheduled -not me was not cancellable. -foo was cancelled. --1 False -42 False -''', 8) - loop = asyncio.get_event_loop() - loop.run_until_complete(run_cancel_test1()) - -# test2() -# This test uses a barrier so that cancelling task pauses until cancelled tasks -# have actually terminated. Also tests the propagation of the thrown exception -# to the awaiting coro. - -async def forever(n): - print('Started forever() instance', n) - while True: - await asyncio.sleep(7 + n) - print('Running instance', n) - -# Intercepting the StopTask exception. -@asyn.cancellable -async def rats(n): - try: - await forever(n) - except asyn.StopTask: - print('Instance', n, 'was cancelled') - -async def run_cancel_test2(): - barrier = asyn.Barrier(3) - loop = asyncio.get_event_loop() - loop.create_task(asyn.NamedTask('rats_1', rats, 1, barrier=barrier)()) - loop.create_task(asyn.NamedTask('rats_2', rats, 2, barrier=barrier)()) - print('Running two tasks') - await asyncio.sleep(10) - print('About to cancel tasks') - await asyn.NamedTask.cancel('rats_1') # These will stop when their wait is complete - await asyn.NamedTask.cancel('rats_2') - await barrier # So wait for that to occur. - print('tasks were cancelled') - -def test2(): - printexp('''Running two tasks -Started forever() instance 1 -Started forever() instance 2 -Running instance 1 -Running instance 2 -About to cancel tasks -Instance 1 was cancelled -Instance 2 was cancelled -tasks were cancelled -''', 20) - loop = asyncio.get_event_loop() - loop.run_until_complete(run_cancel_test2()) - -# test3() -# Test of cancelling a task which has already terminated - -# Intercepting the StopTask exception. -@asyn.cancellable -async def cant3(): - try: - await asyncio.sleep(1) - print('Task cant3 has ended.') - except asyn.StopTask: - print('Task cant3 was cancelled') - -async def run_cancel_test3(): - barrier = asyn.Barrier(2) - loop = asyncio.get_event_loop() - loop.create_task(asyn.NamedTask('cant3', cant3, barrier=barrier)()) - print('Task cant3 running status', asyn.NamedTask.is_running('cant3')) - await asyncio.sleep(3) - print('Task cant3 running status', asyn.NamedTask.is_running('cant3')) - print('About to cancel task') - await asyn.NamedTask.cancel('cant3') - print('Cancelled') - print('Task cant3 running status', asyn.NamedTask.is_running('cant3')) - await barrier - print('tasks were cancelled') - -def test3(): - printexp('''Task cant3 running status True -Task cant3 has ended. -Task cant3 running status False -About to cancel task -Cancelled -Task cant3 running status False -tasks were cancelled -''', 3) - loop = asyncio.get_event_loop() - loop.run_until_complete(run_cancel_test3()) - -# test4() -# Test of cancelling a task which has already terminated - -# Cancellable coros can trap the StopTask. They are passed the -# task_id automatically - -@asyn.cancellable -async def cant40(num): - while True: - try: - await asyn.sleep(1) - print('Task cant40 no. {} running.'.format(num)) - except asyn.StopTask: - print('Task cant40 no. {} was cancelled'.format(num)) - return - -@asyn.cancellable -async def cant41(num, arg=0): - try: - await asyn.sleep(1) - print('Task cant41 no. {} running, arg {}.'.format(num, arg)) - except asyn.StopTask: - print('Task cant41 no. {} was cancelled.'.format(num)) - return - else: - print('Task cant41 no. {} ended.'.format(num)) - -async def cant42(num): - while True: - print('Task cant42 no. {} running'.format(num)) - await asyn.sleep(1.2) - -# Test await syntax and throwing exception to subtask -@asyn.cancellable -async def chained(num, x, y, *, red, blue): - print('Args:', x, y, red, blue) # Test args and kwargs - try: - await cant42(num) - except asyn.StopTask: - print('Task chained no. {} was cancelled'.format(num)) - -async def run_cancel_test4(): - await asyn.Cancellable(cant41, 0, 5) - loop = asyncio.get_event_loop() - loop.create_task(asyn.Cancellable(cant40, 1)()) # 3 instances in default group 0 - loop.create_task(asyn.Cancellable(cant40, 2)()) - loop.create_task(asyn.Cancellable(cant40, 3)()) - loop.create_task(asyn.Cancellable(chained, 4, 1, 2, red=3, blue=4, group=1)()) - loop.create_task(asyn.Cancellable(cant41, 5)()) # Runs to completion - print('Running tasks') - await asyncio.sleep(3) - print('About to cancel group 0 tasks') - await asyn.Cancellable.cancel_all() # All in default group 0 - print('Group 0 tasks were cancelled') - await asyncio.sleep(1) # Demo chained still running - print('About to cancel group 1 tasks') - await asyn.Cancellable.cancel_all(1) # Group 1 - print('Group 1 tasks were cancelled') - await asyncio.sleep(1) - -def test4(): - printexp('''Task cant41 no. 0 running, arg 5. -Task cant41 no. 0 ended. -Running tasks -Args: 1 2 3 4 -Task cant42 no. 4 running -Task cant40 no. 1 running. -Task cant40 no. 2 running. -Task cant40 no. 3 running. -Task cant41 no. 5 running, arg 0. -Task cant41 no. 5 ended. -Task cant42 no. 4 running -Task cant40 no. 1 running. -Task cant40 no. 2 running. -Task cant40 no. 3 running. -Task cant42 no. 4 running -About to cancel group 0 tasks -Task cant40 no. 1 was cancelled -Task cant40 no. 2 was cancelled -Task cant40 no. 3 was cancelled -Group 0 tasks were cancelled -Task cant42 no. 4 running -About to cancel group 1 tasks -Task chained no. 4 was cancelled -Group 1 tasks were cancelled -''', 6) - loop = asyncio.get_event_loop() - loop.run_until_complete(run_cancel_test4()) - -# test5 -# Test of task cancellation where tasks are bound methods - -class CanTest(): - async def start(self, loop): - loop.create_task(asyn.Cancellable(self.foo, 1)()) # 3 instances in default group 0 - loop.create_task(asyn.Cancellable(self.foo, 2)()) - loop.create_task(asyn.Cancellable(self.foo, 3)()) - loop.create_task(asyn.NamedTask('my bar', self.bar, 4, y=42)()) - await asyncio.sleep(4.5) - await asyn.NamedTask.cancel('my bar') - await asyn.Cancellable.cancel_all() - await asyncio.sleep(1) - print('Done') - - @asyn.cancellable - async def foo(self, arg): - try: - while True: - await asyn.sleep(1) - print('foo running, arg', arg) - except asyn.StopTask: - print('foo was cancelled') - - @asyn.cancellable - async def bar(self, arg, *, x=1, y=2): - try: - while True: - await asyn.sleep(1) - print('bar running, arg', arg, x, y) - except asyn.StopTask: - print('bar was cancelled') - -def test5(): - printexp('''foo running, arg 1 -foo running, arg 2 -foo running, arg 3 -bar running, arg 4 1 42 -foo running, arg 1 -foo running, arg 2 -foo running, arg 3 -bar running, arg 4 1 42 -foo running, arg 1 -foo running, arg 2 -foo running, arg 3 -bar running, arg 4 1 42 -foo running, arg 1 -foo running, arg 2 -foo running, arg 3 -bar running, arg 4 1 42 -foo was cancelled -foo was cancelled -foo was cancelled -bar was cancelled -Done -''', 6) - cantest = CanTest() - loop = asyncio.get_event_loop() - loop.run_until_complete(cantest.start(loop)) - -# test 6: test NamedTask.is_running() -@asyn.cancellable -async def cant60(name): - print('Task cant60 name \"{}\" running.'.format(name)) - try: - for _ in range(5): - await asyncio.sleep(2) # 2 secs latency. - except asyn.StopTask: - print('Task cant60 name \"{}\" was cancelled.'.format(name)) - return - else: - print('Task cant60 name \"{}\" ended.'.format(name)) - -@asyn.cancellable -async def cant61(): - try: - while True: - for name in ('complete', 'cancel me'): - res = asyn.NamedTask.is_running(name) - print('Task \"{}\" running: {}'.format(name, res)) - await asyncio.sleep(1) - except asyn.StopTask: - print('Task cant61 cancelled.') - -async def run_cancel_test6(loop): - for name in ('complete', 'cancel me'): - loop.create_task(asyn.NamedTask(name, cant60, name)()) - loop.create_task(asyn.Cancellable(cant61)()) - await asyncio.sleep(4.5) - print('Cancelling task \"{}\". 1.5 secs latency.'.format(name)) - await asyn.NamedTask.cancel(name) - await asyncio.sleep(7) - name = 'cancel wait' - loop.create_task(asyn.NamedTask(name, cant60, name)()) - await asyncio.sleep(0.5) - print('Cancelling task \"{}\". 1.5 secs latency.'.format(name)) - t = time.ticks_ms() - await asyn.NamedTask.cancel('cancel wait', nowait=False) - print('Was cancelled in {} ms'.format(time.ticks_diff(time.ticks_ms(), t))) - print('Cancelling cant61') - await asyn.Cancellable.cancel_all() - print('Done') - - -def test6(): - printexp('''Task cant60 name "complete" running. -Task cant60 name "cancel me" running. -Task "complete" running: True -Task "cancel me" running: True -Task "complete" running: True -Task "cancel me" running: True -Task "complete" running: True -Task "cancel me" running: True -Task "complete" running: True -Task "cancel me" running: True -Task "complete" running: True -Task "cancel me" running: True -Cancelling task "cancel me". 1.5 secs latency. -Task "complete" running: True -Task "cancel me" running: True -Task cant60 name "cancel me" was cancelled. -Task "complete" running: True -Task "cancel me" running: False -Task "complete" running: True -Task "cancel me" running: False -Task "complete" running: True -Task "cancel me" running: False -Task "complete" running: True -Task "cancel me" running: False -Task cant60 name "complete" ended. -Task "complete" running: False -Task "cancel me" running: False -Task "complete" running: False -Task "cancel me" running: False -Task cant60 name "cancel wait" running. -Cancelling task "cancel wait". 1.5 secs latency. -Task "complete" running: False -Task "cancel me" running: False -Task "complete" running: False -Task "cancel me" running: False -Task cant60 name "cancel wait" was cancelled. -Was cancelled in 1503 ms -Cancelling cant61 -Task cant61 cancelled. -Done - - -[Duration of cancel wait may vary depending on platform 1500 <= range <= 1600ms] -''', 14) - loop = asyncio.get_event_loop() - loop.run_until_complete(run_cancel_test6(loop)) diff --git a/v2/chain.py b/v2/chain.py deleted file mode 100644 index 38e6f1a..0000000 --- a/v2/chain.py +++ /dev/null @@ -1,20 +0,0 @@ -# chain.py Demo of chained coros under MicroPython uasyncio -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license -try: - import uasyncio as asyncio -except ImportError: - import asyncio - -async def compute(x, y): - print("Compute %s + %s ..." % (x, y)) - await asyncio.sleep(1.0) - return x + y - -async def print_sum(x, y): - result = await compute(x, y) - print("%s + %s = %s" % (x, y, result)) - -loop = asyncio.get_event_loop() -loop.run_until_complete(print_sum(1, 2)) -loop.close() diff --git a/v2/check_async_code.py b/v2/check_async_code.py deleted file mode 100755 index f7907a7..0000000 --- a/v2/check_async_code.py +++ /dev/null @@ -1,206 +0,0 @@ -#! /usr/bin/python3 -# -*- coding: utf-8 -*- -# check_async_code.py -# A simple script to identify a common error which causes silent failures under -# MicroPython (issue #3241). -# This is where a task is declared with async def and then called as if it were -# a regular function. -# Copyright Peter Hinch 2017 -# Issued under the MIT licence - -import sys -import re - -tasks = set() -mismatch = False - -def pass1(part, lnum): - global mismatch - opart = part - sysnames = ('__aenter__', '__aexit__', '__aiter__', '__anext__') - # These are the commonest system functions declared with async def. - # Mimimise spurious duplicate function definition error messages. - good = True - if not part.startswith('#'): - mismatch = False - part = stripquotes(part, lnum) # Remove quoted strings (which might contain code) - good &= not mismatch - if part.startswith('async'): - pos = part.find('def') - if pos >= 0: - part = part[pos + 3:] - part = part.lstrip() - pos = part.find('(') - if pos >= 0: - fname = part[:pos].strip() - if fname in tasks and fname not in sysnames: - # Note this gives a false positive if a method of the same name - # exists in more than one class. - print('Duplicate function declaration "{}" in line {}'.format(fname, lnum)) - print(opart) - print() - good = False - else: - tasks.add(fname) - return good - -# Strip quoted strings (which may contain code) -def stripquotes(part, lnum=0): - global mismatch - for qchar in ('"', "'"): - pos = part.find(qchar) - if pos >= 0: - part = part[:pos] + part[pos + 1:] # strip 1st qchar - pos1 = part.find(qchar) - if pos > 0: - part = part[:pos] + part[pos1+1:] # Strip whole quoted string - part = stripquotes(part, lnum) - else: - print('Mismatched quotes in line', lnum) - mismatch = True - return part # for what it's worth - return part - -def pass2(part, lnum): - global mismatch - opart = part - good = True - if not part.startswith('#') and not part.startswith('async'): - mismatch = False - part = stripquotes(part, lnum) # Remove quoted strings (which might contain code) - good &= not mismatch - for task in tasks: - sstr = ''.join((task, r'\w*')) - match = re.search(sstr, part) - if match is None: # No match - continue - if match.group(0) != task: # No exact match - continue - # Accept await task, await task(args), a = await task(args) - sstr = ''.join((r'.*await[ \t]+', task)) - if re.search(sstr, part): - continue - # Accept await obj.task, await obj.task(args), a = await obj.task(args) - sstr = ''.join((r'.*await[ \t]+\w+\.', task)) - if re.search(sstr, part): - continue - # Accept assignments e.g. a = mytask or - # after = asyncio.after if p_version else asyncio.sleep - # or comparisons thistask == thattask - sstr = ''.join((r'=[ \t]*', task, r'[ \t]*[^(]')) - if re.search(sstr, part): - continue - # Not awaited but could be passed to function e.g. - # run_until_complete(mytask(args)) - sstr = ''.join((r'.*\w+[ \t]*\([ \t]*', task, r'[ \t]*\(')) - if re.search(sstr, part): - sstr = r'run_until_complete|run_forever|create_task|NamedTask' - if re.search(sstr, part): - continue - print('Please review line {}: async function "{}" is passed to a function.'.format(lnum, task)) - print(opart) - print() - good = False - continue - # func(mytask, more_args) may or may not be an error - sstr = ''.join((r'.*\w+[ \t]*\([ \t]*', task, r'[ \t]*[^\(]')) - if re.search(sstr, part): - print('Please review line {}: async function "{}" is passed to a function.'.format(lnum, task)) - print(opart) - print() - good = False - continue - - # Might be a method. Discard object. - sstr = ''.join((r'.*\w+[ \t]*\([ \t]*\w+\.', task)) - if re.search(sstr, part): - continue - print('Please review line {}: async function "{}" is not awaited.'.format(lnum, task)) - print(opart) - print() - good = False - return good - -txt = '''check_async_code.py -usage: check_async_code.py sourcefile.py - -This rather crude script is designed to locate a single type of coding error -which leads to silent runtime failure and hence can be hard to locate. - -It is intended to be used on otherwise correct source files and is not robust -in the face of syntax errors. Use pylint or other tools for general syntax -checking. - -It assumes code is written in the style advocated in the tutorial where coros -are declared with "async def". - -Under certain circumstances it can produce false positives. In some cases this -is by design. Given an asynchronous function foo the following is correct: -loop.run_until_complete(foo()) -The following line may or may not be an error depending on the design of bar() -bar(foo, args) -Likewise asynchronous functions can be put into objects such as dicts, lists or -sets. You may wish to review such lines to check that the intention was to put -the function rather than its result into the object. - -A false positive which is a consequence of the hacky nature of this script is -where a task has the same name as a synchronous bound method of some class. A -call to the bound method will produce an erroneous warning. This is because the -code does not parse class definitions. - -In practice the odd false positive is easily spotted in the code. -''' - -def usage(code=0): - print(txt) - sys.exit(code) - -# Process a line -in_triple_quote = False -def do_line(line, passn, lnum): - global in_triple_quote - ignore = False - good = True - # TODO The following isn't strictly correct. A line might be of the form - # erroneous Python ; ''' start of string - # It could therefore miss the error. - if re.search(r'[^"]*"""|[^\']*\'\'\'', line): - if in_triple_quote: - # Discard rest of line which terminates triple quote - ignore = True - in_triple_quote = not in_triple_quote - if not in_triple_quote and not ignore: - parts = line.split(';') - for part in parts: - # discard comments and whitespace at start and end - part = part.split('#')[0].strip() - if part: - good &= passn(part, lnum) - return good - -def main(fn): - global in_triple_quote - good = True - try: - with open(fn, 'r') as f: - for passn in (pass1, pass2): - in_triple_quote = False - lnum = 1 - for line in f: - good &= do_line(line, passn, lnum) - lnum += 1 - f.seek(0) - - except FileNotFoundError: - print('File {} does not exist.'.format(fn)) - return - if good: - print('No errors found!') - -if __name__ == "__main__": - if len(sys.argv) !=2: - usage(1) - arg = sys.argv[1].strip() - if arg == '--help' or arg == '-h': - usage() - main(arg) diff --git a/v2/client_server/heartbeat.py b/v2/client_server/heartbeat.py deleted file mode 100644 index 68a821e..0000000 --- a/v2/client_server/heartbeat.py +++ /dev/null @@ -1,26 +0,0 @@ -# flash.py Heartbeat code for simple uasyncio-based echo server - -# Released under the MIT licence -# Copyright (c) Peter Hinch 2019 - -import uasyncio as asyncio -from sys import platform - - -async def heartbeat(tms): - if platform == 'pyboard': # V1.x or D series - from pyb import LED - led = LED(1) - elif platform == 'esp8266': - from machine import Pin - led = Pin(2, Pin.OUT, value=1) - elif platform == 'linux': - return # No LED - else: - raise OSError('Unsupported platform.') - while True: - if platform == 'pyboard': - led.toggle() - elif platform == 'esp8266': - led(not led()) - await asyncio.sleep_ms(tms) diff --git a/v2/client_server/uclient.py b/v2/client_server/uclient.py deleted file mode 100644 index cc394cd..0000000 --- a/v2/client_server/uclient.py +++ /dev/null @@ -1,51 +0,0 @@ -# uclient.py Demo of simple uasyncio-based client for echo server - -# Released under the MIT licence -# Copyright (c) Peter Hinch 2019 - -import usocket as socket -import uasyncio as asyncio -import ujson -from heartbeat import heartbeat # Optional LED flash - -server = '192.168.0.32' -port = 8123 - -async def run(): - sock = socket.socket() - def close(): - sock.close() - print('Server disconnect.') - try: - serv = socket.getaddrinfo(server, port)[0][-1] - sock.connect(serv) - except OSError as e: - print('Cannot connect to {} on port {}'.format(server, port)) - sock.close() - return - while True: - sreader = asyncio.StreamReader(sock) - swriter = asyncio.StreamWriter(sock, {}) - data = ['value', 1] - while True: - try: - await swriter.awrite('{}\n'.format(ujson.dumps(data))) - res = await sreader.readline() - except OSError: - close() - return - try: - print('Received', ujson.loads(res)) - except ValueError: - close() - return - await asyncio.sleep(2) - data[1] += 1 - -loop = asyncio.get_event_loop() -# Optional fast heartbeat to confirm nonblocking operation -loop.create_task(heartbeat(100)) -try: - loop.run_until_complete(run()) -except KeyboardInterrupt: - print('Interrupted') # This mechanism doesn't work on Unix build. diff --git a/v2/client_server/userver.py b/v2/client_server/userver.py deleted file mode 100644 index ec7c07c..0000000 --- a/v2/client_server/userver.py +++ /dev/null @@ -1,64 +0,0 @@ -# userver.py Demo of simple uasyncio-based echo server - -# Released under the MIT licence -# Copyright (c) Peter Hinch 2019 - -import usocket as socket -import uasyncio as asyncio -import uselect as select -import ujson -from heartbeat import heartbeat # Optional LED flash - -class Server: - async def run(self, loop, port=8123): - addr = socket.getaddrinfo('0.0.0.0', port, 0, socket.SOCK_STREAM)[0][-1] - s_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # server socket - s_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s_sock.bind(addr) - s_sock.listen(5) - self.socks = [s_sock] # List of current sockets for .close() - print('Awaiting connection on port', port) - poller = select.poll() - poller.register(s_sock, select.POLLIN) - client_id = 1 # For user feedback - while True: - res = poller.poll(1) # 1ms block - if res: # Only s_sock is polled - c_sock, _ = s_sock.accept() # get client socket - loop.create_task(self.run_client(c_sock, client_id)) - client_id += 1 - await asyncio.sleep_ms(200) - - async def run_client(self, sock, cid): - self.socks.append(sock) - sreader = asyncio.StreamReader(sock) - swriter = asyncio.StreamWriter(sock, {}) - print('Got connection from client', cid) - try: - while True: - res = await sreader.readline() - if res == b'': - raise OSError - print('Received {} from client {}'.format(ujson.loads(res.rstrip()), cid)) - await swriter.awrite(res) # Echo back - except OSError: - pass - print('Client {} disconnect.'.format(cid)) - sock.close() - self.socks.remove(sock) - - def close(self): - print('Closing {} sockets.'.format(len(self.socks))) - for sock in self.socks: - sock.close() - -loop = asyncio.get_event_loop() -# Optional fast heartbeat to confirm nonblocking operation -loop.create_task(heartbeat(100)) -server = Server() -try: - loop.run_until_complete(server.run(loop)) -except KeyboardInterrupt: - print('Interrupted') # This mechanism doesn't work on Unix build. -finally: - server.close() diff --git a/v2/fast_io/__init__.py b/v2/fast_io/__init__.py deleted file mode 100644 index 5a2239c..0000000 --- a/v2/fast_io/__init__.py +++ /dev/null @@ -1,377 +0,0 @@ -# uasyncio.__init__ fast_io -# (c) 2014-2018 Paul Sokolovsky. MIT license. - -# This is a fork of official MicroPython uasynco. It is recommended to use -# the official version unless the specific features of this fork are required. - -# Changes copyright (c) Peter Hinch 2018 -# Code at https://github.com/peterhinch/micropython-async.git -# fork: peterhinch/micropython-lib branch: uasyncio-io-fast-and-rw - -import uerrno -import uselect as select -import usocket as _socket -import sys -from uasyncio.core import * - -DEBUG = 0 -log = None - -def set_debug(val): - global DEBUG, log - DEBUG = val - if val: - import logging - log = logging.getLogger("uasyncio") - - -class PollEventLoop(EventLoop): - - def __init__(self, runq_len=16, waitq_len=16, fast_io=0, lp_len=0): - EventLoop.__init__(self, runq_len, waitq_len, fast_io, lp_len) - self.poller = select.poll() - self.rdobjmap = {} - self.wrobjmap = {} - self.flags = {} - - # Remove registration of sock for reading or writing. - def _unregister(self, sock, objmap, flag): - # If StreamWriter.awrite() wrote entire buf on 1st pass sock will never - # have been registered. So test for presence in .flags. - if id(sock) in self.flags: - flags = self.flags[id(sock)] - if flags & flag: # flag is currently registered - flags &= ~flag # Clear current flag - if flags: # Another flag is present - self.flags[id(sock)] = flags - self.poller.register(sock, flags) - else: - del self.flags[id(sock)] # Clear all flags - self.poller.unregister(sock) - del objmap[id(sock)] # Remove coro from appropriate dict - - # Additively register sock for reading or writing - def _register(self, sock, flag): - if id(sock) in self.flags: - self.flags[id(sock)] |= flag - else: - self.flags[id(sock)] = flag - self.poller.register(sock, self.flags[id(sock)]) - - def add_reader(self, sock, cb, *args): - if DEBUG and __debug__: - log.debug("add_reader%s", (sock, cb, args)) - # HACK This should read - # self._register(sock, select.POLLIN) - # Temporary workround for https://github.com/micropython/micropython/issues/5172 - # The following is not compliant with POSIX or with the docs - self._register(sock, select.POLLIN | select.POLLHUP | select.POLLERR) # t35tB0t add HUP and ERR to force LWIP revents - if args: - self.rdobjmap[id(sock)] = (cb, args) - else: - self.rdobjmap[id(sock)] = cb - - def remove_reader(self, sock): - if DEBUG and __debug__: - log.debug("remove_reader(%s)", sock) - self._unregister(sock, self.rdobjmap, select.POLLIN) - - def add_writer(self, sock, cb, *args): - if DEBUG and __debug__: - log.debug("add_writer%s", (sock, cb, args)) - # HACK Should read - # self._register(sock, select.POLLOUT) - # Temporary workround for https://github.com/micropython/micropython/issues/5172 - # The following is not compliant with POSIX or with the docs - self._register(sock, select.POLLOUT | select.POLLHUP | select.POLLERR) # t35tB0t add HUP and ERR to force LWIP revents - if args: - self.wrobjmap[id(sock)] = (cb, args) - else: - self.wrobjmap[id(sock)] = cb - - def remove_writer(self, sock): - if DEBUG and __debug__: - log.debug("remove_writer(%s)", sock) - self._unregister(sock, self.wrobjmap, select.POLLOUT) - - def wait(self, delay): - if DEBUG and __debug__: - log.debug("poll.wait(%d)", delay) - # We need one-shot behavior (second arg of 1 to .poll()) - res = self.poller.ipoll(delay, 1) - #log.debug("poll result: %s", res) - for sock, ev in res: - if ev & select.POLLOUT: - cb = self.wrobjmap[id(sock)] - if cb is None: - continue # Not yet ready. - # Invalidate objmap: can get adverse timing in fast_io whereby add_writer - # is not called soon enough. Ignore poll events occurring before we are - # ready to handle them. - self.wrobjmap[id(sock)] = None - if ev & (select.POLLHUP | select.POLLERR): - # These events are returned even if not requested, and - # are sticky, i.e. will be returned again and again. - # If the caller doesn't do proper error handling and - # unregister this sock, we'll busy-loop on it, so we - # as well can unregister it now "just in case". - self.remove_writer(sock) - if DEBUG and __debug__: - log.debug("Calling IO callback: %r", cb) - if isinstance(cb, tuple): - cb[0](*cb[1]) - else: - prev = cb.pend_throw(None) # Enable task to run. - #if isinstance(prev, Exception): - #print('Put back exception') - #cb.pend_throw(prev) - self._call_io(cb) # Put coro onto runq (or ioq if one exists) - if ev & select.POLLIN: - cb = self.rdobjmap[id(sock)] - if cb is None: - continue - self.rdobjmap[id(sock)] = None - if ev & (select.POLLHUP | select.POLLERR): - # These events are returned even if not requested, and - # are sticky, i.e. will be returned again and again. - # If the caller doesn't do proper error handling and - # unregister this sock, we'll busy-loop on it, so we - # as well can unregister it now "just in case". - self.remove_reader(sock) - if DEBUG and __debug__: - log.debug("Calling IO callback: %r", cb) - if isinstance(cb, tuple): - cb[0](*cb[1]) - else: - prev = cb.pend_throw(None) # Enable task to run. - #if isinstance(prev, Exception): - #cb.pend_throw(prev) - #print('Put back exception') - self._call_io(cb) - - -class StreamReader: - - def __init__(self, polls, ios=None): - if ios is None: - ios = polls - self.polls = polls - self.ios = ios - - def read(self, n=-1): - while True: - yield IORead(self.polls) - res = self.ios.read(n) # Call the device's read method - if res is not None: - break - # This should not happen for real sockets, but can easily - # happen for stream wrappers (ssl, websockets, etc.) - #log.warn("Empty read") - yield IOReadDone(self.polls) # uasyncio.core calls remove_reader - # This de-registers device as a read device with poll via - # PollEventLoop._unregister - return res # Next iteration raises StopIteration and returns result - - def readinto(self, buf, n=0): # See comments in .read - while True: - yield IORead(self.polls) - if n: - res = self.ios.readinto(buf, n) # Call device's readinto method - else: - res = self.ios.readinto(buf) - if res is not None: - break - #log.warn("Empty read") - yield IOReadDone(self.polls) - return res - - def readexactly(self, n): - buf = b"" - while n: - yield IORead(self.polls) - # socket may become unreadable inbetween - # subsequent readline may return None - res = self.ios.read(n) - # returns none if socket not readable vs no data b'' - if res is None: - if DEBUG and __debug__: - log.debug('WARNING: socket write returned type(None)') - # socket may be in HUP or ERR state, so loop back ask poller - continue - else: - if not res: # All done - break - buf += res - n -= len(res) - yield IOReadDone(self.polls) - return buf - - def readline(self): - if DEBUG and __debug__: - log.debug("StreamReader.readline()") - buf = b"" - while True: - yield IORead(self.polls) - # socket may become unreadable inbetween - # subsequent readline may return None - res = self.ios.readline() - if res is None: - if DEBUG and __debug__: - log.debug('WARNING: socket read returned type(None)') - # socket may be in HUP or ERR state, so loop back and ask poller - continue - else: - if not res: - break - buf += res - if buf[-1] == 0x0a: - break - if DEBUG and __debug__: - log.debug("StreamReader.readline(): %s", buf) - yield IOReadDone(self.polls) - return buf - - def aclose(self): - yield IOReadDone(self.polls) - self.ios.close() - - def __repr__(self): - return "" % (self.polls, self.ios) - - -class StreamWriter: - - def __init__(self, s, extra): - self.s = s - self.extra = extra - - def awrite(self, buf, off=0, sz=-1): - # This method is called awrite (async write) to not proliferate - # incompatibility with original asyncio. Unlike original asyncio - # whose .write() method is both not a coroutine and guaranteed - # to return immediately (which means it has to buffer all the - # data), this method is a coroutine. - if sz == -1: - sz = len(buf) - off - if DEBUG and __debug__: - log.debug("StreamWriter.awrite(): spooling %d bytes", sz) - while True: - # Check socket write status first - yield IOWrite(self.s) - # socket may become unwritable inbetween - # subsequent writes may return None - res = self.s.write(buf, off, sz) - if res is None: - if DEBUG and __debug__: - log.debug('WARNING: socket write returned type(None)') - # socket may be in HUP or ERR state, so loop back and ask poller - continue - # If we spooled everything, return immediately - if res == sz: - if DEBUG and __debug__: - log.debug("StreamWriter.awrite(): completed spooling %d bytes", res) - break - if DEBUG and __debug__: - log.debug("StreamWriter.awrite(): spooled partial %d bytes", res) - assert res < sz - off += res - sz -= res - yield IOWrite(self.s) - if DEBUG and __debug__: - log.debug("StreamWriter.awrite(): can write more") - # remove_writer de-registers device as a writer - yield IOWriteDone(self.s) - - # Write piecewise content from iterable (usually, a generator) - def awriteiter(self, iterable): - for buf in iterable: - yield from self.awrite(buf) - - def aclose(self): - yield IOWriteDone(self.s) - self.s.close() - - def get_extra_info(self, name, default=None): - return self.extra.get(name, default) - - def __repr__(self): - return "" % self.s - - -def open_connection(host, port, ssl=False): - if DEBUG and __debug__: - log.debug("open_connection(%s, %s)", host, port) - ai = _socket.getaddrinfo(host, port, 0, _socket.SOCK_STREAM) - ai = ai[0] - s = _socket.socket(ai[0], ai[1], ai[2]) - s.setblocking(False) - try: - s.connect(ai[-1]) - except OSError as e: - if e.args[0] != uerrno.EINPROGRESS: - raise - if DEBUG and __debug__: - log.debug("open_connection: After connect") - yield IOWrite(s) -# if __debug__: -# assert s2.fileno() == s.fileno() - if DEBUG and __debug__: - log.debug("open_connection: After iowait: %s", s) - if ssl: - print("Warning: uasyncio SSL support is alpha") - import ussl - s.setblocking(True) - s2 = ussl.wrap_socket(s) - s.setblocking(False) - return StreamReader(s, s2), StreamWriter(s2, {}) - return StreamReader(s), StreamWriter(s, {}) - - -def start_server(client_coro, host, port, backlog=10): - if DEBUG and __debug__: - log.debug("start_server(%s, %s)", host, port) - ai = _socket.getaddrinfo(host, port, 0, _socket.SOCK_STREAM) - ai = ai[0] - s = _socket.socket(ai[0], ai[1], ai[2]) - s.setblocking(False) - - s.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1) - s.bind(ai[-1]) - s.listen(backlog) - try: - while True: - try: - if DEBUG and __debug__: - log.debug("start_server: Before accept") - yield IORead(s) - if DEBUG and __debug__: - log.debug("start_server: After iowait") - s2, client_addr = s.accept() - s2.setblocking(False) - if DEBUG and __debug__: - log.debug("start_server: After accept: %s", s2) - extra = {"peername": client_addr} - # Detach the client_coro: put it on runq - yield client_coro(StreamReader(s2), StreamWriter(s2, extra)) - s2 = None - - except Exception as e: - if len(e.args)==0: - # This happens but shouldn't. Firmware bug? - # Handle exception as an unexpected unknown error: - # collect details here then close try to continue running - print('start_server:Unknown error: continuing') - sys.print_exception(e) - if not uerrno.errorcode.get(e.args[0], False): - # Handle exception as internal error: close and terminate - # handler (user must trap or crash) - print('start_server:Unexpected error: terminating') - raise - finally: - if s2: - s2.close() - s.close() - - -import uasyncio.core -uasyncio.core._event_loop_class = PollEventLoop diff --git a/v2/fast_io/core.py b/v2/fast_io/core.py deleted file mode 100644 index 7eadcfc..0000000 --- a/v2/fast_io/core.py +++ /dev/null @@ -1,462 +0,0 @@ -# uasyncio.core fast_io -# (c) 2014-2018 Paul Sokolovsky. MIT license. - -# This is a fork of official MicroPython uasynco. It is recommended to use -# the official version unless the specific features of this fork are required. - -# Changes copyright (c) Peter Hinch 2018, 2019 -# Code at https://github.com/peterhinch/micropython-async.git -# fork: peterhinch/micropython-lib branch: uasyncio-io-fast-and-rw - -version = ('fast_io', '0.26') -try: - import rtc_time as time # Low power timebase using RTC -except ImportError: - import utime as time -import utimeq -import ucollections - - -type_gen = type((lambda: (yield))()) -type_genf = type((lambda: (yield))) # Type of a generator function upy iss #3241 - -DEBUG = 0 -log = None - -def set_debug(val): - global DEBUG, log - DEBUG = val - if val: - import logging - log = logging.getLogger("uasyncio.core") - - -class CancelledError(Exception): - pass - - -class TimeoutError(CancelledError): - pass - - -class EventLoop: - - def __init__(self, runq_len=16, waitq_len=16, ioq_len=0, lp_len=0): - self.runq = ucollections.deque((), runq_len, True) - self._max_od = 0 - self.lpq = utimeq.utimeq(lp_len) if lp_len else None - self.ioq_len = ioq_len - self.canned = set() - if ioq_len: - self.ioq = ucollections.deque((), ioq_len, True) - self._call_io = self._call_now - else: - self._call_io = self.call_soon - self.waitq = utimeq.utimeq(waitq_len) - # Current task being run. Task is a top-level coroutine scheduled - # in the event loop (sub-coroutines executed transparently by - # yield from/await, event loop "doesn't see" them). - self.cur_task = None - - def time(self): - return time.ticks_ms() - - def create_task(self, coro): - # CPython 3.4.2 - assert not isinstance(coro, type_genf), 'Coroutine arg expected.' # upy issue #3241 - # create_task with a callable would work, so above assert only traps the easily-made error - self.call_later_ms(0, coro) - # CPython asyncio incompatibility: we don't return Task object - - def _call_now(self, callback, *args): # For stream I/O only - if __debug__ and DEBUG: - log.debug("Scheduling in ioq: %s", (callback, args)) - self.ioq.append(callback) - if not isinstance(callback, type_gen): - self.ioq.append(args) - - def max_overdue_ms(self, t=None): - if t is not None: - self._max_od = int(t) - return self._max_od - - # Low priority versions of call_later() call_later_ms() and call_at_() - def call_after_ms(self, delay, callback, *args): - self.call_at_lp_(time.ticks_add(self.time(), delay), callback, *args) - - def call_after(self, delay, callback, *args): - self.call_at_lp_(time.ticks_add(self.time(), int(delay * 1000)), callback, *args) - - def call_at_lp_(self, time, callback, *args): - if self.lpq is not None: - self.lpq.push(time, callback, args) - if isinstance(callback, type_gen): - callback.pend_throw(id(callback)) - else: - raise OSError('No low priority queue exists.') - - def call_soon(self, callback, *args): - if __debug__ and DEBUG: - log.debug("Scheduling in runq: %s", (callback, args)) - self.runq.append(callback) - if not isinstance(callback, type_gen): - self.runq.append(args) - - def call_later(self, delay, callback, *args): - self.call_at_(time.ticks_add(self.time(), int(delay * 1000)), callback, args) - - def call_later_ms(self, delay, callback, *args): - if not delay: - return self.call_soon(callback, *args) - self.call_at_(time.ticks_add(self.time(), delay), callback, args) - - def call_at_(self, time, callback, args=()): - if __debug__ and DEBUG: - log.debug("Scheduling in waitq: %s", (time, callback, args)) - self.waitq.push(time, callback, args) - if isinstance(callback, type_gen): - callback.pend_throw(id(callback)) - - def wait(self, delay): - # Default wait implementation, to be overriden in subclasses - # with IO scheduling - if __debug__ and DEBUG: - log.debug("Sleeping for: %s", delay) - time.sleep_ms(delay) - - def run_forever(self): - cur_task = [0, 0, 0] - # Put a task on the runq unless it was cancelled - def runq_add(): - if isinstance(cur_task[1], type_gen): - tid = id(cur_task[1]) - if tid in self.canned: - self.canned.remove(tid) - else: - cur_task[1].pend_throw(None) - self.call_soon(cur_task[1], *cur_task[2]) - else: - self.call_soon(cur_task[1], *cur_task[2]) - - while True: - # Expire entries in waitq and move them to runq - tnow = self.time() - if self.lpq: - # Schedule a LP task if overdue or if no normal task is ready - to_run = False # Assume no LP task is to run - t = self.lpq.peektime() - tim = time.ticks_diff(t, tnow) - to_run = self._max_od > 0 and tim < -self._max_od - if not (to_run or self.runq): # No overdue LP task or task on runq - # zero delay tasks go straight to runq. So don't schedule LP if runq - to_run = tim <= 0 # True if LP task is due - if to_run and self.waitq: # Set False if normal tasks due. - t = self.waitq.peektime() - to_run = time.ticks_diff(t, tnow) > 0 # No normal task is ready - if to_run: - self.lpq.pop(cur_task) - runq_add() - - while self.waitq: - t = self.waitq.peektime() - delay = time.ticks_diff(t, tnow) - if delay > 0: - break - self.waitq.pop(cur_task) - if __debug__ and DEBUG: - log.debug("Moving from waitq to runq: %s", cur_task[1]) - runq_add() - - # Process runq. This can append tasks to the end of .runq so get initial - # length so we only process those items on the queue at the start. - l = len(self.runq) - if __debug__ and DEBUG: - log.debug("Entries in runq: %d", l) - cur_q = self.runq # Default: always get tasks from runq - dl = 1 # Subtract this from entry count l - while l or self.ioq_len: - if self.ioq_len: # Using fast_io - self.wait(0) # Schedule I/O. Can append to ioq. - if self.ioq: - cur_q = self.ioq - dl = 0 # No effect on l - elif l == 0: - break # Both queues are empty - else: - cur_q = self.runq - dl = 1 - l -= dl - cb = cur_q.popleft() # Remove most current task - args = () - if not isinstance(cb, type_gen): # It's a callback not a generator so get args - args = cur_q.popleft() - l -= dl - if __debug__ and DEBUG: - log.info("Next callback to run: %s", (cb, args)) - cb(*args) # Call it - continue # Proceed to next runq entry - - if __debug__ and DEBUG: - log.info("Next coroutine to run: %s", (cb, args)) - self.cur_task = cb # Stored in a bound variable for TimeoutObj - delay = 0 - low_priority = False # Assume normal priority - try: - if args is (): - ret = next(cb) # Schedule the coro, get result - else: - ret = cb.send(*args) - if __debug__ and DEBUG: - log.info("Coroutine %s yield result: %s", cb, ret) - if isinstance(ret, SysCall1): # Coro returned a SysCall1: an object with an arg spcified in its constructor - arg = ret.arg - if isinstance(ret, SleepMs): - delay = arg - if isinstance(ret, AfterMs): - low_priority = True - if isinstance(ret, After): - delay = int(delay*1000) - elif isinstance(ret, IORead): # coro was a StreamReader read method - cb.pend_throw(False) # Marks the task as waiting on I/O for cancellation/timeout - # If task is cancelled or times out, it is put on runq to process exception. - # Debug note: if task is scheduled other than by wait (which does pend_throw(None) - # an exception (exception doesn't inherit from Exception) is thrown - self.add_reader(arg, cb) # Set up select.poll for read and store the coro in object map - continue # Don't reschedule. Coro is scheduled by wait() when poll indicates h/w ready - elif isinstance(ret, IOWrite): # coro was StreamWriter.awrite. Above comments apply. - cb.pend_throw(False) - self.add_writer(arg, cb) - continue - elif isinstance(ret, IOReadDone): # update select.poll registration and if necessary remove coro from map - self.remove_reader(arg) - self._call_io(cb, args) # Next call produces StopIteration enabling result to be returned - continue - elif isinstance(ret, IOWriteDone): - self.remove_writer(arg) - self._call_io(cb, args) # Next call produces StopIteration: see StreamWriter.aclose - continue - elif isinstance(ret, StopLoop): # e.g. from run_until_complete. run_forever() terminates - return arg - else: - assert False, "Unknown syscall yielded: %r (of type %r)" % (ret, type(ret)) - elif isinstance(ret, type_gen): # coro has yielded a coro (or generator) - self.call_soon(ret) # append to .runq - elif isinstance(ret, int): # If coro issued yield N, delay = N ms - delay = ret - elif ret is None: - # coro issued yield. delay == 0 so code below will put the current task back on runq - pass - elif ret is False: - # yield False causes coro not to be rescheduled i.e. it stops. - continue - else: - assert False, "Unsupported coroutine yield value: %r (of type %r)" % (ret, type(ret)) - except StopIteration as e: - if __debug__ and DEBUG: - log.debug("Coroutine finished: %s", cb) - continue - except CancelledError as e: - if __debug__ and DEBUG: - log.debug("Coroutine cancelled: %s", cb) - continue - # Currently all syscalls don't return anything, so we don't - # need to feed anything to the next invocation of coroutine. - # If that changes, need to pass that value below. - if low_priority: - self.call_after_ms(delay, cb) # Put on lpq - elif delay: - self.call_later_ms(delay, cb) - else: - self.call_soon(cb) - - # Wait until next waitq task or I/O availability - delay = 0 - if not self.runq: - delay = -1 - if self.waitq: - tnow = self.time() - t = self.waitq.peektime() - delay = time.ticks_diff(t, tnow) - if delay < 0: - delay = 0 - if self.lpq: - t = self.lpq.peektime() - lpdelay = time.ticks_diff(t, tnow) - if lpdelay < 0: - lpdelay = 0 - if lpdelay < delay or delay < 0: - delay = lpdelay # waitq is empty or lp task is more current - self.wait(delay) - - def run_until_complete(self, coro): - assert not isinstance(coro, type_genf), 'Coroutine arg expected.' # upy issue #3241 - def _run_and_stop(): - ret = yield from coro # https://github.com/micropython/micropython-lib/pull/270 - yield StopLoop(ret) - self.call_soon(_run_and_stop()) - return self.run_forever() - - def stop(self): - self.call_soon((lambda: (yield StopLoop(0)))()) - - def close(self): - pass - - -class SysCall: - - def __init__(self, *args): - self.args = args - - def handle(self): - raise NotImplementedError - -# Optimized syscall with 1 arg -class SysCall1(SysCall): - - def __init__(self, arg): - self.arg = arg - -class StopLoop(SysCall1): - pass - -class IORead(SysCall1): - pass - -class IOWrite(SysCall1): - pass - -class IOReadDone(SysCall1): - pass - -class IOWriteDone(SysCall1): - pass - - -_event_loop = None -_event_loop_class = EventLoop -def get_event_loop(runq_len=16, waitq_len=16, ioq_len=0, lp_len=0): - global _event_loop - if _event_loop is None: - _event_loop = _event_loop_class(runq_len, waitq_len, ioq_len, lp_len) - return _event_loop - -# Allow user classes to determine prior event loop instantiation. -def get_running_loop(): - if _event_loop is None: - raise RuntimeError('Event loop not instantiated') - return _event_loop - -def got_event_loop(): # Kept to avoid breaking code - return _event_loop is not None - -def sleep(secs): - yield int(secs * 1000) - -# Implementation of sleep_ms awaitable with zero heap memory usage -class SleepMs(SysCall1): - - def __init__(self): - self.v = None - self.arg = None - - def __call__(self, arg): - self.v = arg - #print("__call__") - return self - - def __iter__(self): - #print("__iter__") - return self - - def __next__(self): - if self.v is not None: - #print("__next__ syscall enter") - self.arg = self.v - self.v = None - return self - #print("__next__ syscall exit") - _stop_iter.__traceback__ = None - raise _stop_iter - -_stop_iter = StopIteration() -sleep_ms = SleepMs() - - -def cancel(coro): - prev = coro.pend_throw(CancelledError()) - if prev is False: # Waiting on I/O. Not on q so put it there. - _event_loop._call_io(coro) - elif isinstance(prev, int): # On waitq or lpq - # task id - _event_loop.canned.add(prev) # Alas this allocates - _event_loop._call_io(coro) # Put on runq/ioq - else: - assert prev is None - - -class TimeoutObj: - def __init__(self, coro): - self.coro = coro - - -def wait_for_ms(coro, timeout): - - def waiter(coro, timeout_obj): - res = yield from coro - if __debug__ and DEBUG: - log.debug("waiter: cancelling %s", timeout_obj) - timeout_obj.coro = None - return res - - def timeout_func(timeout_obj): - if timeout_obj.coro: - if __debug__ and DEBUG: - log.debug("timeout_func: cancelling %s", timeout_obj.coro) - prev = timeout_obj.coro.pend_throw(TimeoutError()) - if prev is False: # Waiting on I/O - _event_loop._call_io(timeout_obj.coro) - elif isinstance(prev, int): # On waitq or lpq - # prev==task id - _event_loop.canned.add(prev) # Alas this allocates - _event_loop._call_io(timeout_obj.coro) # Put on runq/ioq - else: - assert prev is None - - timeout_obj = TimeoutObj(_event_loop.cur_task) - _event_loop.call_later_ms(timeout, timeout_func, timeout_obj) - return (yield from waiter(coro, timeout_obj)) - - -def wait_for(coro, timeout): - return wait_for_ms(coro, int(timeout * 1000)) - - -def coroutine(f): - return f - -# Low priority -class AfterMs(SleepMs): - pass - -class After(AfterMs): - pass - -after_ms = AfterMs() -after = After() - -# -# The functions below are deprecated in uasyncio, and provided only -# for compatibility with CPython asyncio -# - -def ensure_future(coro, loop=_event_loop): - _event_loop.call_soon(coro) - # CPython asyncio incompatibility: we don't return Task object - return coro - - -# CPython asyncio incompatibility: Task is a function, not a class (for efficiency) -def Task(coro, loop=_event_loop): - # Same as async() - _event_loop.call_soon(coro) diff --git a/v2/fast_io/fast_can_test.py b/v2/fast_io/fast_can_test.py deleted file mode 100644 index 1600080..0000000 --- a/v2/fast_io/fast_can_test.py +++ /dev/null @@ -1,67 +0,0 @@ -# fast_can_test.py Test of cancellation of tasks which call sleep - -# Copyright (c) Peter Hinch 2019 -# Released under the MIT licence - -import uasyncio as asyncio -import sys -ermsg = 'This test requires the fast_io version of uasyncio V2.4 or later.' -try: - print('Uasyncio version', asyncio.version) - if not isinstance(asyncio.version, tuple): - print(ermsg) - sys.exit(0) -except AttributeError: - print(ermsg) - sys.exit(0) - -# If a task times out the TimeoutError can't be trapped: -# no exception is thrown to the task - -async def foo(t): - try: - print('foo started') - await asyncio.sleep(t) - print('foo ended', t) - except asyncio.CancelledError: - print('foo cancelled', t) - -async def lpfoo(t): - try: - print('lpfoo started') - await asyncio.after(t) - print('lpfoo ended', t) - except asyncio.CancelledError: - print('lpfoo cancelled', t) - -async def run(coro, t): - await asyncio.wait_for(coro, t) - -async def bar(loop): - foo1 = foo(1) - foo5 = foo(5) - lpfoo1 = lpfoo(1) - lpfoo5 = lpfoo(5) - loop.create_task(foo1) - loop.create_task(foo5) - loop.create_task(lpfoo1) - loop.create_task(lpfoo5) - await asyncio.sleep(2) - print('Cancelling tasks') - asyncio.cancel(foo1) - asyncio.cancel(foo5) - asyncio.cancel(lpfoo1) - asyncio.cancel(lpfoo5) - await asyncio.sleep(0) # Allow cancellation to occur - print('Pausing 7s to ensure no task still running.') - await asyncio.sleep(7) - print('Launching tasks with 2s timeout') - loop.create_task(run(foo(1), 2)) - loop.create_task(run(lpfoo(1), 2)) - loop.create_task(run(foo(20), 2)) - loop.create_task(run(lpfoo(20), 2)) - print('Pausing 7s to ensure no task still running.') - await asyncio.sleep(7) - -loop = asyncio.get_event_loop(ioq_len=16, lp_len=16) -loop.run_until_complete(bar(loop)) diff --git a/v2/fast_io/iorw_can.py b/v2/fast_io/iorw_can.py deleted file mode 100644 index 8ef7929..0000000 --- a/v2/fast_io/iorw_can.py +++ /dev/null @@ -1,140 +0,0 @@ -# iorw_can.py Emulate a device which can read and write one character at a time -# and test cancellation. - -# Copyright (c) Peter Hinch 2019 -# Released under the MIT licence - -# This requires the modified version of uasyncio (fast_io directory). -# Slow hardware is emulated using timers. -# MyIO.write() ouputs a single character and sets the hardware not ready. -# MyIO.readline() returns a single character and sets the hardware not ready. -# Timers asynchronously set the hardware ready. - -import io, pyb -import uasyncio as asyncio -import micropython -import sys -try: - print('Uasyncio version', asyncio.version) - if not isinstance(asyncio.version, tuple): - print('Please use fast_io version 0.24 or later.') - sys.exit(0) -except AttributeError: - print('ERROR: This test requires the fast_io version. It will not run correctly') - print('under official uasyncio V2.0 owing to a bug which prevents concurrent') - print('input and output.') - sys.exit(0) - -print('Issue iorw_can.test(True) to test ioq, iorw_can.test() to test runq.') -print('Tasks time out after 15s.') -print('Issue ctrl-d after each run.') - -micropython.alloc_emergency_exception_buf(100) - -MP_STREAM_POLL_RD = const(1) -MP_STREAM_POLL_WR = const(4) -MP_STREAM_POLL = const(3) -MP_STREAM_ERROR = const(-1) - -def printbuf(this_io): - print(bytes(this_io.wbuf[:this_io.wprint_len]).decode(), end='') - -class MyIO(io.IOBase): - def __init__(self, read=False, write=False): - self.ready_rd = False # Read and write not ready - self.rbuf = b'ready\n' # Read buffer - self.ridx = 0 - pyb.Timer(4, freq = 5, callback = self.do_input) - self.wch = b'' - self.wbuf = bytearray(100) # Write buffer - self.wprint_len = 0 - self.widx = 0 - pyb.Timer(5, freq = 10, callback = self.do_output) - - # Read callback: emulate asynchronous input from hardware. - # Typically would put bytes into a ring buffer and set .ready_rd. - def do_input(self, t): - self.ready_rd = True # Data is ready to read - - # Write timer callback. Emulate hardware: if there's data in the buffer - # write some or all of it - def do_output(self, t): - if self.wch: - self.wbuf[self.widx] = self.wch - self.widx += 1 - if self.wch == ord('\n'): - self.wprint_len = self.widx # Save for schedule - micropython.schedule(printbuf, self) - self.widx = 0 - self.wch = b'' - - - def ioctl(self, req, arg): # see ports/stm32/uart.c - ret = MP_STREAM_ERROR - if req == MP_STREAM_POLL: - ret = 0 - if arg & MP_STREAM_POLL_RD: - if self.ready_rd: - ret |= MP_STREAM_POLL_RD - if arg & MP_STREAM_POLL_WR: - if not self.wch: - ret |= MP_STREAM_POLL_WR # Ready if no char pending - return ret - - # Test of device that produces one character at a time - def readline(self): - self.ready_rd = False # Cleared by timer cb do_input - ch = self.rbuf[self.ridx] - if ch == ord('\n'): - self.ridx = 0 - else: - self.ridx += 1 - return chr(ch) - - # Emulate unbuffered hardware which writes one character: uasyncio waits - # until hardware is ready for the next. Hardware ready is emulated by write - # timer callback. - def write(self, buf, off, sz): - self.wch = buf[off] # Hardware starts to write a char - return 1 # 1 byte written. uasyncio waits on ioctl write ready - -# Note that trapping the exception and returning is still mandatory. -async def receiver(myior): - sreader = asyncio.StreamReader(myior) - try: - while True: - res = await sreader.readline() - print('Received', res) - except asyncio.CancelledError: - print('Receiver cancelled') - -async def sender(myiow): - swriter = asyncio.StreamWriter(myiow, {}) - await asyncio.sleep(1) - count = 0 - try: # Trap in outermost scope to catch cancellation of .sleep - while True: - count += 1 - tosend = 'Wrote Hello MyIO {}\n'.format(count) - await swriter.awrite(tosend.encode('UTF8')) - await asyncio.sleep(2) - except asyncio.CancelledError: - print('Sender cancelled') - -async def cannem(coros, t): - await asyncio.sleep(t) - for coro in coros: - asyncio.cancel(coro) - await asyncio.sleep(1) - -def test(ioq=False): - myio = MyIO() - if ioq: - loop = asyncio.get_event_loop(ioq_len=16) - else: - loop = asyncio.get_event_loop() - rx = receiver(myio) - tx = sender(myio) - loop.create_task(rx) - loop.create_task(tx) - loop.run_until_complete(cannem((rx, tx), 15)) diff --git a/v2/fast_io/iorw_to.py b/v2/fast_io/iorw_to.py deleted file mode 100644 index 79e05fd..0000000 --- a/v2/fast_io/iorw_to.py +++ /dev/null @@ -1,143 +0,0 @@ -# iorw_to.py Emulate a device which can read and write one character at a time -# and test timeouts. - -# Copyright (c) Peter Hinch 2019 -# Released under the MIT licence - -# This requires the modified version of uasyncio (fast_io directory). -# Slow hardware is emulated using timers. -# MyIO.write() ouputs a single character and sets the hardware not ready. -# MyIO.readline() returns a single character and sets the hardware not ready. -# Timers asynchronously set the hardware ready. - -import io, pyb -import uasyncio as asyncio -import micropython -import sys -try: - print('Uasyncio version', asyncio.version) - if not isinstance(asyncio.version, tuple): - print('Please use fast_io version 0.24 or later.') - sys.exit(0) -except AttributeError: - print('ERROR: This test requires the fast_io version. It will not run correctly') - print('under official uasyncio V2.0 owing to a bug which prevents concurrent') - print('input and output.') - sys.exit(0) - -print('Issue iorw_to.test(True) to test ioq, iorw_to.test() to test runq.') -print('Test runs until interrupted. Tasks time out after 15s.') -print('Issue ctrl-d after each run.') - -micropython.alloc_emergency_exception_buf(100) - -MP_STREAM_POLL_RD = const(1) -MP_STREAM_POLL_WR = const(4) -MP_STREAM_POLL = const(3) -MP_STREAM_ERROR = const(-1) - -def printbuf(this_io): - print(bytes(this_io.wbuf[:this_io.wprint_len]).decode(), end='') - -class MyIO(io.IOBase): - def __init__(self, read=False, write=False): - self.ready_rd = False # Read and write not ready - self.rbuf = b'ready\n' # Read buffer - self.ridx = 0 - pyb.Timer(4, freq = 5, callback = self.do_input) - self.wch = b'' - self.wbuf = bytearray(100) # Write buffer - self.wprint_len = 0 - self.widx = 0 - pyb.Timer(5, freq = 10, callback = self.do_output) - - # Read callback: emulate asynchronous input from hardware. - # Typically would put bytes into a ring buffer and set .ready_rd. - def do_input(self, t): - self.ready_rd = True # Data is ready to read - - # Write timer callback. Emulate hardware: if there's data in the buffer - # write some or all of it - def do_output(self, t): - if self.wch: - self.wbuf[self.widx] = self.wch - self.widx += 1 - if self.wch == ord('\n'): - self.wprint_len = self.widx # Save for schedule - micropython.schedule(printbuf, self) - self.widx = 0 - self.wch = b'' - - - def ioctl(self, req, arg): # see ports/stm32/uart.c - ret = MP_STREAM_ERROR - if req == MP_STREAM_POLL: - ret = 0 - if arg & MP_STREAM_POLL_RD: - if self.ready_rd: - ret |= MP_STREAM_POLL_RD - if arg & MP_STREAM_POLL_WR: - if not self.wch: - ret |= MP_STREAM_POLL_WR # Ready if no char pending - return ret - - # Test of device that produces one character at a time - def readline(self): - self.ready_rd = False # Cleared by timer cb do_input - ch = self.rbuf[self.ridx] - if ch == ord('\n'): - self.ridx = 0 - else: - self.ridx += 1 - return chr(ch) - - # Emulate unbuffered hardware which writes one character: uasyncio waits - # until hardware is ready for the next. Hardware ready is emulated by write - # timer callback. - def write(self, buf, off, sz): - self.wch = buf[off] # Hardware starts to write a char - return 1 # 1 byte written. uasyncio waits on ioctl write ready - -# Note that trapping the exception and returning is still mandatory. -async def receiver(myior): - sreader = asyncio.StreamReader(myior) - try: - while True: - res = await sreader.readline() - print('Received', res) - except asyncio.TimeoutError: - print('Receiver timeout') - -async def sender(myiow): - swriter = asyncio.StreamWriter(myiow, {}) - await asyncio.sleep(1) - count = 0 - try: # Trap in outermost scope to catch cancellation of .sleep - while True: - count += 1 - tosend = 'Wrote Hello MyIO {}\n'.format(count) - await swriter.awrite(tosend.encode('UTF8')) - await asyncio.sleep(2) - except asyncio.TimeoutError: - print('Sender timeout') - -async def run(coro, t): - await asyncio.wait_for_ms(coro, t) - -async def do_test(loop, t): - myio = MyIO() - while True: - tr = t * 1000 + (pyb.rng() >> 20) # Add ~1s uncertainty - tw = t * 1000 + (pyb.rng() >> 20) - print('Timeouts: {:7.3f}s read {:7.3f}s write'.format(tr/1000, tw/1000)) - loop.create_task(run(receiver(myio), tr)) - await run(sender(myio), tw) - await asyncio.sleep(2) # Wait out timing randomness - -def test(ioq=False): - if ioq: - loop = asyncio.get_event_loop(ioq_len=16) - else: - loop = asyncio.get_event_loop() - loop.create_task(do_test(loop, 15)) - loop.run_forever() diff --git a/v2/fast_io/ms_timer.py b/v2/fast_io/ms_timer.py deleted file mode 100644 index f539289..0000000 --- a/v2/fast_io/ms_timer.py +++ /dev/null @@ -1,33 +0,0 @@ -# ms_timer.py A relatively high precision delay class for the fast_io version -# of uasyncio - -import uasyncio as asyncio -import utime -import io -MP_STREAM_POLL_RD = const(1) -MP_STREAM_POLL = const(3) -MP_STREAM_ERROR = const(-1) - -class MillisecTimer(io.IOBase): - def __init__(self): - self.end = 0 - self.sreader = asyncio.StreamReader(self) - - def __iter__(self): - await self.sreader.readline() - - def __call__(self, ms): - self.end = utime.ticks_add(utime.ticks_ms(), ms) - return self - - def readline(self): - return b'\n' - - def ioctl(self, req, arg): - ret = MP_STREAM_ERROR - if req == MP_STREAM_POLL: - ret = 0 - if arg & MP_STREAM_POLL_RD: - if utime.ticks_diff(utime.ticks_ms(), self.end) >= 0: - ret |= MP_STREAM_POLL_RD - return ret diff --git a/v2/fast_io/ms_timer_test.py b/v2/fast_io/ms_timer_test.py deleted file mode 100644 index 5870317..0000000 --- a/v2/fast_io/ms_timer_test.py +++ /dev/null @@ -1,45 +0,0 @@ -# ms_timer_test.py Test/demo program for MillisecTimer - -import uasyncio as asyncio -import utime -import ms_timer - -async def timer_test(n): - timer = ms_timer.MillisecTimer() - while True: - t = utime.ticks_ms() - await timer(30) - print(n, utime.ticks_diff(utime.ticks_ms(), t)) - await asyncio.sleep(0.5 + n/5) - -async def foo(): - while True: - await asyncio.sleep(0) - utime.sleep_ms(10) # Emulate slow processing - -async def killer(): - await asyncio.sleep(10) - -def test(fast_io=True): - loop = asyncio.get_event_loop(ioq_len=6 if fast_io else 0) - for _ in range(10): - loop.create_task(foo()) - for n in range(3): - loop.create_task(timer_test(n)) - loop.run_until_complete(killer()) - -s = '''This test creates ten tasks each of which blocks for 10ms. -It also creates three tasks each of which runs a MillisecTimer for 30ms, -timing the period which elapses while it runs. Under the fast_io version -the elapsed time is ~30ms as expected. Under the normal version it is -about 300ms because of competetion from the blocking coros. - -This competetion is worse than might be expected because of inefficiency -in the way the official version handles I/O. - -Run test() to test fast I/O, test(False) to test normal I/O. - -Test prints the task number followed by the actual elapsed time in ms. -Test runs for 10s.''' - -print(s) diff --git a/v2/fast_io/pin_cb.py b/v2/fast_io/pin_cb.py deleted file mode 100644 index 91e69e2..0000000 --- a/v2/fast_io/pin_cb.py +++ /dev/null @@ -1,47 +0,0 @@ -# pin_cb.py Demo of device driver using fast I/O to schedule a callback -# PinCall class allows a callback to be associated with a change in pin state. - -# This class is not suitable for switch I/O because of contact bounce: -# see Switch and Pushbutton classes in aswitch.py - -import uasyncio as asyncio -import io -MP_STREAM_POLL_RD = const(1) -MP_STREAM_POLL = const(3) -MP_STREAM_ERROR = const(-1) - -class PinCall(io.IOBase): - def __init__(self, pin, *, cb_rise=None, cbr_args=(), cb_fall=None, cbf_args=()): - self.pin = pin - self.cb_rise = cb_rise - self.cbr_args = cbr_args - self.cb_fall = cb_fall - self.cbf_args = cbf_args - self.pinval = pin.value() - self.sreader = asyncio.StreamReader(self) - loop = asyncio.get_event_loop() - loop.create_task(self.run()) - - async def run(self): - while True: - await self.sreader.read(1) - - def read(self, _): - v = self.pinval - if v and self.cb_rise is not None: - self.cb_rise(*self.cbr_args) - return b'\n' - if not v and self.cb_fall is not None: - self.cb_fall(*self.cbf_args) - return b'\n' - - def ioctl(self, req, arg): - ret = MP_STREAM_ERROR - if req == MP_STREAM_POLL: - ret = 0 - if arg & MP_STREAM_POLL_RD: - v = self.pin.value() - if v != self.pinval: - self.pinval = v - ret = MP_STREAM_POLL_RD - return ret diff --git a/v2/fast_io/pin_cb_test.py b/v2/fast_io/pin_cb_test.py deleted file mode 100644 index 60dab70..0000000 --- a/v2/fast_io/pin_cb_test.py +++ /dev/null @@ -1,68 +0,0 @@ -# ********* TEST ********** - -# With fast_io false latency is up to 50.96ms -# With fast_io True we see ~450μs to 5.208ms. - -import utime -import pyb -import uasyncio as asyncio -from pin_cb import PinCall - -t = 0 # Time of last output transition -max_latency = 0 -pinout = pyb.Pin(pyb.Pin.board.X1, pyb.Pin.OUT) - -# Timer callback: generate asynchronous pin state changes -def toggle(_): - global t - pinout.value(not pinout.value()) - t = utime.ticks_us() - -# Callback for basic test -def cb(pin, ud): - print('Callback', pin.value(), ud) - -# Callback for latency test -def cbl(pinin): - global max_latency - dt = utime.ticks_diff(utime.ticks_us(), t) - max_latency = max(max_latency, dt) - print('Latency {:6d}μs {:6d}μs max'.format(dt, max_latency)) - -async def dummy(): - while True: - await asyncio.sleep(0) - utime.sleep_ms(5) # Emulate slow processing - -async def killer(): - await asyncio.sleep(20) - -def test(fast_io=True, latency=False): - loop = asyncio.get_event_loop(ioq_len=6 if fast_io else 0) - pinin = pyb.Pin(pyb.Pin.board.X2, pyb.Pin.IN) - pyb.Timer(4, freq = 2.1, callback = toggle) - for _ in range(5): - loop.create_task(dummy()) - if latency: - pin_cb = PinCall(pinin, cb_rise = cbl, cbr_args = (pinin,)) - else: - pincall = PinCall(pinin, cb_rise = cb, cbr_args = (pinin, 'rise'), cb_fall = cb, cbf_args = (pinin, 'fall')) - loop.run_until_complete(killer()) - -print('''Link Pyboard pins X1 and X2. - -This test uses a timer to toggle pin X1, recording the time of each state change. - -The basic test with latency False just demonstrates the callbacks. -The latency test measures the time between the leading edge of X1 output and the -driver detecting the state change. This is in the presence of five competing coros -each of which blocks for 5ms. Latency is on the order of 5ms max under fast_io, -50ms max under official V2.0. - -Issue ctrl-D between runs. - -test(fast_io=True, latency=False) -args: -fast_io test fast I/O mechanism. -latency test latency (delay between X1 and X2 leading edge). -Tests run for 20s.''') diff --git a/v2/gps/LICENSE b/v2/gps/LICENSE deleted file mode 100644 index 798b35f..0000000 --- a/v2/gps/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 Calvin McCoy - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/v2/gps/README.md b/v2/gps/README.md deleted file mode 100644 index 2803187..0000000 --- a/v2/gps/README.md +++ /dev/null @@ -1,917 +0,0 @@ -# 1. as_GPS - -This repository offers a suite of asynchronous device drivers for GPS devices -which communicate with the host via a UART. GPS [NMEA-0183] sentence parsing is -based on this excellent library [micropyGPS]. - -## 1.1 Driver characteristics - - * Asynchronous: UART messaging is handled as a background task allowing the - application to perform other tasks such as handling user interaction. - * The read-only driver is suitable for resource constrained devices and will - work with most GPS devices using a UART for communication. - * Can write `.kml` files for displaying journeys on Google Earth. - * The read-write driver enables altering the configuration of GPS devices - based on the popular MTK3329/MTK3339 chips. - * The above drivers are portable between [MicroPython] and Python 3.5 or above. - * Timing drivers for [MicroPython] only extend the capabilities of the - read-only and read-write drivers to provide accurate sub-ms GPS timing. On - STM-based hosts (e.g. the Pyboard) the RTC may be set from GPS and calibrated - to achieve timepiece-level accuracy. - * Drivers may be extended via subclassing, for example to support additional - sentence types. - -Testing was performed using a [Pyboard] with the Adafruit -[Ultimate GPS Breakout] board. Most GPS devices will work with the read-only -driver as they emit [NMEA-0183] sentences on startup. - -## 1.2 Comparison with [micropyGPS] - -[NMEA-0183] sentence parsing is based on [micropyGPS] but with significant -changes. - - * As asynchronous drivers they require `uasyncio` on [MicroPython] or - `asyncio` under Python 3.5+. - * Sentence parsing is adapted for asynchronous use. - * Rollover of local time into the date value enables worldwide use. - * RAM allocation is cut by various techniques to lessen heap fragmentation. - This improves application reliability on RAM constrained devices. - * Some functionality is devolved to a utility module, reducing RAM usage where - these functions are unused. - * The read/write driver is a subclass of the read-only driver. - * Timing drivers are added offering time measurement with μs resolution and - high absolute accuracy. These are implemented by subclassing these drivers. - * Hooks are provided for user-designed subclassing, for example to parse - additional message types. - -###### [Main README](../README.md) - -## 1.1 Overview - -The `AS_GPS` object runs a coroutine which receives [NMEA-0183] sentences from -the UART and parses them as they arrive. Valid sentences cause local bound -variables to be updated. These can be accessed at any time with minimal latency -to access data such as position, altitude, course, speed, time and date. - -### 1.1.1 Wiring - -These notes are for the Adafruit Ultimate GPS Breakout. It may be run from 3.3V -or 5V. If running the Pyboard from USB, GPS Vin may be wired to Pyboard V+. If -the Pyboard is run from a voltage >5V the Pyboard 3V3 pin should be used. - -| GPS | Pyboard | Optional | -|:---:|:----------:|:--------:| -| Vin | V+ or 3V3 | | -| Gnd | Gnd | | -| PPS | X3 | Y | -| Tx | X2 (U4 rx) | | -| Rx | X1 (U4 tx) | Y | - -This is based on UART 4 as used in the test programs; any UART may be used. The -UART Tx-GPS Rx connection is only necessary if using the read/write driver. The -PPS connection is required only if using the timing driver `as_tGPS.py`. Any -pin may be used. - -On the Pyboard D the 3.3V output is switched. Enable it with the following -(typically in `main.py`): -```python -import time -machine.Pin.board.EN_3V3.value(1) -time.sleep(1) -``` - -## 1.2 Basic Usage - -If running on a [MicroPython] target the `uasyncio` library must be installed. - -In the example below a UART is instantiated and an `AS_GPS` instance created. -A callback is specified which will run each time a valid fix is acquired. -The test runs for 60 seconds once data has been received. - -```python -import uasyncio as asyncio -import as_GPS -from machine import UART -def callback(gps, *_): # Runs for each valid fix - print(gps.latitude(), gps.longitude(), gps.altitude) - -uart = UART(4, 9600) -sreader = asyncio.StreamReader(uart) # Create a StreamReader -gps = as_GPS.AS_GPS(sreader, fix_cb=callback) # Instantiate GPS - -async def test(): - print('waiting for GPS data') - await gps.data_received(position=True, altitude=True) - await asyncio.sleep(60) # Run for one minute -loop = asyncio.get_event_loop() -loop.run_until_complete(test()) -``` - -This example achieves the same thing without using a callback: - -```python -import uasyncio as asyncio -import as_GPS -from machine import UART - -uart = UART(4, 9600) -sreader = asyncio.StreamReader(uart) # Create a StreamReader -gps = as_GPS.AS_GPS(sreader) # Instantiate GPS - -async def test(): - print('waiting for GPS data') - await gps.data_received(position=True, altitude=True) - for _ in range(10): - print(gps.latitude(), gps.longitude(), gps.altitude) - await asyncio.sleep(2) - -loop = asyncio.get_event_loop() -loop.run_until_complete(test()) -``` - -## 1.3 Files - -The following are relevant to the default read-only driver. - - * `as_GPS.py` The library. Supports the `AS_GPS` class for read-only access to - GPS hardware. - * `as_GPS_utils.py` Additional formatted string methods for `AS_GPS`. - * `ast_pb.py` Test/demo program: assumes a MicroPython hardware device with - GPS connected to UART 4. - * `log_kml.py` A simple demo which logs a route travelled to a .kml file which - may be displayed on Google Earth. - -On RAM-constrained devices `as_GPS_utils.py` may be omitted in which case the -`date_string` and `compass_direction` methods will be unavailable. - -Files for the read/write driver are listed -[here](./README.md#31-files). -Files for the timing driver are listed -[here](./README.md#41-files). - -## 1.4 Installation - -### 1.4.1 Micropython - -To install on "bare metal" hardware such as the Pyboard copy the files -`as_GPS.py` and `as_GPS_utils.py` onto the device's filesystem and ensure that -`uasyncio` is installed. The code was tested on the Pyboard with `uasyncio` V2 -and the Adafruit [Ultimate GPS Breakout] module. If memory errors are -encountered on resource constrained devices install each file as a -[frozen module]. - -For the [read/write driver](./README.md#3-the-gps-class-read-write-driver) the -file `as_rwGPS.py` must also be installed. The test/demo `ast_pbrw.py` may -optionally be installed; this requires `aswitch.py` from the root of this -repository. -For the [timing driver](./README.md#4-using-gps-for-accurate-timing) -`as_tGPS.py` should also be copied across. The optional test program -`as_GPS_time.py` requires `asyn.py` from the root of this repository. - -### 1.4.2 Python 3.5 or later - -On platforms with an underlying OS such as the Raspberry Pi ensure that the -required driver files are on the Python path and that the Python version is 3.5 -or later. - -# 2. The AS_GPS Class read-only driver - -Method calls and access to bound variables are nonblocking and return the most -current data. This is updated transparently by a coroutine. In situations where -updates cannot be achieved, for example in buildings or tunnels, values will be -out of date. The action to take (if any) is application dependent. - -Three mechanisms exist for responding to outages. - * Check the `time_since_fix` method [section 2.2.3](./README.md#223-time-and-date). - * Pass a `fix_cb` callback to the constructor (see below). - * Cause a coroutine to pause until an update is received: see - [section 2.3.1](./README.md#231-data-validity). This ensures current data. - -## 2.1 Constructor - -Mandatory positional arg: - * `sreader` This is a `StreamReader` instance associated with the UART. -Optional positional args: - * `local_offset` Local timezone offset in hours realtive to UTC (GMT). May be - an integer or float. - * `fix_cb` An optional callback. This runs after a valid message of a chosen - type has been received and processed. - * `cb_mask` A bitmask determining which sentences will trigger the callback. - Default `RMC`: the callback will occur on RMC messages only (see below). - * `fix_cb_args` A tuple of args for the callback (default `()`). - -Notes: -`local_offset` will alter the date when time passes the 00.00.00 boundary. -If `sreader` is `None` a special test mode is engaged (see `astests.py`). - -### 2.1.1 The fix callback - -This receives the following positional args: - 1. The GPS instance. - 2. An integer defining the message type which triggered the callback. - 3. Any args provided in `msg_cb_args`. - -Message types are defined by the following constants in `as_GPS.py`: `RMC`, -`GLL`, `VTG`, `GGA`, `GSA` and `GSV`. - -The `cb_mask` constructor argument may be the logical `or` of any of these -constants. In this example the callback will occur after successful processing -of RMC and VTG messages: - -```python -gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG) -``` - -## 2.2 Public Methods - -### 2.2.1 Location - - * `latitude` Optional arg `coord_format=as_GPS.DD`. Returns the most recent - latitude. - If `coord_format` is `as_GPS.DM` returns a tuple `(degs, mins, hemi)`. - If `as_GPS.DD` is passed returns `(degs, hemi)` where degs is a float. - If `as_GPS.DMS` is passed returns `(degs, mins, secs, hemi)`. - `hemi` is 'N' or 'S'. - - * `longitude` Optional arg `coord_format=as_GPS.DD`. Returns the most recent - longitude. - If `coord_format` is `as_GPS.DM` returns a tuple `(degs, mins, hemi)`. - If `as_GPS.DD` is passed returns `(degs, hemi)` where degs is a float. - If `as_GPS.DMS` is passed returns `(degs, mins, secs, hemi)`. - `hemi` is 'E' or 'W'. - - * `latitude_string` Optional arg `coord_format=as_GPS.DM`. Returns the most - recent latitude in human-readable format. Formats are `as_GPS.DM`, - `as_GPS.DD`, `as_GPS.DMS` or `as_GPS.KML`. - If `coord_format` is `as_GPS.DM` it returns degrees, minutes and hemisphere - ('N' or 'S'). - `as_GPS.DD` returns degrees and hemisphere. - `as_GPS.DMS` returns degrees, minutes, seconds and hemisphere. - `as_GPS.KML` returns decimal degrees, +ve in northern hemisphere and -ve in - southern, intended for logging to Google Earth compatible kml files. - - * `longitude_string` Optional arg `coord_format=as_GPS.DM`. Returns the most - recent longitude in human-readable format. Formats are `as_GPS.DM`, - `as_GPS.DD`, `as_GPS.DMS` or `as_GPS.KML`. - If `coord_format` is `as_GPS.DM` it returns degrees, minutes and hemisphere - ('E' or 'W'). - `as_GPS.DD` returns degrees and hemisphere. - `as_GPS.DMS` returns degrees, minutes, seconds and hemisphere. - `as_GPS.KML` returns decimal degrees, +ve in eastern hemisphere and -ve in - western, intended for logging to Google Earth compatible kml files. - -### 2.2.2 Course - - * `speed` Optional arg `unit=as_GPS.KPH`. Returns the current speed in the - specified units. Options: `as_GPS.KPH`, `as_GPS.MPH`, `as_GPS.KNOT`. - - * `speed_string` Optional arg `unit=as_GPS.KPH`. Returns the current speed in - the specified units. Options `as_GPS.KPH`, `as_GPS.MPH`, `as_GPS.KNOT`. - - * `compass_direction` No args. Returns current course as a string e.g. 'ESE' - or 'NW'. Note that this requires the file `as_GPS_utils.py`. - -### 2.2.3 Time and date - - * `time_since_fix` No args. Returns time in milliseconds since last valid fix. - - * `time_string` Optional arg `local=True`. Returns the current time in form - 'hh:mm:ss.sss'. If `local` is `False` returns UTC time. - - * `date_string` Optional arg `formatting=MDY`. Returns the date as - a string. Formatting options: - `as_GPS.MDY` returns 'MM/DD/YY'. - `as_GPS.DMY` returns 'DD/MM/YY'. - `as_GPS.LONG` returns a string of form 'January 1st, 2014'. - Note that this requires the file `as_GPS_utils.py`. - -## 2.3 Public coroutines - -### 2.3.1 Data validity - -On startup after a cold start it may take time before valid data is received. -During and shortly after an outage messages will be absent. To avoid reading -stale data, reception of messages can be checked before accessing data. - - * `data_received` Boolean args: `position`, `course`, `date`, `altitude`. - All default `False`. The coroutine will pause until at least one valid message - of each specified types has been received. This example will pause until new - position and altitude messages have been received: - -```python -while True: - await my_gps.data_received(position=True, altitude=True) - # Access these data values now -``` - -No option is provided for satellite data: this functionality is provided by the -`get_satellite_data` coroutine. - -### 2.3.2 Satellite Data - -Satellite data requires multiple sentences from the GPS and therefore requires -a coroutine which will pause execution until a complete set of data has been -acquired. - - * `get_satellite_data` No args. Waits for a set of GSV (satellites in view) - sentences and returns a dictionary. Typical usage in a user coroutine: - -```python - d = await my_gps.get_satellite_data() - print(d.keys()) # List of satellite PRNs - print(d.values()) # [(elev, az, snr), (elev, az, snr)...] -``` - -Dictionary values are (elevation, azimuth, snr) where elevation and azimuth are -in degrees and snr (a measure of signal strength) is in dB in range 0-99. -Higher is better. - -Note that if the GPS module does not support producing GSV sentences this -coroutine will pause forever. It can also pause for arbitrary periods if -satellite reception is blocked, such as in a building. - -## 2.4 Public bound variables/properties - -These are updated whenever a sentence of the relevant type has been correctly -received from the GPS unit. For crucial navigation data the `time_since_fix` -method may be used to determine how current these values are. - -The sentence type which updates a value is shown in brackets e.g. (GGA). - -### 2.4.1 Position/course - - * `course` Track angle in degrees. (VTG). - * `altitude` Metres above mean sea level. (GGA). - * `geoid_height` Height of geoid (mean sea level) in metres above WGS84 - ellipsoid. (GGA). - * `magvar` Magnetic variation. Degrees. -ve == West. Current firmware does not - produce this data: it will always read zero. - -### 2.4.2 Statistics and status - -The following are counts since instantiation. - * `crc_fails` Usually 0 but can occur on baudrate change. - * `clean_sentences` Number of sentences received without major failures. - * `parsed_sentences` Sentences successfully parsed. - * `unsupported_sentences` This is incremented if a sentence is received which - has a valid format and checksum, but is not supported by the class. This - value will also increment if these are supported in a subclass. See - [section 6](./README.md#6-developer-notes). - -### 2.4.3 Date and time - - * `utc` (property) [hrs: int, mins: int, secs: int] UTC time e.g. - [23, 3, 58]. Note the integer seconds value. The MTK3339 chip provides a float - buts its value is always an integer. To achieve accurate subsecond timing see - [section 4](./README.md#4-using-gps-for-accurate-timing). - * `local_time` (property) [hrs: int, mins: int, secs: int] Local time. - * `date` (property) [day: int, month: int, year: int] e.g. [23, 3, 18] - * `local_offset` Local time offset in hrs as specified to constructor. - * `epoch_time` Integer. Time in seconds since the epoch. Epoch start depends - on whether running under MicroPython (Y2K) or Python 3.5+ (1970 on Unix). - -The `utc`, `date` and `local_time` properties updates on receipt of RMC -messages. If a nonzero `local_offset` value is specified the `date` value will -update when local time passes midnight (local time and date are computed from -`epoch_time`). - -### 2.4.4 Satellite data - - * `satellites_in_view` No. of satellites in view. (GSV). - * `satellites_in_use` No. of satellites in use. (GGA). - * `satellites_used` List of satellite PRN's. (GSA). - * `pdop` Dilution of precision (GSA). - * `hdop` Horizontal dilution of precsion (GSA). - * `vdop` Vertical dilution of precision (GSA). - -Dilution of Precision (DOP) values close to 1.0 indicate excellent quality -position data. Increasing values indicate decreasing precision. - -## 2.5 Subclass hooks - -The following public methods are null. They are intended for optional -overriding in subclasses. Or monkey patching if you like that sort of thing. - - * `reparse` Called after a supported sentence has been parsed. - * `parse` Called when an unsupported sentence has been received. - -If the received string is invalid (e.g. bad character or incorrect checksum) -these will not be called. - -Both receive as arguments a list of strings, each being a segment of the comma -separated sentence. The '$' character in the first arg and the '*' character -and subsequent characters are stripped from the last. Thus if the string -`b'$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n'` -was received `reparse` would see -`['GPGGA','123519','4807.038','N','01131.000','E','1','08','0.9','545.4','M','46.9','M','','']` - -## 2.6 Public class variable - - * `FULL_CHECK` Default `True`. If set `False` disables CRC checking and other - basic checks on received sentences. If GPS is linked directly to the target - (rather than via long cables) these checks are arguably not neccessary. - -# 3. The GPS class read-write driver - -This is a subclass of `AS_GPS` and supports all its public methods, coroutines -and bound variables. It provides support for sending PMTK command packets to -GPS modules based on the MTK3329/MTK3339 chip. These include: - - * Adafruit Ultimate GPS Breakout - * Digilent PmodGPS - * Sparkfun GPS Receiver LS20031 - * 43oh MTK3339 GPS Launchpad Boosterpack - -A subset of the PMTK packet types is supported but this may be extended by -subclassing. - -## 3.1 Files - - * `as_rwGPS.py` Supports the `GPS` class. This subclass of `AS_GPS` enables - writing PMTK packets. - * `as_GPS.py` The library containing the `AS_GPS` base class. - * `as_GPS_utils.py` Additional formatted string methods. - * `ast_pbrw.py` Test script which changes various attributes. - -The test script will pause until a fix has been achieved. After that changes -are made for about 1 minute, after which it runs indefinitely reporting data at -the REPL and on the LEDs. It may be interrupted with `ctrl-c` when the default -baudrate will be restored. - -LED's: - * Red: Toggles each time a GPS update occurs. - * Green: ON if GPS data is being received, OFF if no data received for >10s. - * Yellow: Toggles each 4s if navigation updates are being received. - * Blue: Toggles each 4s if time updates are being received. - -### 3.1.1 Usage example - -This reduces to 2s the interval at which the GPS sends messages: - -```python -import uasyncio as asyncio -import as_rwGPS -from machine import UART - -uart = UART(4, 9600) -sreader = asyncio.StreamReader(uart) # Create a StreamReader -swriter = asyncio.StreamWriter(uart, {}) -gps = as_rwGPS.GPS(sreader, swriter) # Instantiate GPS - -async def test(): - print('waiting for GPS data') - await gps.data_received(position=True, altitude=True) - await gps.update_interval(2000) # Reduce message rate - for _ in range(10): - print(gps.latitude(), gps.longitude(), gps.altitude) - await asyncio.sleep(2) - -loop = asyncio.get_event_loop() -loop.run_until_complete(test()) -``` - -## 3.2 GPS class Constructor - -This takes two mandatory positional args: - * `sreader` This is a `StreamReader` instance associated with the UART. - * `swriter` This is a `StreamWriter` instance associated with the UART. - -Optional positional args: - * `local_offset` Local timezone offset in hours realtive to UTC (GMT). - * `fix_cb` An optional callback which runs each time a valid fix is received. - * `cb_mask` A bitmask determining which sentences will trigger the callback. - Default `RMC`: the callback will occur on RMC messages only (see below). - * `fix_cb_args` A tuple of args for the callback. - * `msg_cb` Optional callback. This will run if any handled message is received - and also for unhandled `PMTK` messages. - * `msg_cb_args` A tuple of args for the above callback. - -If implemented the message callback will receive the following positional args: - 1. The GPS instance. - 2. A list of text strings from the message. - 3. Any args provided in `msg_cb_args`. - -In the case of handled messages the list of text strings has length 2. The -first is 'version', 'enabled' or 'antenna' followed by the value of the -relevant bound variable e.g. `['antenna', 3]`. - -For unhandled messages text strings are as received, processed as per -[section 2.5](./README.md#25-subclass-hooks). - -The args presented to the fix callback are as described in -[section 2.1](./README.md#21-constructor). - -## 3.3 Public coroutines - - * `baudrate` Arg: baudrate. Must be 4800, 9600, 14400, 19200, 38400, 57600 - or 115200. See below. - * `update_interval` Arg: interval in ms. Default 1000. Must be between 100 - and 10000. If the rate is to be increased see - [notes on timing](./README.md#7-notes-on-timing). - * `enable` Determine the frequency with which each sentence type is sent. A - value of 0 disables a sentence, a value of 1 causes it to be sent with each - received position fix. A value of N causes it to be sent once every N fixes. - It takes 7 keyword-only integer args, one for each supported sentence. These, - with default values, are: - `gll=0`, `rmc=1`, `vtg=1`, `gga=1`, `gsa=1`, `gsv=5`, `chan=0`. The last - represents GPS channel status. These values are the factory defaults. - * `command` Arg: a command from the following set: - * `as_rwGPS.HOT_START` Use all available data in the chip's NV Store. - * `as_rwGPS.WARM_START` Don't use Ephemeris at re-start. - * `as_rwGPS.COLD_START` Don't use Time, Position, Almanacs and Ephemeris data - at re-start. - * `as_rwGPS.FULL_COLD_START` A 'cold_start', but additionally clear - system/user configurations at re-start. That is, reset the receiver to the - factory status. - * `as_rwGPS.STANDBY` Put into standby mode. Sending any command resumes - operation. - * `as_rwGPS.DEFAULT_SENTENCES` Sets all sentence frequencies to factory - default values as listed under `enable`. - * `as_rwGPS.VERSION` Causes the GPS to report its firmware version. This will - appear as the `version` bound variable when the report is received. - * `as_rwGPS.ENABLE` Causes the GPS to report the enabled status of the various - message types as set by the `enable` coroutine. This will appear as the - `enable` bound variable when the report is received. - * `as_rwGPS.ANTENNA` Causes the GPS to send antenna status messages. The - status value will appear in the `antenna` bound variable each time a report is - received. - * `as_rwGPS.NO_ANTENNA` Turns off antenna messages. - -**Antenna issues** In my testing the antenna functions have issues which -hopefully will be fixed in later firmware versions. The `NO_ANTENNA` message -has no effect. And, while issuing the `ANTENNA` message works, it affects the -response of the unit to subsequent commands. If possible issue it after all -other commands have been sent. I have also observed issues which can only be -cleared by power cycling the GPS. - -### 3.3.1 Changing baudrate - -I have experienced failures on a Pyboard V1.1 at baudrates higher than 19200. -Under investigation. **TODO UPDATE THIS** - -Further, there are problems (at least with my GPS firmware build -['AXN_2.31_3339_13101700', '5632', 'PA6H', '1.0']) whereby setting baudrates -only works for certain rates. 19200, 38400, 57600 and 115200 work. 4800 -sets 115200. Importantly 9600 does nothing. This means that the only way to -restore the default is to perform a `FULL_COLD_START`. The test programs do -this. - -If you change the GPS baudrate the UART should be re-initialised immediately -after the `baudrate` coroutine terminates: - -```python -async def change_status(gps, uart): - await gps.baudrate(19200) - uart.init(19200) -``` - -At risk of stating the obvious to seasoned programmers, if your application -changes the GPS unit's baudrate and you interrupt it with ctrl-c, the GPS will -still be running at the new baudrate. Your application may need to be designed -to reflect this: see `ast_pbrw.py` which uses try-finally to reset the baudrate -in the event that the program terminates due to an exception or otherwise. - -Particular care needs to be used if a backup battery is employed as the GPS -will then remember its baudrate over a power cycle. - -See also [notes on timing](./README.md#7-notes-on-timing). - -## 3.4 Public bound variables - -These are updated when a response to a command is received. The time taken for -this to occur depends on the GPS unit. One solution is to implement a message -callback. Alternatively await a coroutine which periodically (in intervals -measured in seconds) polls the value, returning it when it changes. - - * `version` Initially `None`. A list of version strings. - * `enabled` Initially `None`. A dictionary of frequencies indexed by message - type (see `enable` coroutine above). - * `antenna` Initially 0. Values: - 0 No report received. - 1 Antenna fault. - 2 Internal antenna. - 3 External antenna. - -## 3.5 The parse method (developer note) - -The null `parse` method in the base class is overridden. It intercepts the -single response to `VERSION` and `ENABLE` commands and updates the above bound -variables. The `ANTENNA` command causes repeated messages to be sent. These -update the `antenna` bound variable. These "handled" messages call the message -callback with the `GPS` instance followed by a list of sentence segments -followed by any args specified in the constructor. - -Other `PMTK` messages are passed to the optional message callback as described -[in section 3.2](./README.md#32-gps-class-constructor). - -# 4. Using GPS for accurate timing - -Many GPS chips (e.g. MTK3339) provide a PPS signal which is a pulse occurring -at 1s intervals whose leading edge is a highly accurate UTC time reference. - -This driver uses this pulse to provide accurate subsecond UTC and local time -values. The driver requires MicroPython because PPS needs a pin interrupt. - -On STM platforms such as the Pyboard it may be used to set and to calibrate the -realtime clock (RTC). This functionality is not currently portable to other -chips. - -See [Absolute accuracy](./README.md#45-absolute-accuracy) for a discussion of -the absolute accuracy provided by this module (believed to be on the order of -+-70μs). - -Two classes are provided: `GPS_Timer` for read-only access to the GPS device -and `GPS_RWTimer` for read/write access. - -## 4.1 Files - - * `as_GPS.py` The library containing the base class. - * `as_GPS_utils.py` Additional formatted string methods for `AS_GPS`. - * `as_rwGPS.py` Required if using the read/write variant. - * `as_tGPS.py` The library. Provides `GPS_Timer` and `GPS_RWTimer` classes. - * `as_GPS_time.py` Test scripts for read only driver. - * `as_rwGPS_time.py` Test scripts for read/write driver. - -### 4.1.1 Usage example - -```python -import uasyncio as asyncio -import pyb -import as_tGPS - -async def test(): - fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}' - red = pyb.LED(1) - blue = pyb.LED(4) - uart = pyb.UART(4, 9600, read_buf_len=200) - sreader = asyncio.StreamReader(uart) - pps_pin = pyb.Pin('X3', pyb.Pin.IN) - gps_tim = as_tGPS.GPS_Timer(sreader, pps_pin, local_offset=1, - fix_cb=lambda *_: red.toggle(), - pps_cb=lambda *_: blue.toggle()) - print('Waiting for signal.') - await gps_tim.ready() # Wait for GPS to get a signal - await gps_tim.set_rtc() # Set RTC from GPS - while True: - await asyncio.sleep(1) - # In a precision app, get the time list without allocation: - t = gps_tim.get_t_split() - print(fstr.format(gps_tim.get_ms(), t[0], t[1], t[2], t[3])) - -loop = asyncio.get_event_loop() -loop.create_task(test()) -loop.run_forever() -``` - -## 4.2 GPS_Timer and GPS_RWTimer classes - -These classes inherit from `AS_GPS` and `GPS` respectively, with read-only and -read/write access to the GPS hardware. All public methods and bound variables of -the base classes are supported. Additional functionality is detailed below. - -### 4.2.1 GPS_Timer class Constructor - -Mandatory positional args: - * `sreader` The `StreamReader` instance associated with the UART. - * `pps_pin` An initialised input `Pin` instance for the PPS signal. - -Optional positional args: - * `local_offset` See [base class](./README.md#21-constructor) for details of - these args. - * `fix_cb` - * `cb_mask` - * `fix_cb_args` - * `pps_cb` Callback runs when a PPS interrupt occurs. The callback runs in an - interrupt context so it should return quickly and cannot allocate RAM. Default - is a null method. See below for callback args. - * `pps_cb_args` Default `()`. A tuple of args for the callback. The callback - receives the `GPS_Timer` instance as the first arg, followed by any args in - the tuple. - -### 4.2.2 GPS_RWTimer class Constructor - -This takes three mandatory positional args: - * `sreader` The `StreamReader` instance associated with the UART. - * `swriter` The `StreamWriter` instance associated with the UART. - * `pps_pin` An initialised input `Pin` instance for the PPS signal. - -Optional positional args: - * `local_offset` See [base class](./README.md#32-gps-class-constructor) for - details of these args. - * `fix_cb` - * `cb_mask` - * `fix_cb_args` - * `msg_cb` - * `msg_cb_args` - * `pps_cb` Callback runs when a PPS interrupt occurs. The callback runs in an - interrupt context so it should return quickly and cannot allocate RAM. Default - is a null method. See below for callback args. - * `pps_cb_args` Default `()`. A tuple of args for the callback. The callback - receives the `GPS_RWTimer` instance as the first arg, followed by any args in - the tuple. - -## 4.3 Public methods - -The methods that return an accurate GPS time of day run as fast as possible. To -achieve this they avoid allocation and dispense with error checking: these -methods should not be called until a valid time/date message and PPS signal -have occurred. Await the `ready` coroutine prior to first use. Subsequent calls -may occur without restriction; see usage example above. - -These methods use the MicroPython microsecond timer to interpolate between PPS -pulses. They do not involve the RTC. Hence they should work on any MicroPython -target supporting `machine.ticks_us`. - - * `get_ms` No args. Returns an integer: the period past midnight in ms. - * `get_t_split` No args. Returns time of day in a list of form - `[hrs: int, mins: int, secs: int, μs: int]`. - * `close` No args. Shuts down the PPS pin interrupt handler. Usage is optional - but in test situations avoids the ISR continuing to run after termination. - -See [Absolute accuracy](./README.md#45-absolute-accuracy) for a discussion of -the accuracy of these methods. - -## 4.4 Public coroutines - -All MicroPython targets: - * `ready` No args. Pauses until a valid time/date message and PPS signal have - occurred. - -STM hosts only: - * `set_rtc` No args. Sets the RTC to GPS time. Coroutine pauses for up - to 1s as it waits for a PPS pulse. - * `delta` No args. Returns no. of μs RTC leads GPS. Coro pauses for up to 1s. - * `calibrate` Arg: integer, no. of minutes to run default 5. Calibrates the - RTC and returns the calibration factor for it. - -The `calibrate` coroutine sets the RTC (with any existing calibration removed) -and measures its drift with respect to the GPS time. This measurement becomes -more precise as time passes. It calculates a calibration value at 10s intervals -and prints progress information. When the calculated calibration factor is -repeatable within one digit (or the spcified time has elapsed) it terminates. -Typical run times are on the order of two miutes. - -Achieving an accurate calibration factor takes time but does enable the Pyboard -RTC to achieve timepiece quality results. Note that calibration is lost on -power down: solutions are either to use an RTC backup battery or to store the -calibration factor in a file (or in code) and re-apply it on startup. - -Crystal oscillator frequency has a small temperature dependence; consequently -the optimum calibration factor has a similar dependence. For best results allow -the hardware to reach working temperature before calibrating. - -## 4.5 Absolute accuracy - -The claimed absolute accuracy of the leading edge of the PPS signal is +-10ns. -In practice this is dwarfed by errors including latency in the MicroPython VM. -Nevertheless the `get_ms` method can be expected to provide 1 digit (+-1ms) -accuracy and the `get_t_split` method should provide accuracy on the order of --5μs +65μs (standard deviation). This is based on a Pyboard running at 168MHz. -The reasoning behind this is discussed in -[section 7](./README.md#7-notes-on-timing). - -## 4.6 Test/demo program as_GPS_time.py - -This comprises the following test functions. Reset the chip with ctrl-d between -runs. - * `time(minutes=1)` Print out GPS time values. - * `calibrate(minutes=5)` Determine the calibration factor of the Pyboard RTC. - Set it and calibrate it. - * `drift(minutes=5)` Monitor the drift between RTC time and GPS time. At the - end of the run, print the error in μs/hr and minutes/year. - * `usec(minutes=1)` Measure the accuracy of `utime.ticks_us()` against the PPS - signal. Print basic statistics at the end of the run. Provides an estimate of - some limits to the absolute accuracy of the `get_t_split` method as discussed - above. - -# 5. Supported Sentences - - * GPRMC GP indicates NMEA sentence (US GPS system). - * GLRMC GL indicates GLONASS (Russian system). - * GNRMC GN GNSS (Global Navigation Satellite System). - * GPGLL - * GLGLL - * GPGGA - * GLGGA - * GNGGA - * GPVTG - * GLVTG - * GNVTG - * GPGSA - * GLGSA - * GPGSV - * GLGSV - -# 6 Developer notes - -These notes are for those wishing to adapt these drivers. - -## 6.1 Subclassing - -If support for further sentence types is required the `AS_GPS` class may be -subclassed. If a correctly formed sentence with a valid checksum is received, -but is not supported, the `parse` method is called. By default this is a -`lambda` which ignores args and returns `True`. - -A subclass may override `parse` to parse such sentences. An example this may be -found in the `as_rwGPS.py` module. - -The `parse` method receives an arg `segs` being a list of strings. These are -the parts of the sentence which were delimited by commas. See -[section 2.5](./README.md#25-subclass-hooks) for details. - -The `parse` method should return `True` if the sentence was successfully -parsed, otherwise `False`. - -Where a sentence is successfully parsed by the driver, a null `reparse` method -is called. It receives the same string list as `parse`. It may be overridden in -a subclass, possibly to extract further information from the sentence. - -## 6.2 Special test programs - -These tests allow NMEA parsing to be verified in the absence of GPS hardware: - - * `astests.py` Test with synthetic data. Run on CPython 3.x or MicroPython. - * `astests_pyb.py` Test with synthetic data on UART. GPS hardware replaced by - a loopback on UART 4. Requires CPython 3.5 or later or MicroPython and - `uasyncio`. - -# 7. Notes on timing - -At the default 1s update rate the GPS hardware emits a PPS pulse followed by a -set of messages. It then remains silent until the next PPS. At the default -baudrate of 9600 the UART continued receiving data for 400ms when a set of GPSV -messages came in. This time could be longer depending on data. So if an update -rate higher than the default 1 second is to be used, either the baudrate should -be increased or satellite information messages should be disabled. - -The accuracy of the timing drivers may be degraded if a PPS pulse arrives while -the UART is still receiving. The update rate should be chosen to avoid this. - -The PPS signal on the MTK3339 occurs only when a fix has been achieved. The -leading edge occurs on a 1s boundary with high absolute accuracy. It therefore -follows that the RMC message carrying the time/date of that second arrives -after the leading edge (because of processing and UART latency). It is also -the case that on a one-second boundary minutes, hours and the date may roll -over. - -Further, the local_time offset can affect the date. These drivers aim to handle -these factors. They do this by storing the epoch time (as an integer number of -seconds) as the fundamental time reference. This is updated by the RMC message. -The `utc`, `date` and `localtime` properties convert this to usable values with -the latter two using the `local_offset` value to ensure correct results. - -## 7.1 Absolute accuracy - -Without an atomic clock synchronised to a Tier 1 NTP server, absolute accuracy -(Einstein notwithstanding :-)) is hard to prove. However if the manufacturer's -claim of the accuracy of the PPS signal is accepted, the errors contributed by -MicroPython can be estimated. - -The driver interpolates between PPS pulses using `utime.ticks_us()` to provide -μs precision. The leading edge of PPS triggers an interrupt which records the -arrival time of PPS in the `acquired` bound variable. The ISR also records, to -1 second precision, an accurate datetime derived from the previous RMC message. -The time can therefore be estimated by taking the datetime and adding the -elapsed time since the time stored in the `acquired` bound variable. This is -subject to the following errors: - -Sources of fixed lag: - * Latency in the function used to retrieve the time. - * Mean value of the interrupt latency. - -Sources of variable error: - * Variations in interrupt latency (small on Pyboard). - * Inaccuracy in the `ticks_us` timer (significant over a 1 second interval). - -With correct usage when the PPS interrupt occurs the UART will not be receiving -data (this can substantially affect ISR latency variability). Consequently, on -the Pyboard, variations in interrupt latency are small. Using an osciloscope a -normal latency of 15μs was measured with the `time` test in `as_GPS_time.py` -running. The maximum observed was 17μs. - -The test program `as_GPS_time.py` has a test `usecs` which aims to assess the -sources of variable error. Over a period it repeatedly uses `ticks_us` to -measure the time between PPS pulses. Given that the actual time is effectively -constant the measurement is of error relative to the expected value of 1s. At -the end of the measurement period the test calculates some simple statistics on -the results. On targets other than a 168MHz Pyboard this may be run to estimate -overheads. - -The timing method `get_t_split` measures the time when it is called, which it -records as quickly as possible. Assuming this has a similar latency to the ISR -there is likely to be a 30μs lag coupled with ~+-35μs (SD) jitter largely -caused by inaccuracy of `ticks_us` over a 1 second period. Note that I have -halved the jitter time on the basis that the timing method is called -asynchronously to PPS: the interval will centre on 0.5s. The assumption is that -inaccuracy in the `ticks_us` timer measured in μs is proportional to the -duration over which it is measured. - -[MicroPython]:https://micropython.org/ -[frozen module]:https://learn.adafruit.com/micropython-basics-loading-modules/frozen-modules -[NMEA-0183]:http://aprs.gids.nl/nmea/ -[TinyGPS]:http://arduiniana.org/libraries/tinygps/ -[pyboard]:http://docs.micropython.org/en/latest/pyboard/pyboard/quickref.html -[MTK_command]:https://github.com/inmcm/MTK_commands -[Ultimate GPS Breakout]:http://www.adafruit.com/product/746 -[micropyGPS]:https://github.com/inmcm/micropyGPS.git diff --git a/v2/gps/as_GPS.py b/v2/gps/as_GPS.py deleted file mode 100644 index 1a912d5..0000000 --- a/v2/gps/as_GPS.py +++ /dev/null @@ -1,614 +0,0 @@ -# as_GPS.py Asynchronous device driver for GPS devices using a UART. -# Sentence parsing based on MicropyGPS by Michael Calvin McCoy -# https://github.com/inmcm/micropyGPS -# http://www.gpsinformation.org/dale/nmea.htm -# Docstrings removed because of question marks over their use in resource -# constrained systems e.g. https://github.com/micropython/micropython/pull/3748 - -# Copyright (c) 2018-2020 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -# astests.py runs under CPython but not MicroPython because mktime is missing -# from Unix build of utime - -try: - import uasyncio as asyncio -except ImportError: - import asyncio - -try: - from micropython import const -except ImportError: - const = lambda x : x - -from math import modf - -# Float conversion tolerant of empty field -# gfloat = lambda x : float(x) if x else 0.0 - -# Angle formats -DD = const(1) -DMS = const(2) -DM = const(3) -KML = const(4) -# Speed units -KPH = const(10) -MPH = const(11) -KNOT = const(12) -# Date formats -MDY = const(20) -DMY = const(21) -LONG = const(22) - -# Sentence types -RMC = const(1) -GLL = const(2) -VTG = const(4) -GGA = const(8) -GSA = const(16) -GSV = const(32) -# Messages carrying data -POSITION = const(RMC | GLL | GGA) -ALTITUDE = const(GGA) -DATE = const(RMC) -COURSE = const(RMC | VTG) - - -class AS_GPS(object): - # Can omit time consuming checks: CRC 6ms Bad char and line length 9ms - FULL_CHECK = True - _SENTENCE_LIMIT = 76 # Max sentence length (based on GGA sentence) - _NO_FIX = 1 - - # Return day of week from date. Pyboard RTC format: 1-7 for Monday through Sunday. - # https://stackoverflow.com/questions/9847213/how-do-i-get-the-day-of-week-given-a-date-in-python?noredirect=1&lq=1 - # Adapted for Python 3 and Pyboard RTC format. - @staticmethod - def _week_day(year, month, day, offset = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]): - aux = year - 1700 - (1 if month <= 2 else 0) - # day_of_week for 1700/1/1 = 5, Friday - day_of_week = 5 - # partial sum of days betweem current date and 1700/1/1 - day_of_week += (aux + (1 if month <= 2 else 0)) * 365 - # leap year correction - day_of_week += aux // 4 - aux // 100 + (aux + 100) // 400 - # sum monthly and day offsets - day_of_week += offset[month - 1] + (day - 1) - day_of_week %= 7 - day_of_week = day_of_week if day_of_week else 7 - return day_of_week - - # 8-bit xor of characters between "$" and "*". Takes 6ms on Pyboard! - @staticmethod - def _crc_check(res, ascii_crc): - try: - crc = int(ascii_crc, 16) - except ValueError: - return False - x = 1 - crc_xor = 0 - while res[x] != '*': - crc_xor ^= ord(res[x]) - x += 1 - return crc_xor == crc - - def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC, fix_cb_args=()): - self._sreader = sreader # If None testing: update is called with simulated data - self._fix_cb = fix_cb - self.cb_mask = cb_mask - self._fix_cb_args = fix_cb_args - self.battery = False # Assume no backup battery - - # CPython compatibility. Import utime or time for fix time handling. - try: - import utime - self._get_time = utime.ticks_ms - self._time_diff = utime.ticks_diff - self._localtime = utime.localtime - self._mktime = utime.mktime - except ImportError: - # Otherwise default to time module for non-embedded implementations - # Should still support millisecond resolution. - import time - self._get_time = time.time - self._time_diff = lambda start, end: 1000 * (start - end) - self._localtime = time.localtime - self._mktime = time.mktime - - # Key: currently supported NMEA sentences. Value: parse method. - self.supported_sentences = {'RMC': self._gprmc, - 'GGA': self._gpgga, - 'VTG': self._gpvtg, - 'GSA': self._gpgsa, - 'GSV': self._gpgsv, - 'GLL': self._gpgll, - } - - ##################### - # Object Status Flags - self._fix_time = None - - ##################### - # Sentence Statistics - self.crc_fails = 0 - self.clean_sentences = 0 - self.parsed_sentences = 0 - self.unsupported_sentences = 0 - - ##################### - # Data From Sentences - # Time. http://www.gpsinformation.org/dale/nmea.htm indicates seconds - # is an integer. However hardware returns a float, but the fractional - # part is always zero. So treat seconds value as an integer. For - # precise timing use PPS signal and as_tGPS library. - self.local_offset = local_offset # hrs - self.epoch_time = 0 # Integer secs since epoch (Y2K under MicroPython) - # Add ms if supplied by device. Only used by timing drivers. - self.msecs = 0 - - # Position/Motion - self._latitude = [0, 0.0, 'N'] # (°, mins, N/S) - self._longitude = [0, 0.0, 'W'] # (°, mins, E/W) - self._speed = 0.0 # Knot - self.course = 0.0 # ° clockwise from N - self.altitude = 0.0 # Metres - self.geoid_height = 0.0 # Metres - self.magvar = 0.0 # Magnetic variation (°, -ve == west) - - # State variables - self._last_sv_sentence = 0 # for GSV parsing - self._total_sv_sentences = 0 - self._satellite_data = dict() # for get_satellite_data() - self._update_ms = 1000 # Update rate for timing drivers. Default 1 sec. - - # GPS Info - self.satellites_in_view = 0 - self.satellites_in_use = 0 - self.satellites_used = [] - self.hdop = 0.0 - self.pdop = 0.0 - self.vdop = 0.0 - - # Received status - self._valid = 0 # Bitfield of received sentences - if sreader is not None: # Running with UART data - loop = asyncio.get_event_loop() - loop.create_task(self._run(loop)) - - ########################################## - # Data Stream Handler Functions - ########################################## - - async def _run(self, loop): - while True: - res = await self._sreader.readline() - try: - res = res.decode('utf8') - except UnicodeError: # Garbage: can happen e.g. on baudrate change - continue - loop.create_task(self._update(res)) - await asyncio.sleep(0) # Ensure task runs and res is copied - - # Update takes a line of text - async def _update(self, line): - line = line.rstrip() # Copy line - # Basic integrity check: may have received partial line e.g on power up - if not line.startswith('$') or not '*' in line or len(line) > self._SENTENCE_LIMIT: - return - # 2.4ms on Pyboard: - if self.FULL_CHECK and not all(10 <= ord(c) <= 126 for c in line): - return # Bad character received - - a = line.split(',') - segs = a[:-1] + a[-1].split('*') - await asyncio.sleep(0) - - if self.FULL_CHECK: # 6ms on Pyboard - if not self._crc_check(line, segs[-1]): - self.crc_fails += 1 # Update statistics - return - await asyncio.sleep(0) - - self.clean_sentences += 1 # Sentence is good but unparsed. - segs[0] = segs[0][1:] # discard $ - segs = segs[:-1] # and checksum - seg0 = segs[0] # e.g. GPGLL - segx = seg0[2:] # e.g. GLL - if seg0.startswith('G') and segx in self.supported_sentences: - try: - s_type = self.supported_sentences[segx](segs) # Parse - except ValueError: - s_type = False - await asyncio.sleep(0) - if isinstance(s_type, int) and (s_type & self.cb_mask): - # Successfully parsed, data was valid and mask matches sentence type - self._fix_cb(self, s_type, *self._fix_cb_args) # Run the callback - if s_type: # Successfully parsed - if self.reparse(segs): # Subclass hook - self.parsed_sentences += 1 - return seg0 # For test programs - else: - if self.parse(segs): # Subclass hook - self.parsed_sentences += 1 - self.unsupported_sentences += 1 - return seg0 # For test programs - - # Optional hooks for subclass - def parse(self, segs): # Parse unsupported sentences - return True - - def reparse(self, segs): # Re-parse supported sentences - return True - - ######################################## - # Fix and Time Functions - ######################################## - - # Caller traps ValueError - def _fix(self, gps_segments, idx_lat, idx_long): - # Latitude - l_string = gps_segments[idx_lat] - lat_degs = int(l_string[0:2]) - lat_mins = float(l_string[2:]) - lat_hemi = gps_segments[idx_lat + 1] - # Longitude - l_string = gps_segments[idx_long] - lon_degs = int(l_string[0:3]) - lon_mins = float(l_string[3:]) - lon_hemi = gps_segments[idx_long + 1] - - if lat_hemi not in 'NS'or lon_hemi not in 'EW': - raise ValueError - self._latitude[0] = lat_degs # In-place to avoid allocation - self._latitude[1] = lat_mins - self._latitude[2] = lat_hemi - self._longitude[0] = lon_degs - self._longitude[1] = lon_mins - self._longitude[2] = lon_hemi - self._fix_time = self._get_time() - - def _dtset(self, _): # For subclass - pass - - # A local offset may exist so check for date rollover. Local offsets can - # include fractions of an hour but not seconds (AFAIK). - # Caller traps ValueError - def _set_date_time(self, utc_string, date_string): - if not date_string or not utc_string: - raise ValueError - hrs = int(utc_string[0:2]) # h - mins = int(utc_string[2:4]) # mins - # Secs from MTK3339 chip is a float but others may return only 2 chars - # for integer secs. If a float keep epoch as integer seconds and store - # the fractional part as integer ms (ms since midnight fits 32 bits). - fss, fsecs = modf(float(utc_string[4:])) - secs = int(fsecs) - self.msecs = int(fss * 1000) - d = int(date_string[0:2]) # day - m = int(date_string[2:4]) # month - y = int(date_string[4:6]) + 2000 # year - wday = self._week_day(y, m, d) - t = int(self._mktime((y, m, d, hrs, mins, int(secs), wday - 1, 0, 0))) - self.epoch_time = t # This is the fundamental datetime reference. - self._dtset(wday) # Subclass may override - - ######################################## - # Sentence Parsers - ######################################## - -# For all parsers: -# Initially the ._valid bit for the sentence type is cleared. -# On error a ValueError is raised: trapped by the caller. -# On successful parsing the ._valid bit is set. -# The ._valid mechanism enables the data_received coro to determine what -# sentence types have been received. - - # Chip sends rubbish RMC messages before first PPS pulse, but these have - # data valid set to 'V' (void) - def _gprmc(self, gps_segments): # Parse RMC sentence - self._valid &= ~RMC - # Check Receiver Data Valid Flag ('A' active) - if not self.battery: - if gps_segments[2] != 'A': - raise ValueError - - # UTC Timestamp and date. Can raise ValueError. - self._set_date_time(gps_segments[1], gps_segments[9]) - # Check Receiver Data Valid Flag ('A' active) - if gps_segments[2] != 'A': - raise ValueError - - # Data from Receiver is Valid/Has Fix. Longitude / Latitude - # Can raise ValueError. - self._fix(gps_segments, 3, 5) - # Speed - spd_knt = float(gps_segments[7]) - # Course - course = float(gps_segments[8]) - # Add Magnetic Variation if firmware supplies it - if gps_segments[10]: - mv = float(gps_segments[10]) # Float conversions can throw ValueError, caught by caller. - if gps_segments[11] not in ('EW'): - raise ValueError - self.magvar = mv if gps_segments[11] == 'E' else -mv - # Update Object Data - self._speed = spd_knt - self.course = course - self._valid |= RMC - return RMC - - def _gpgll(self, gps_segments): # Parse GLL sentence - self._valid &= ~GLL - # Check Receiver Data Valid Flag - if gps_segments[6] != 'A': # Invalid. Don't update data - raise ValueError - - # Data from Receiver is Valid/Has Fix. Longitude / Latitude - self._fix(gps_segments, 1, 3) - # Update Last Fix Time - self._valid |= GLL - return GLL - - # Chip sends VTG messages with meaningless data before getting a fix. - def _gpvtg(self, gps_segments): # Parse VTG sentence - self._valid &= ~VTG - course = float(gps_segments[1]) - spd_knt = float(gps_segments[5]) - self._speed = spd_knt - self.course = course - self._valid |= VTG - return VTG - - def _gpgga(self, gps_segments): # Parse GGA sentence - self._valid &= ~GGA - # Number of Satellites in Use - satellites_in_use = int(gps_segments[7]) - # Horizontal Dilution of Precision - hdop = float(gps_segments[8]) - # Get Fix Status - fix_stat = int(gps_segments[6]) - - # Process Location and Altitude if Fix is GOOD - if fix_stat: - # Longitude / Latitude - self._fix(gps_segments, 2, 4) - # Altitude / Height Above Geoid - altitude = float(gps_segments[9]) - geoid_height = float(gps_segments[11]) - # Update Object Data - self.altitude = altitude - self.geoid_height = geoid_height - self._valid |= GGA - - # Update Object Data - self.satellites_in_use = satellites_in_use - self.hdop = hdop - return GGA - - def _gpgsa(self, gps_segments): # Parse GSA sentence - self._valid &= ~GSA - # Fix Type (None,2D or 3D) - fix_type = int(gps_segments[2]) - # Read All (up to 12) Available PRN Satellite Numbers - sats_used = [] - for sats in range(12): - sat_number_str = gps_segments[3 + sats] - if sat_number_str: - sat_number = int(sat_number_str) - sats_used.append(sat_number) - else: - break - # PDOP,HDOP,VDOP - pdop = float(gps_segments[15]) - hdop = float(gps_segments[16]) - vdop = float(gps_segments[17]) - - # If Fix is GOOD, update fix timestamp - if fix_type <= self._NO_FIX: # Deviation from Michael McCoy's logic. Is this right? - raise ValueError - self.satellites_used = sats_used - self.hdop = hdop - self.vdop = vdop - self.pdop = pdop - self._valid |= GSA - return GSA - - def _gpgsv(self, gps_segments): - # Parse Satellites in View (GSV) sentence. Updates no. of SV sentences, - # the no. of the last SV sentence parsed, and data on each satellite - # present in the sentence. - self._valid &= ~GSV - num_sv_sentences = int(gps_segments[1]) - current_sv_sentence = int(gps_segments[2]) - sats_in_view = int(gps_segments[3]) - - # Create a blank dict to store all the satellite data from this sentence in: - # satellite PRN is key, tuple containing telemetry is value - satellite_dict = dict() - - # Calculate Number of Satelites to pull data for and thus how many segment positions to read - if num_sv_sentences == current_sv_sentence: - sat_segment_limit = ((sats_in_view % 4) * 4) + 4 # Last sentence may have 1-4 satellites - else: - sat_segment_limit = 20 # Non-last sentences have 4 satellites and thus read up to position 20 - - # Try to recover data for up to 4 satellites in sentence - for sats in range(4, sat_segment_limit, 4): - - # If a PRN is present, grab satellite data - if gps_segments[sats]: - try: - sat_id = int(gps_segments[sats]) - except IndexError: - raise ValueError # Abandon - - try: # elevation can be null (no value) when not tracking - elevation = int(gps_segments[sats+1]) - except (ValueError,IndexError): - elevation = None - - try: # azimuth can be null (no value) when not tracking - azimuth = int(gps_segments[sats+2]) - except (ValueError,IndexError): - azimuth = None - - try: # SNR can be null (no value) when not tracking - snr = int(gps_segments[sats+3]) - except (ValueError,IndexError): - snr = None - # If no PRN is found, then the sentence has no more satellites to read - else: - break - - # Add Satellite Data to Sentence Dict - satellite_dict[sat_id] = (elevation, azimuth, snr) - - # Update Object Data - self._total_sv_sentences = num_sv_sentences - self._last_sv_sentence = current_sv_sentence - self.satellites_in_view = sats_in_view - - # For a new set of sentences, we either clear out the existing sat data or - # update it as additional SV sentences are parsed - if current_sv_sentence == 1: - self._satellite_data = satellite_dict - else: - self._satellite_data.update(satellite_dict) - # Flag that a msg has been received. Does not mean a full set of data is ready. - self._valid |= GSV - return GSV - - ######################################### - # User Interface Methods - ######################################### - - # Data Validity. On startup data may be invalid. During an outage it will be absent. - async def data_received(self, position=False, course=False, date=False, - altitude=False): - self._valid = 0 # Assume no messages at start - result = False - while not result: - result = True - await asyncio.sleep(1) # Successfully parsed messages set ._valid bits - if position and not self._valid & POSITION: - result = False - if date and not self._valid & DATE: - result = False - # After a hard reset the chip sends course messages even though no fix - # was received. Ignore this garbage until a fix is received. - if course: - if self._valid & COURSE: - if not self._valid & POSITION: - result = False - else: - result = False - if altitude and not self._valid & ALTITUDE: - result = False - - def latitude(self, coord_format=DD): - # Format Latitude Data Correctly - if coord_format == DD: - decimal_degrees = self._latitude[0] + (self._latitude[1] / 60) - return [decimal_degrees, self._latitude[2]] - elif coord_format == DMS: - mins = int(self._latitude[1]) - seconds = round((self._latitude[1] - mins) * 60) - return [self._latitude[0], mins, seconds, self._latitude[2]] - elif coord_format == DM: - return self._latitude - raise ValueError('Unknown latitude format.') - - def longitude(self, coord_format=DD): - # Format Longitude Data Correctly - if coord_format == DD: - decimal_degrees = self._longitude[0] + (self._longitude[1] / 60) - return [decimal_degrees, self._longitude[2]] - elif coord_format == DMS: - mins = int(self._longitude[1]) - seconds = round((self._longitude[1] - mins) * 60) - return [self._longitude[0], mins, seconds, self._longitude[2]] - elif coord_format == DM: - return self._longitude - raise ValueError('Unknown longitude format.') - - def speed(self, units=KNOT): - if units == KNOT: - return self._speed - if units == KPH: - return self._speed * 1.852 - if units == MPH: - return self._speed * 1.151 - raise ValueError('Unknown speed units.') - - async def get_satellite_data(self): - self._total_sv_sentences = 0 - while self._total_sv_sentences == 0: - await asyncio.sleep(0) - while self._total_sv_sentences > self._last_sv_sentence: - await asyncio.sleep(0) - return self._satellite_data - - def time_since_fix(self): # ms since last valid fix - if self._fix_time is None: - return -1 # No fix yet found - return self._time_diff(self._get_time(), self._fix_time) - - def compass_direction(self): # Return cardinal point as string. - from as_GPS_utils import compass_direction - return compass_direction(self) - - def latitude_string(self, coord_format=DM): - if coord_format == DD: - return '{:3.6f}° {:s}'.format(*self.latitude(DD)) - if coord_format == DMS: - return """{:3d}° {:2d}' {:2d}" {:s}""".format(*self.latitude(DMS)) - if coord_format == KML: - form_lat = self.latitude(DD) - return '{:4.6f}'.format(form_lat[0] if form_lat[1] == 'N' else -form_lat[0]) - return "{:3d}° {:3.4f}' {:s}".format(*self.latitude(coord_format)) - - def longitude_string(self, coord_format=DM): - if coord_format == DD: - return '{:3.6f}° {:s}'.format(*self.longitude(DD)) - if coord_format == DMS: - return """{:3d}° {:2d}' {:2d}" {:s}""".format(*self.longitude(DMS)) - if coord_format == KML: - form_long = self.longitude(DD) - return '{:4.6f}'.format(form_long[0] if form_long[1] == 'E' else -form_long[0]) - return "{:3d}° {:3.4f}' {:s}".format(*self.longitude(coord_format)) - - def speed_string(self, unit=KPH): - sform = '{:3.2f} {:s}' - speed = self.speed(unit) - if unit == MPH: - return sform.format(speed, 'mph') - elif unit == KNOT: - return sform.format(speed, 'knots') - return sform.format(speed, 'km/h') - - # Return local time (hrs: int, mins: int, secs:float) - @property - def local_time(self): - t = self.epoch_time + int(3600 * self.local_offset) - _, _, _, hrs, mins, secs, *_ = self._localtime(t) - return hrs, mins, secs - - @property - def date(self): - t = self.epoch_time + int(3600 * self.local_offset) - y, m, d, *_ = self._localtime(t) - return d, m, y - 2000 - - @property - def utc(self): - t = self.epoch_time - _, _, _, hrs, mins, secs, *_ = self._localtime(t) - return hrs, mins, secs - - def time_string(self, local=True): - hrs, mins, secs = self.local_time if local else self.utc - return '{:02d}:{:02d}:{:02d}'.format(hrs, mins, secs) - - def date_string(self, formatting=MDY): - from as_GPS_utils import date_string - return date_string(self, formatting) diff --git a/v2/gps/as_GPS_time.py b/v2/gps/as_GPS_time.py deleted file mode 100644 index 02028d1..0000000 --- a/v2/gps/as_GPS_time.py +++ /dev/null @@ -1,173 +0,0 @@ -# as_GPS_time.py Test scripts for as_tGPS.py read-only driver. -# Using GPS for precision timing and for calibrating Pyboard RTC - -# This is STM-specific: requires pyb module. -# Requires asyn.py from this repo. - -# Copyright (c) 2018 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -import uasyncio as asyncio -import pyb -import utime -import math -import asyn -import as_tGPS - -# Hardware assumptions. Change as required. -PPS_PIN = pyb.Pin.board.X3 -UART_ID = 4 - -print('Available tests:') -print('calibrate(minutes=5) Set and calibrate the RTC.') -print('drift(minutes=5) Repeatedly print the difference between RTC and GPS time.') -print('time(minutes=1) Print get_ms() and get_t_split values.') -print('usec(minutes=1) Measure accuracy of usec timer.') -print('Press ctrl-d to reboot after each test.') - -# Setup for tests. Red LED toggles on fix, blue on PPS interrupt. -async def setup(): - red = pyb.LED(1) - blue = pyb.LED(4) - uart = pyb.UART(UART_ID, 9600, read_buf_len=200) - sreader = asyncio.StreamReader(uart) - pps_pin = pyb.Pin(PPS_PIN, pyb.Pin.IN) - return as_tGPS.GPS_Timer(sreader, pps_pin, local_offset=1, - fix_cb=lambda *_: red.toggle(), - pps_cb=lambda *_: blue.toggle()) - -# Test terminator: task sets the passed event after the passed time. -async def killer(end_event, minutes): - print('Will run for {} minutes.'.format(minutes)) - await asyncio.sleep(minutes * 60) - end_event.set() - -# ******** Calibrate and set the Pyboard RTC ******** -async def do_cal(minutes): - gps = await setup() - await gps.calibrate(minutes) - gps.close() - -def calibrate(minutes=5): - loop = asyncio.get_event_loop() - loop.run_until_complete(do_cal(minutes)) - -# ******** Drift test ******** -# Every 10s print the difference between GPS time and RTC time -async def drift_test(terminate, gps): - dstart = await gps.delta() - while not terminate.is_set(): - dt = await gps.delta() - print('{} Delta {}μs'.format(gps.time_string(), dt)) - await asyncio.sleep(10) - return dt - dstart - -async def do_drift(minutes): - print('Setting up GPS.') - gps = await setup() - print('Waiting for time data.') - await gps.ready() - terminate = asyn.Event() - loop = asyncio.get_event_loop() - loop.create_task(killer(terminate, minutes)) - print('Setting RTC.') - await gps.set_rtc() - print('Measuring drift.') - change = await drift_test(terminate, gps) - ush = int(60 * change/minutes) - spa = int(ush * 365 * 24 / 1000000) - print('Rate of change {}μs/hr {}secs/year'.format(ush, spa)) - gps.close() - -def drift(minutes=5): - loop = asyncio.get_event_loop() - loop.run_until_complete(do_drift(minutes)) - -# ******** Time printing demo ******** -# Every 10s print the difference between GPS time and RTC time -async def do_time(minutes): - fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}' - print('Setting up GPS.') - gps = await setup() - print('Waiting for time data.') - await gps.ready() - print('Setting RTC.') - await gps.set_rtc() - terminate = asyn.Event() - loop = asyncio.get_event_loop() - loop.create_task(killer(terminate, minutes)) - while not terminate.is_set(): - await asyncio.sleep(1) - # In a precision app, get the time list without allocation: - t = gps.get_t_split() - print(fstr.format(gps.get_ms(), t[0], t[1], t[2], t[3])) - gps.close() - -def time(minutes=1): - loop = asyncio.get_event_loop() - loop.run_until_complete(do_time(minutes)) - -# ******** Measure accracy of μs clock ******** -# At 9600 baud see occasional lag of up to 3ms followed by similar lead. -# This implies that the ISR is being disabled for that period (~3 chars). -# SD 584μs typical. -# Test produces better numbers at 57600 baud (SD 112μs) -# and better still at 10Hz update rate (SD 34μs). Why?? -# Unsure why. Setting of .FULL_CHECK has no effect (as expected). - -# Callback occurs in interrupt context -us_acquired = None -def us_cb(my_gps, tick, led): - global us_acquired # Time of previous PPS edge in ticks_us() - if us_acquired is not None: - # Trigger event. Pass time between PPS measured by utime.ticks_us() - tick.set(utime.ticks_diff(my_gps.acquired, us_acquired)) - us_acquired = my_gps.acquired - led.toggle() - -# Setup initialises with above callback -async def us_setup(tick): - red = pyb.LED(1) - blue = pyb.LED(4) - uart = pyb.UART(UART_ID, 9600, read_buf_len=200) - sreader = asyncio.StreamReader(uart) - pps_pin = pyb.Pin(PPS_PIN, pyb.Pin.IN) - return as_tGPS.GPS_Timer(sreader, pps_pin, local_offset=1, - fix_cb=lambda *_: red.toggle(), - pps_cb=us_cb, pps_cb_args=(tick, blue)) - -async def do_usec(minutes): - tick = asyn.Event() - print('Setting up GPS.') - gps = await us_setup(tick) - print('Waiting for time data.') - await gps.ready() - max_us = 0 - min_us = 0 - sd = 0 - nsamples = 0 - count = 0 - terminate = asyn.Event() - loop = asyncio.get_event_loop() - loop.create_task(killer(terminate, minutes)) - while not terminate.is_set(): - await tick - usecs = tick.value() - tick.clear() - err = 1000000 - usecs - count += 1 - print('Timing discrepancy is {:4d}μs {}'.format(err, '(skipped)' if count < 3 else '')) - if count < 3: # Discard 1st two samples from statistics - continue # as these can be unrepresentative - max_us = max(max_us, err) - min_us = min(min_us, err) - sd += err * err - nsamples += 1 - # SD: apply Bessel's correction for infinite population - sd = int(math.sqrt(sd/(nsamples - 1))) - print('Timing discrepancy is: {:5d}μs max {:5d}μs min. Standard deviation {:4d}μs'.format(max_us, min_us, sd)) - gps.close() - -def usec(minutes=1): - loop = asyncio.get_event_loop() - loop.run_until_complete(do_usec(minutes)) diff --git a/v2/gps/as_GPS_utils.py b/v2/gps/as_GPS_utils.py deleted file mode 100644 index 7deb5d6..0000000 --- a/v2/gps/as_GPS_utils.py +++ /dev/null @@ -1,48 +0,0 @@ -# as_GPS_utils.py Extra functionality for as_GPS.py -# Put in separate file to minimise size of as_GPS.py for resource constrained -# systems. - -# Copyright (c) 2018 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file -from as_GPS import MDY, DMY, LONG - -_DIRECTIONS = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', - 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW') - -def compass_direction(gps): # Return cardinal point as string. - # Calculate the offset for a rotated compass - if gps.course >= 348.75: - offset_course = 360 - gps.course - else: - offset_course = gps.course + 11.25 - # Each compass point is separated by 22.5°, divide to find lookup value - return _DIRECTIONS[int(offset_course // 22.5)] - -_MONTHS = ('January', 'February', 'March', 'April', 'May', - 'June', 'July', 'August', 'September', 'October', - 'November', 'December') - -def date_string(gps, formatting=MDY): - day, month, year = gps.date - # Long Format January 1st, 2014 - if formatting == LONG: - dform = '{:s} {:2d}{:s}, 20{:2d}' - # Retrieve Month string from private set - month = _MONTHS[month - 1] - # Determine Date Suffix - if day in (1, 21, 31): - suffix = 'st' - elif day in (2, 22): - suffix = 'nd' - elif day in (3, 23): - suffix = 'rd' - else: - suffix = 'th' - return dform.format(month, day, suffix, year) - - dform = '{:02d}/{:02d}/{:02d}' - if formatting == DMY: - return dform.format(day, month, year) - elif formatting == MDY: # Default date format - return dform.format(month, day, year) - raise ValueError('Unknown date format.') diff --git a/v2/gps/as_rwGPS.py b/v2/gps/as_rwGPS.py deleted file mode 100644 index 2cb5540..0000000 --- a/v2/gps/as_rwGPS.py +++ /dev/null @@ -1,118 +0,0 @@ -# as_rwGPS.py Asynchronous device driver for GPS devices using a UART. -# Supports a limited subset of the PMTK command packets employed by the -# widely used MTK3329/MTK3339 chip. -# Sentence parsing based on MicropyGPS by Michael Calvin McCoy -# https://github.com/inmcm/micropyGPS - -# Copyright (c) 2018 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -import as_GPS -try: - from micropython import const -except ImportError: - const = lambda x : x - -HOT_START = const(1) -WARM_START = const(2) -COLD_START = const(3) -FULL_COLD_START = const(4) -STANDBY = const(5) -DEFAULT_SENTENCES = const(6) -VERSION = const(7) -ENABLE = const(8) -ANTENNA = const(9) -NO_ANTENNA = const(10) - -# Return CRC of a bytearray. -def _crc(sentence): - x = 1 - crc = 0 - while sentence[x] != ord('*'): - crc ^= sentence[x] - x += 1 - return crc # integer - - -class GPS(as_GPS.AS_GPS): - fixed_commands = {HOT_START: b'$PMTK101*32\r\n', - WARM_START: b'$PMTK102*31\r\n', - COLD_START: b'$PMTK103*30\r\n', - FULL_COLD_START: b'$PMTK104*37\r\n', - STANDBY: b'$PMTK161,0*28\r\n', - DEFAULT_SENTENCES: b'$PMTK314,-1*04\r\n', - VERSION: b'$PMTK605*31\r\n', - ENABLE: b'$PMTK414*33\r\n', - ANTENNA: b'$PGCMD,33,1*6C', - NO_ANTENNA: b'$PGCMD,33,0*6D', - } - - def __init__(self, sreader, swriter, local_offset=0, - fix_cb=lambda *_ : None, cb_mask=as_GPS.RMC, fix_cb_args=(), - msg_cb=lambda *_ : None, msg_cb_args=()): - super().__init__(sreader, local_offset, fix_cb, cb_mask, fix_cb_args) - self._swriter = swriter - self.version = None # Response to VERSION query - self.enabled = None # Response to ENABLE query - self.antenna = 0 # Response to ANTENNA. - self._msg_cb = msg_cb - self._msg_cb_args = msg_cb_args - - async def _send(self, sentence): - # Create a bytes object containing hex CRC - bcrc = '{:2x}'.format(_crc(sentence)).encode() - sentence[-4] = bcrc[0] # Fix up CRC bytes - sentence[-3] = bcrc[1] - await self._swriter.awrite(sentence) - - async def baudrate(self, value=9600): - if value not in (4800,9600,14400,19200,38400,57600,115200): - raise ValueError('Invalid baudrate {:d}.'.format(value)) - - sentence = bytearray('$PMTK251,{:d}*00\r\n'.format(value)) - await self._send(sentence) - - async def update_interval(self, ms=1000): - if ms < 100 or ms > 10000: - raise ValueError('Invalid update interval {:d}ms.'.format(ms)) - sentence = bytearray('$PMTK220,{:d}*00\r\n'.format(ms)) - await self._send(sentence) - self._update_ms = ms # Save for timing driver - - async def enable(self, *, gll=0, rmc=1, vtg=1, gga=1, gsa=1, gsv=5, chan=0): - fstr = '$PMTK314,{:d},{:d},{:d},{:d},{:d},{:d},0,0,0,0,0,0,0,0,0,0,0,0,{:d}*00\r\n' - sentence = bytearray(fstr.format(gll, rmc, vtg, gga, gsa, gsv, chan)) - await self._send(sentence) - - async def command(self, cmd): - if cmd not in self.fixed_commands: - raise ValueError('Invalid command {:s}.'.format(cmd)) - await self._swriter.awrite(self.fixed_commands[cmd]) - - # Should get 705 from VERSION 514 from ENABLE - def parse(self, segs): - if segs[0] == 'PMTK705': # Version response - self.version = segs[1:] - segs[0] = 'version' - self._msg_cb(self, segs, *self._msg_cb_args) - return True - - if segs[0] == 'PMTK514': - print('enabled segs', segs) - self.enabled = {'gll': segs[1], 'rmc': segs[2], 'vtg': segs[3], - 'gga': segs[4], 'gsa': segs[5], 'gsv': segs[6], - 'chan': segs[19]} - segs = ['enabled', self.enabled] - self._msg_cb(self, segs, *self._msg_cb_args) - return True - - if segs[0] == 'PGTOP': - self.antenna = segs[2] - segs = ['antenna', self.antenna] - self._msg_cb(self, segs, *self._msg_cb_args) - return True - - if segs[0][:4] == 'PMTK': - self._msg_cb(self, segs, *self._msg_cb_args) - return True - return False diff --git a/v2/gps/as_rwGPS_time.py b/v2/gps/as_rwGPS_time.py deleted file mode 100644 index 09c7f13..0000000 --- a/v2/gps/as_rwGPS_time.py +++ /dev/null @@ -1,237 +0,0 @@ -# as_rwGPS_time.py Test scripts for as_tGPS read-write driver. -# Using GPS for precision timing and for calibrating Pyboard RTC -# This is STM-specific: requires pyb module. -# Requires asyn.py from this repo. - -# Copyright (c) 2018 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -# See README.md notes re setting baudrates. In particular 9600 does not work. -# So these tests issue a factory reset on completion to restore the baudrate. - -# String sent for 9600: $PMTK251,9600*17\r\n -# Data has (for 38400): $PMTK251,38400*27 -# Sending: $PMTK251,38400*27\r\n' - -import uasyncio as asyncio -import pyb -import utime -import math -import asyn -import as_tGPS -import as_rwGPS - -# Hardware assumptions. Change as required. -PPS_PIN = pyb.Pin.board.X3 -UART_ID = 4 - -BAUDRATE = 57600 -UPDATE_INTERVAL = 100 -READ_BUF_LEN = 200 - -print('Available tests:') -print('calibrate(minutes=5) Set and calibrate the RTC.') -print('drift(minutes=5) Repeatedly print the difference between RTC and GPS time.') -print('time(minutes=1) Print get_ms() and get_t_split values.') -print('usec(minutes=1) Measure accuracy of usec timer.') -print('Press ctrl-d to reboot after each test.') - -# Initially use factory baudrate -uart = pyb.UART(UART_ID, 9600, read_buf_len=READ_BUF_LEN) - -async def shutdown(): - global gps - # Normally UART is already at BAUDRATE. But if last session didn't restore - # factory baudrate we can restore connectivity in the subsequent stuck - # session with ctrl-c. - uart.init(BAUDRATE) - await asyncio.sleep(0.5) - await gps.command(as_rwGPS.FULL_COLD_START) - print('Factory reset') - gps.close() # Stop ISR - #print('Restoring default baudrate (9600).') - #await gps.baudrate(9600) - #uart.init(9600) - #gps.close() # Stop ISR - #print('Restoring default 1s update rate.') - #await asyncio.sleep(0.5) - #await gps.update_interval(1000) # 1s update rate - #print('Restoring satellite data.') - #await gps.command(as_rwGPS.DEFAULT_SENTENCES) # Restore satellite data - -# Setup for tests. Red LED toggles on fix, blue on PPS interrupt. -async def setup(): - global uart, gps # For shutdown - red = pyb.LED(1) - blue = pyb.LED(4) - sreader = asyncio.StreamReader(uart) - swriter = asyncio.StreamWriter(uart, {}) - pps_pin = pyb.Pin(PPS_PIN, pyb.Pin.IN) - gps = as_tGPS.GPS_RWTimer(sreader, swriter, pps_pin, local_offset=1, - fix_cb=lambda *_: red.toggle(), - pps_cb=lambda *_: blue.toggle()) - gps.FULL_CHECK = False - await asyncio.sleep(2) - await gps.baudrate(BAUDRATE) - uart.init(BAUDRATE) - await asyncio.sleep(1) - await gps.enable(gsa=0, gsv=0) # Disable satellite data - await gps.update_interval(UPDATE_INTERVAL) - pstr = 'Baudrate {} update interval {}ms satellite messages disabled.' - print(pstr.format(BAUDRATE, UPDATE_INTERVAL)) - return gps - -# Test terminator: task sets the passed event after the passed time. -async def killer(end_event, minutes): - print('Will run for {} minutes.'.format(minutes)) - await asyncio.sleep(minutes * 60) - end_event.set() - -# ******** Calibrate and set the Pyboard RTC ******** -async def do_cal(minutes): - gps = await setup() - await gps.calibrate(minutes) - -def calibrate(minutes=5): - loop = asyncio.get_event_loop() - try: - loop.run_until_complete(do_cal(minutes)) - finally: - loop.run_until_complete(shutdown()) - -# ******** Drift test ******** -# Every 10s print the difference between GPS time and RTC time -async def drift_test(terminate, gps): - dstart = await gps.delta() - while not terminate.is_set(): - dt = await gps.delta() - print('{} Delta {}μs'.format(gps.time_string(), dt)) - await asyncio.sleep(10) - return dt - dstart - -async def do_drift(minutes): - global gps - print('Setting up GPS.') - gps = await setup() - print('Waiting for time data.') - await gps.ready() - print('Setting RTC.') - await gps.set_rtc() - print('Measuring drift.') - terminate = asyn.Event() - loop = asyncio.get_event_loop() - loop.create_task(killer(terminate, minutes)) - change = await drift_test(terminate, gps) - ush = int(60 * change/minutes) - spa = int(ush * 365 * 24 / 1000000) - print('Rate of change {}μs/hr {}secs/year'.format(ush, spa)) - -def drift(minutes=5): - loop = asyncio.get_event_loop() - try: - loop.run_until_complete(do_drift(minutes)) - finally: - loop.run_until_complete(shutdown()) - -# ******** Time printing demo ******** -# Every 10s print the difference between GPS time and RTC time -async def do_time(minutes): - global gps - fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}' - print('Setting up GPS.') - gps = await setup() - print('Waiting for time data.') - await gps.ready() - print('Setting RTC.') - await gps.set_rtc() - print('RTC is set.') - terminate = asyn.Event() - loop = asyncio.get_event_loop() - loop.create_task(killer(terminate, minutes)) - while not terminate.is_set(): - await asyncio.sleep(1) - # In a precision app, get the time list without allocation: - t = gps.get_t_split() - print(fstr.format(gps.get_ms(), t[0], t[1], t[2], t[3])) - -def time(minutes=1): - loop = asyncio.get_event_loop() - try: - loop.run_until_complete(do_time(minutes)) - finally: - loop.run_until_complete(shutdown()) - -# ******** Measure accracy of μs clock ******** -# Test produces better numbers at 57600 baud (SD 112μs) -# and better still at 10Hz update rate (SD 34μs). -# Unsure why. - -# Callback occurs in interrupt context -us_acquired = None # Time of previous PPS edge in ticks_us() -def us_cb(my_gps, tick, led): - global us_acquired - if us_acquired is not None: - # Trigger event. Pass time between PPS measured by utime.ticks_us() - tick.set(utime.ticks_diff(my_gps.acquired, us_acquired)) - us_acquired = my_gps.acquired - led.toggle() - -# Setup initialises with above callback -async def us_setup(tick): - global uart, gps # For shutdown - red = pyb.LED(1) - blue = pyb.LED(4) - sreader = asyncio.StreamReader(uart) - swriter = asyncio.StreamWriter(uart, {}) - pps_pin = pyb.Pin(PPS_PIN, pyb.Pin.IN) - gps = as_tGPS.GPS_RWTimer(sreader, swriter, pps_pin, local_offset=1, - fix_cb=lambda *_: red.toggle(), - pps_cb=us_cb, pps_cb_args=(tick, blue)) - gps.FULL_CHECK = False - await asyncio.sleep(2) - await gps.baudrate(BAUDRATE) - uart.init(BAUDRATE) - await asyncio.sleep(1) - await gps.enable(gsa=0, gsv=0) # Disable satellite data - await gps.update_interval(UPDATE_INTERVAL) - pstr = 'Baudrate {} update interval {}ms satellite messages disabled.' - print(pstr.format(BAUDRATE, UPDATE_INTERVAL)) - -async def do_usec(minutes): - global gps - tick = asyn.Event() - print('Setting up GPS.') - await us_setup(tick) - print('Waiting for time data.') - await gps.ready() - max_us = 0 - min_us = 0 - sd = 0 - nsamples = 0 - count = 0 - terminate = asyn.Event() - loop = asyncio.get_event_loop() - loop.create_task(killer(terminate, minutes)) - while not terminate.is_set(): - await tick - usecs = tick.value() - tick.clear() - err = 1000000 - usecs - count += 1 - print('Timing discrepancy is {:4d}μs {}'.format(err, '(skipped)' if count < 3 else '')) - if count < 3: # Discard 1st two samples from statistics - continue # as these can be unrepresentative - max_us = max(max_us, err) - min_us = min(min_us, err) - sd += err * err - nsamples += 1 - # SD: apply Bessel's correction for infinite population - sd = int(math.sqrt(sd/(nsamples - 1))) - print('Timing discrepancy is: {:5d}μs max {:5d}μs min. Standard deviation {:4d}μs'.format(max_us, min_us, sd)) - -def usec(minutes=1): - loop = asyncio.get_event_loop() - try: - loop.run_until_complete(do_usec(minutes)) - finally: - loop.run_until_complete(shutdown()) diff --git a/v2/gps/as_tGPS.py b/v2/gps/as_tGPS.py deleted file mode 100644 index df7c2aa..0000000 --- a/v2/gps/as_tGPS.py +++ /dev/null @@ -1,241 +0,0 @@ -# as_tGPS.py Using GPS for precision timing and for calibrating Pyboard RTC -# This is STM-specific: requires pyb module. -# Hence not as RAM-critical as as_GPS - -# Copyright (c) 2018 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file -# TODO Test machine version. Replace LED with callback. Update tests and doc. - -import uasyncio as asyncio -import machine -try: - import pyb - on_pyboard = True - rtc = pyb.RTC() -except ImportError: - on_pyboard = False -import utime -import micropython -import gc -import as_GPS -import as_rwGPS - -micropython.alloc_emergency_exception_buf(100) - -# Convenience function. Return RTC seconds since midnight as float -def rtc_secs(): - if not on_pyboard: - raise OSError('Only available on STM targets.') - dt = rtc.datetime() - return 3600*dt[4] + 60*dt[5] + dt[6] + (255 - dt[7])/256 - -# Constructor for GPS_Timer class -def gps_ro_t_init(self, sreader, pps_pin, local_offset=0, - fix_cb=lambda *_ : None, cb_mask=as_GPS.RMC, fix_cb_args=(), - pps_cb=lambda *_ : None, pps_cb_args=()): - as_GPS.AS_GPS.__init__(self, sreader, local_offset, fix_cb, cb_mask, fix_cb_args) - self.setup(pps_pin, pps_cb, pps_cb_args) - -# Constructor for GPS_RWTimer class -def gps_rw_t_init(self, sreader, swriter, pps_pin, local_offset=0, - fix_cb=lambda *_ : None, cb_mask=as_GPS.RMC, fix_cb_args=(), - msg_cb=lambda *_ : None, msg_cb_args=(), - pps_cb=lambda *_ : None, pps_cb_args=()): - as_rwGPS.GPS.__init__(self, sreader, swriter, local_offset, fix_cb, cb_mask, fix_cb_args, - msg_cb, msg_cb_args) - self.setup(pps_pin, pps_cb, pps_cb_args) - -class GPS_Tbase(): - def setup(self, pps_pin, pps_cb, pps_cb_args): - self._pps_pin = pps_pin - self._pps_cb = pps_cb - self._pps_cb_args = pps_cb_args - self.msecs = None # Integer time in ms since midnight at last PPS - self.t_ms = 0 # ms since midnight - self.acquired = None # Value of ticks_us at edge of PPS - self._rtc_set = False # Set RTC flag - self._rtcbuf = [0]*8 # Buffer for RTC setting - self._time = [0]*4 # get_t_split() time buffer. - loop = asyncio.get_event_loop() - loop.create_task(self._start()) - - async def _start(self): - await self.data_received(date=True) - self._pps_pin.irq(self._isr, trigger = machine.Pin.IRQ_RISING) - - def close(self): - self._pps_pin.irq(None) - - # If update rate > 1Hz, when PPS edge occurs the last RMC message will have - # a nonzero ms value. Need to set RTC to 1 sec after the last 1 second boundary - def _isr(self, _): - acquired = utime.ticks_us() # Save time of PPS - # Time in last NMEA sentence was time of last PPS. - # Reduce to integer secs since midnight local time. - isecs = (self.epoch_time + int(3600*self.local_offset)) % 86400 - # ms since midnight (28 bits). Add in any ms in RMC data - msecs = isecs * 1000 + self.msecs - # This PPS is presumed to be one update later - msecs += self._update_ms - if msecs >= 86400000: # Next PPS will deal with rollover - return - if self.t_ms == msecs: # No RMC message has arrived: nothing to do - return - self.t_ms = msecs # Current time in ms past midnight - self.acquired = acquired - # Set RTC if required and if last RMC indicated a 1 second boundary - if self._rtc_set: - # Time as int(seconds) in last NMEA sentence. Earlier test ensures - # no rollover when we add 1. - self._rtcbuf[6] = (isecs + 1) % 60 - rtc.datetime(self._rtcbuf) - self._rtc_set = False - # Could be an outage here, so PPS arrives many secs after last sentence - # Is this right? Does PPS continue during outage? - self._pps_cb(self, *self._pps_cb_args) - - # Called when base class updates the epoch_time. - # Need local time for setting Pyboard RTC in interrupt context - def _dtset(self, wday): - t = self.epoch_time + int(3600 * self.local_offset) - y, m, d, hrs, mins, secs, *_ = self._localtime(t) - self._rtcbuf[0] = y - self._rtcbuf[1] = m - self._rtcbuf[2] = d - self._rtcbuf[3] = wday - self._rtcbuf[4] = hrs - self._rtcbuf[5] = mins - self._rtcbuf[6] = secs - - # Subsecs register is read-only. So need to set RTC on PPS leading edge. - # Set flag and let ISR set the RTC. Pause until done. - async def set_rtc(self): - if not on_pyboard: - raise OSError('Only available on STM targets.') - self._rtc_set = True - while self._rtc_set: - await asyncio.sleep_ms(250) - - # Value of RTC time at current instant. This is a notional arbitrary - # precision integer in μs since Y2K. Notional because RTC is set to - # local time. - def _get_rtc_usecs(self): - y, m, d, weekday, hrs, mins, secs, subsecs = rtc.datetime() - tim = 1000000 * utime.mktime((y, m, d, hrs, mins, secs, weekday - 1, 0)) - return tim + ((1000000 * (255 - subsecs)) >> 8) - - # Return no. of μs RTC leads GPS. Done by comparing times at the instant of - # PPS leading edge. - async def delta(self): - if not on_pyboard: - raise OSError('Only available on STM targets.') - rtc_time, gps_time = await self._await_pps() # μs since Y2K at time of latest PPS - return rtc_time - gps_time - - # Pause until PPS interrupt occurs. Then wait for an RTC subsecond change. - # Read the RTC time in μs since Y2K and adjust to give the time the RTC - # (notionally) would have read at the PPS leading edge. - async def _await_pps(self): - t0 = self.acquired - while self.acquired == t0: # Busy-wait on PPS interrupt: not time-critical - await asyncio.sleep_ms(0) # because acquisition time stored in ISR. - gc.collect() # Time-critical code follows - st = rtc.datetime()[7] - while rtc.datetime()[7] == st: # Wait for RTC to change (4ms max) - pass - dt = utime.ticks_diff(utime.ticks_us(), self.acquired) - trtc = self._get_rtc_usecs() - dt # Read RTC now and adjust for PPS edge - tgps = 1000000 * (self.epoch_time + 3600*self.local_offset + 1) - return trtc, tgps - - # Non-realtime calculation of calibration factor. times are in μs - def _calculate(self, gps_start, gps_end, rtc_start, rtc_end): - # Duration (μs) between PPS edges - pps_delta = (gps_end - gps_start) - # Duration (μs) between PPS edges as measured by RTC and corrected - rtc_delta = (rtc_end - rtc_start) - ppm = (1000000 * (rtc_delta - pps_delta)) / pps_delta # parts per million - return int(-ppm/0.954) - - # Measure difference between RTC and GPS rate and return calibration factor - # If 3 successive identical results are within 1 digit the outcome is considered - # valid and the coro quits. - async def _getcal(self, minutes=5): - if minutes < 1: - raise ValueError('minutes must be >= 1') - results = [0, 0, 0] # Last 3 cal results - idx = 0 # Index into above circular buffer - nresults = 0 # Count of results - rtc.calibration(0) # Clear existing RTC calibration - await self.set_rtc() - # Wait for PPS, then RTC 1/256 second change. Return the time the RTC - # would have measured at instant of PPS (notional μs since Y2K). Also - # GPS time at the same instant. - rtc_start, gps_start = await self._await_pps() - for n in range(minutes): - for _ in range(6): # Try every 10s - await asyncio.sleep(10) - # Get RTC time at instant of PPS - rtc_end, gps_end = await self._await_pps() - cal = self._calculate(gps_start, gps_end, rtc_start, rtc_end) - print('Mins {:d} cal factor {:d}'.format(n + 1, cal)) - results[idx] = cal - idx += 1 - idx %= len(results) - nresults += 1 - if nresults >= 4 and (abs(max(results) - min(results)) <= 1): - return round(sum(results)/len(results)) - return cal - - # Pause until time/date message received and 1st PPS interrupt has occurred. - async def ready(self): - while self.acquired is None: - await asyncio.sleep(1) - - async def calibrate(self, minutes=5): - if not on_pyboard: - raise OSError('Only available on STM targets.') - print('Waiting for GPS startup.') - await self.ready() - print('Waiting up to {} minutes to acquire calibration factor...'.format(minutes)) - cal = await self._getcal(minutes) - if cal <= 512 and cal >= -511: - rtc.calibration(cal) - print('Pyboard RTC is calibrated. Factor is {:d}.'.format(cal)) - else: - print('Calibration factor {:d} is out of range.'.format(cal)) - - # User interface functions: accurate GPS time. - # Return GPS time in ms since midnight (small int on 32 bit h/w). - # No allocation. - def get_ms(self): - state = machine.disable_irq() - t = self.t_ms - acquired = self.acquired - machine.enable_irq(state) - return t + utime.ticks_diff(utime.ticks_us(), acquired) // 1000 - - # Return accurate GPS time of day (hrs: int, mins: int, secs: int, μs: int) - # The ISR can skip an update of .secs if a day rollover would occur. Next - # RMC handles this, so if updates are at 1s intervals the subsequent ISR - # will see hms = 0, 0, 1 and a value of .acquired > 1000000. - # Even at the slowest update rate of 10s this can't overflow into minutes. - def get_t_split(self): - state = machine.disable_irq() - t = self.t_ms - acquired = self.acquired - machine.enable_irq(state) - isecs, ims = divmod(t, 1000) # Get integer secs and ms - x, secs = divmod(isecs, 60) - hrs, mins = divmod(x, 60) - dt = utime.ticks_diff(utime.ticks_us(), acquired) # μs to time now - ds, us = divmod(dt, 1000000) - # If dt > 1e6 can add to secs without risk of rollover: see above. - self._time[0] = hrs - self._time[1] = mins - self._time[2] = secs + ds - self._time[3] = us + ims*1000 - return self._time - -GPS_Timer = type('GPS_Timer', (GPS_Tbase, as_GPS.AS_GPS), {'__init__': gps_ro_t_init}) -GPS_RWTimer = type('GPS_RWTimer', (GPS_Tbase, as_rwGPS.GPS), {'__init__': gps_rw_t_init}) diff --git a/v2/gps/ast_pb.py b/v2/gps/ast_pb.py deleted file mode 100644 index b9498bf..0000000 --- a/v2/gps/ast_pb.py +++ /dev/null @@ -1,101 +0,0 @@ -# ast_pb.py -# Basic test/demo of AS_GPS class (asynchronous GPS device driver) -# Runs on a Pyboard with GPS data on pin X2. -# Copyright (c) Peter Hinch 2018-2020 -# Released under the MIT License (MIT) - see LICENSE file -# Test asynchronous GPS device driver as_pyGPS - -import pyb -import uasyncio as asyncio -import aswitch -import as_GPS - -red = pyb.LED(1) -green = pyb.LED(2) -ntimeouts = 0 - -def callback(gps, _, timer): - red.toggle() - green.on() - timer.trigger(10000) - -def timeout(): - global ntimeouts - green.off() - ntimeouts += 1 - -# Print satellite data every 10s -async def sat_test(gps): - while True: - d = await gps.get_satellite_data() - print('***** SATELLITE DATA *****') - for i in d: - print(i, d[i]) - print() - await asyncio.sleep(10) - -# Print statistics every 30s -async def stats(gps): - while True: - await asyncio.sleep(30) - print('***** STATISTICS *****') - print('Outages:', ntimeouts) - print('Sentences Found:', gps.clean_sentences) - print('Sentences Parsed:', gps.parsed_sentences) - print('CRC_Fails:', gps.crc_fails) - print() - -# Print navigation data every 4s -async def navigation(gps): - while True: - await asyncio.sleep(4) - await gps.data_received(position=True) - print('***** NAVIGATION DATA *****') - print('Data is Valid:', gps._valid) - print('Longitude:', gps.longitude(as_GPS.DD)) - print('Latitude', gps.latitude(as_GPS.DD)) - print() - -async def course(gps): - while True: - await asyncio.sleep(4) - await gps.data_received(course=True) - print('***** COURSE DATA *****') - print('Data is Valid:', gps._valid) - print('Speed:', gps.speed_string(as_GPS.MPH)) - print('Course', gps.course) - print('Compass Direction:', gps.compass_direction()) - print() - -async def date(gps): - while True: - await asyncio.sleep(4) - await gps.data_received(date=True) - print('***** DATE AND TIME *****') - print('Data is Valid:', gps._valid) - print('UTC time:', gps.utc) - print('Local time:', gps.local_time) - print('Date:', gps.date_string(as_GPS.LONG)) - print() - -async def gps_test(): - print('Initialising') - # Adapt for other MicroPython hardware - uart = pyb.UART(4, 9600, read_buf_len=200) - # read_buf_len is precautionary: code runs reliably without it.) - sreader = asyncio.StreamReader(uart) - timer = aswitch.Delay_ms(timeout) - sentence_count = 0 - gps = as_GPS.AS_GPS(sreader, local_offset=1, fix_cb=callback, fix_cb_args=(timer,)) - loop = asyncio.get_event_loop() - print('awaiting first fix') - loop.create_task(sat_test(gps)) - loop.create_task(stats(gps)) - loop.create_task(navigation(gps)) - loop.create_task(course(gps)) - loop.create_task(date(gps)) - - -loop = asyncio.get_event_loop() -loop.create_task(gps_test()) -loop.run_forever() diff --git a/v2/gps/ast_pbrw.py b/v2/gps/ast_pbrw.py deleted file mode 100644 index 2fdda30..0000000 --- a/v2/gps/ast_pbrw.py +++ /dev/null @@ -1,173 +0,0 @@ -# ast_pb.py -# Basic test/demo of AS_GPS class (asynchronous GPS device driver) -# Runs on a Pyboard with GPS data on pin X2. -# Copyright (c) Peter Hinch 2018 -# Released under the MIT License (MIT) - see LICENSE file -# Test asynchronous GPS device driver as_rwGPS - -# LED's: -# Green indicates data is being received. -# Red toggles on RMC message received. -# Yellow and blue: coroutines have 4s loop delay. -# Yellow toggles on position reading. -# Blue toggles on date valid. - -import pyb -import uasyncio as asyncio -import aswitch -import as_GPS -import as_rwGPS - -# Avoid multiple baudrates. Tests use 9600 or 19200 only. -BAUDRATE = 19200 -red, green, yellow = pyb.LED(1), pyb.LED(2), pyb.LED(3) -ntimeouts = 0 - -def callback(gps, _, timer): - red.toggle() - green.on() - timer.trigger(10000) # Outage is declared after 10s - -def cb_timeout(): - global ntimeouts - green.off() - ntimeouts += 1 - -def message_cb(gps, segs): - print('Message received:', segs) - -# Print satellite data every 10s -async def sat_test(gps): - while True: - d = await gps.get_satellite_data() - print('***** SATELLITE DATA *****') - print('Data is Valid:', hex(gps._valid)) - for i in d: - print(i, d[i]) - print() - await asyncio.sleep(10) - -# Print statistics every 30s -async def stats(gps): - while True: - await gps.data_received(position=True) # Wait for a valid fix - await asyncio.sleep(30) - print('***** STATISTICS *****') - print('Outages:', ntimeouts) - print('Sentences Found:', gps.clean_sentences) - print('Sentences Parsed:', gps.parsed_sentences) - print('CRC_Fails:', gps.crc_fails) - print('Antenna status:', gps.antenna) - print('Firmware vesrion:', gps.version) - print('Enabled sentences:', gps.enabled) - print() - -# Print navigation data every 4s -async def navigation(gps): - while True: - await asyncio.sleep(4) - await gps.data_received(position=True) - yellow.toggle() - print('***** NAVIGATION DATA *****') - print('Data is Valid:', hex(gps._valid)) - print('Longitude:', gps.longitude(as_GPS.DD)) - print('Latitude', gps.latitude(as_GPS.DD)) - print() - -async def course(gps): - while True: - await asyncio.sleep(4) - await gps.data_received(course=True) - print('***** COURSE DATA *****') - print('Data is Valid:', hex(gps._valid)) - print('Speed:', gps.speed_string(as_GPS.MPH)) - print('Course', gps.course) - print('Compass Direction:', gps.compass_direction()) - print() - -async def date(gps): - while True: - await asyncio.sleep(4) - await gps.data_received(date=True) - print('***** DATE AND TIME *****') - print('Data is Valid:', hex(gps._valid)) - print('UTC Time:', gps.utc) - print('Local time:', gps.local_time) - print('Date:', gps.date_string(as_GPS.LONG)) - print() - -async def change_status(gps, uart): - await asyncio.sleep(10) - print('***** Changing status. *****') - await gps.baudrate(BAUDRATE) - uart.init(BAUDRATE) - print('***** baudrate 19200 *****') - await asyncio.sleep(5) # Ensure baudrate is sorted - print('***** Query VERSION *****') - await gps.command(as_rwGPS.VERSION) - await asyncio.sleep(10) - print('***** Query ENABLE *****') - await gps.command(as_rwGPS.ENABLE) - await asyncio.sleep(10) # Allow time for 1st report - await gps.update_interval(2000) - print('***** Update interval 2s *****') - await asyncio.sleep(10) - await gps.enable(gsv = False, chan = False) - print('***** Disable satellite in view and channel messages *****') - await asyncio.sleep(10) - print('***** Query ENABLE *****') - await gps.command(as_rwGPS.ENABLE) - -# See README.md re antenna commands -# await asyncio.sleep(10) -# await gps.command(as_rwGPS.ANTENNA) -# print('***** Antenna reports requested *****') -# await asyncio.sleep(60) -# await gps.command(as_rwGPS.NO_ANTENNA) -# print('***** Antenna reports turned off *****') -# await asyncio.sleep(10) - -async def gps_test(): - global gps, uart # For shutdown - print('Initialising') - # Adapt UART instantiation for other MicroPython hardware - uart = pyb.UART(4, 9600, read_buf_len=200) - # read_buf_len is precautionary: code runs reliably without it. - sreader = asyncio.StreamReader(uart) - swriter = asyncio.StreamWriter(uart, {}) - timer = aswitch.Delay_ms(cb_timeout) - sentence_count = 0 - gps = as_rwGPS.GPS(sreader, swriter, local_offset=1, fix_cb=callback, - fix_cb_args=(timer,), msg_cb = message_cb) - await asyncio.sleep(2) - await gps.command(as_rwGPS.DEFAULT_SENTENCES) - print('Set sentence frequencies to default') - #await gps.command(as_rwGPS.FULL_COLD_START) - #print('Performed FULL_COLD_START') - print('awaiting first fix') - loop = asyncio.get_event_loop() - loop.create_task(sat_test(gps)) - loop.create_task(stats(gps)) - loop.create_task(navigation(gps)) - loop.create_task(course(gps)) - loop.create_task(date(gps)) - await gps.data_received(True, True, True, True) # all messages - loop.create_task(change_status(gps, uart)) - -async def shutdown(): - # Normally UART is already at BAUDRATE. But if last session didn't restore - # factory baudrate we can restore connectivity in the subsequent stuck - # session with ctrl-c. - uart.init(BAUDRATE) - await asyncio.sleep(1) - await gps.command(as_rwGPS.FULL_COLD_START) - print('Factory reset') - #print('Restoring default baudrate.') - #await gps.baudrate(9600) - -loop = asyncio.get_event_loop() -loop.create_task(gps_test()) -try: - loop.run_forever() -finally: - loop.run_until_complete(shutdown()) diff --git a/v2/gps/astests.py b/v2/gps/astests.py deleted file mode 100755 index 6bfbebd..0000000 --- a/v2/gps/astests.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env python3.5 -# -*- coding: utf-8 -*- - -# astests.py -# Tests for AS_GPS module (asynchronous GPS device driver) -# Based on tests for MicropyGPS by Michael Calvin McCoy -# https://github.com/inmcm/micropyGPS - -# Copyright (c) 2018 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file -# Run under CPython 3.5+ or MicroPython - -import as_GPS -try: - import uasyncio as asyncio -except ImportError: - import asyncio - -async def run(): - sentence_count = 0 - - test_RMC = ['$GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62\n', - '$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\n', - '$GPRMC,225446,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E*68\n', - '$GPRMC,180041.896,A,3749.1851,N,08338.7891,W,001.9,154.9,240911,,,A*7A\n', - '$GPRMC,180049.896,A,3749.1808,N,08338.7869,W,001.8,156.3,240911,,,A*70\n', - '$GPRMC,092751.000,A,5321.6802,N,00630.3371,W,0.06,31.66,280511,,,A*45\n'] - - test_VTG = ['$GPVTG,232.9,T,,M,002.3,N,004.3,K,A*01\n'] - test_GGA = ['$GPGGA,180050.896,3749.1802,N,08338.7865,W,1,07,1.1,397.4,M,-32.5,M,,0000*6C\n'] - test_GSA = ['$GPGSA,A,3,07,11,28,24,26,08,17,,,,,,2.0,1.1,1.7*37\n', - '$GPGSA,A,3,07,02,26,27,09,04,15,,,,,,1.8,1.0,1.5*33\n'] - test_GSV = ['$GPGSV,3,1,12,28,72,355,39,01,52,063,33,17,51,272,44,08,46,184,38*74\n', - '$GPGSV,3,2,12,24,42,058,33,11,34,053,33,07,20,171,40,20,15,116,*71\n', - '$GPGSV,3,3,12,04,12,204,34,27,11,324,35,32,11,089,,26,10,264,40*7B\n', - '$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74\n', - '$GPGSV,3,2,11,14,25,170,00,16,57,208,39,18,67,296,40,19,40,246,00*74\n', - '$GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D\n', - '$GPGSV,4,1,14,22,81,349,25,14,64,296,22,18,54,114,21,51,40,212,*7D\n', - '$GPGSV,4,2,14,24,30,047,22,04,22,312,26,31,22,204,,12,19,088,23*72\n', - '$GPGSV,4,3,14,25,17,127,18,21,16,175,,11,09,315,16,19,05,273,*72\n', - '$GPGSV,4,4,14,32,05,303,,15,02,073,*7A\n'] - test_GLL = ['$GPGLL,3711.0942,N,08671.4472,W,000812.000,A,A*46\n', - '$GPGLL,4916.45,N,12311.12,W,225444,A,*1D\n', - '$GPGLL,4250.5589,S,14718.5084,E,092204.999,A*2D\n', - '$GPGLL,0000.0000,N,00000.0000,E,235947.000,V*2D\n'] - - my_gps = as_GPS.AS_GPS(None) - sentence = '' - for sentence in test_RMC: - my_gps._valid = 0 - sentence_count += 1 - sentence = await my_gps._update(sentence) - if sentence is None: - print('RMC sentence is invalid.') - else: - print('Parsed a', sentence, 'Sentence') - print('Longitude:', my_gps.longitude()) - print('Latitude', my_gps.latitude()) - print('UTC Timestamp:', my_gps.utc) - print('Speed:', my_gps.speed()) - print('Date Stamp:', my_gps.date) - print('Course', my_gps.course) - print('Data is Valid:', bool(my_gps._valid & 1)) - print('Compass Direction:', my_gps.compass_direction()) - print('') - - for sentence in test_GLL: - my_gps._valid = 0 - sentence_count += 1 - sentence = await my_gps._update(sentence) - if sentence is None: - print('GLL sentence is invalid.') - else: - print('Parsed a', sentence, 'Sentence') - print('Longitude:', my_gps.longitude()) - print('Latitude', my_gps.latitude()) - print('UTC Timestamp:', my_gps.utc) - print('Data is Valid:', bool(my_gps._valid & 2)) - print('') - - for sentence in test_VTG: - my_gps._valid = 0 - sentence_count += 1 - sentence = await my_gps._update(sentence) - if sentence is None: - print('VTG sentence is invalid.') - else: - print('Parsed a', sentence, 'Sentence') - print('Speed:', my_gps.speed()) - print('Course', my_gps.course) - print('Compass Direction:', my_gps.compass_direction()) - print('Data is Valid:', bool(my_gps._valid & 4)) - print('') - - for sentence in test_GGA: - my_gps._valid = 0 - sentence_count += 1 - sentence = await my_gps._update(sentence) - if sentence is None: - print('GGA sentence is invalid.') - else: - print('Parsed a', sentence, 'Sentence') - print('Longitude', my_gps.longitude()) - print('Latitude', my_gps.latitude()) - print('UTC Timestamp:', my_gps.utc) - print('Altitude:', my_gps.altitude) - print('Height Above Geoid:', my_gps.geoid_height) - print('Horizontal Dilution of Precision:', my_gps.hdop) - print('Satellites in Use by Receiver:', my_gps.satellites_in_use) - print('Data is Valid:', bool(my_gps._valid & 8)) - print('') - - for sentence in test_GSA: - my_gps._valid = 0 - sentence_count += 1 - sentence = await my_gps._update(sentence) - if sentence is None: - print('GSA sentence is invalid.') - else: - print('Parsed a', sentence, 'Sentence') - print('Satellites Used', my_gps.satellites_used) - print('Horizontal Dilution of Precision:', my_gps.hdop) - print('Vertical Dilution of Precision:', my_gps.vdop) - print('Position Dilution of Precision:', my_gps.pdop) - print('Data is Valid:', bool(my_gps._valid & 16)) - print('') - - for sentence in test_GSV: - my_gps._valid = 0 - sentence_count += 1 - sentence = await my_gps._update(sentence) - if sentence is None: - print('GSV sentence is invalid.') - else: - print('Parsed a', sentence, 'Sentence') - print('SV Sentences Parsed', my_gps._last_sv_sentence) - print('SV Sentences in Total', my_gps._total_sv_sentences) - print('# of Satellites in View:', my_gps.satellites_in_view) - print('Data is Valid:', bool(my_gps._valid & 32)) - data_valid = my_gps._total_sv_sentences > 0 and my_gps._total_sv_sentences == my_gps._last_sv_sentence - print('Is Satellite Data Valid?:', data_valid) - if data_valid: - print('Satellite Data:', my_gps._satellite_data) - print('Satellites Visible:', list(my_gps._satellite_data.keys())) - print('') - - print("Pretty Print Examples:") - print('Latitude (degs):', my_gps.latitude_string(as_GPS.DD)) - print('Longitude (degs):', my_gps.longitude_string(as_GPS.DD)) - print('Latitude (dms):', my_gps.latitude_string(as_GPS.DMS)) - print('Longitude (dms):', my_gps.longitude_string(as_GPS.DMS)) - print('Latitude (kml):', my_gps.latitude_string(as_GPS.KML)) - print('Longitude (kml):', my_gps.longitude_string(as_GPS.KML)) - print('Latitude (degs, mins):', my_gps.latitude_string()) - print('Longitude (degs, mins):', my_gps.longitude_string()) - print('Speed:', my_gps.speed_string(as_GPS.KPH), 'or', - my_gps.speed_string(as_GPS.MPH), 'or', - my_gps.speed_string(as_GPS.KNOT)) - print('Date (Long Format):', my_gps.date_string(as_GPS.LONG)) - print('Date (Short D/M/Y Format):', my_gps.date_string(as_GPS.DMY)) - print('Date (Short M/D/Y Format):', my_gps.date_string(as_GPS.MDY)) - print('Time:', my_gps.time_string()) - print() - - print('### Final Results ###') - print('Sentences Attempted:', sentence_count) - print('Sentences Found:', my_gps.clean_sentences) - print('Sentences Parsed:', my_gps.parsed_sentences) - print('Unsupported sentences:', my_gps.unsupported_sentences) - print('CRC_Fails:', my_gps.crc_fails) - -def run_tests(): - loop = asyncio.get_event_loop() - loop.run_until_complete(run()) - -if __name__ == "__main__": - run_tests() diff --git a/v2/gps/astests_pyb.py b/v2/gps/astests_pyb.py deleted file mode 100755 index 5846fe9..0000000 --- a/v2/gps/astests_pyb.py +++ /dev/null @@ -1,150 +0,0 @@ -# astests_pyb.py - -# Tests for AS_GPS module. Emulates a GPS unit using a UART loopback. -# Run on a Pyboard with X1 and X2 linked -# Tests for AS_GPS module (asynchronous GPS device driver) -# Based on tests for MicropyGPS by Michael Calvin McCoy -# https://github.com/inmcm/micropyGPS - -# Copyright (c) 2018 Peter Hinch -# Released under the MIT License (MIT) - see LICENSE file - -import as_GPS -from machine import UART -import uasyncio as asyncio - -def callback(gps, _, arg): - print('Fix callback. Time:', gps.utc, arg) - -async def run_tests(): - uart = UART(4, 9600, read_buf_len=200) - swriter = asyncio.StreamWriter(uart, {}) - sreader = asyncio.StreamReader(uart) - sentence_count = 0 - - test_RMC = ['$GPRMC,180041.896,A,3749.1851,N,08338.7891,W,001.9,154.9,240911,,,A*7A\n', - '$GPRMC,180049.896,A,3749.1808,N,08338.7869,W,001.8,156.3,240911,,,A*70\n', - '$GPRMC,092751.000,A,5321.6802,N,00630.3371,W,0.06,31.66,280511,,,A*45\n'] - - test_VTG = ['$GPVTG,232.9,T,,M,002.3,N,004.3,K,A*01\n'] - test_GGA = ['$GPGGA,180050.896,3749.1802,N,08338.7865,W,1,07,1.1,397.4,M,-32.5,M,,0000*6C\n'] - test_GSA = ['$GPGSA,A,3,07,11,28,24,26,08,17,,,,,,2.0,1.1,1.7*37\n', - '$GPGSA,A,3,07,02,26,27,09,04,15,,,,,,1.8,1.0,1.5*33\n'] - test_GSV = ['$GPGSV,3,1,12,28,72,355,39,01,52,063,33,17,51,272,44,08,46,184,38*74\n', - '$GPGSV,3,2,12,24,42,058,33,11,34,053,33,07,20,171,40,20,15,116,*71\n', - '$GPGSV,3,3,12,04,12,204,34,27,11,324,35,32,11,089,,26,10,264,40*7B\n', - '$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74\n', - '$GPGSV,3,2,11,14,25,170,00,16,57,208,39,18,67,296,40,19,40,246,00*74\n', - '$GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D\n', - '$GPGSV,4,1,14,22,81,349,25,14,64,296,22,18,54,114,21,51,40,212,*7D\n', - '$GPGSV,4,2,14,24,30,047,22,04,22,312,26,31,22,204,,12,19,088,23*72\n', - '$GPGSV,4,3,14,25,17,127,18,21,16,175,,11,09,315,16,19,05,273,*72\n', - '$GPGSV,4,4,14,32,05,303,,15,02,073,*7A\n'] - test_GLL = ['$GPGLL,3711.0942,N,08671.4472,W,000812.000,A,A*46\n', - '$GPGLL,4916.45,N,12311.12,W,225444,A,*1D\n', - '$GPGLL,4250.5589,S,14718.5084,E,092204.999,A*2D\n', - '$GPGLL,4250.5589,S,14718.5084,E,092204.999,A*2D\n',] - -# '$GPGLL,0000.0000,N,00000.0000,E,235947.000,V*2D\n', # Will ignore this one - - my_gps = as_GPS.AS_GPS(sreader, fix_cb=callback, fix_cb_args=(42,)) - sentence = '' - for sentence in test_RMC: - sentence_count += 1 - await swriter.awrite(sentence) - await my_gps.data_received(date=True) - print('Longitude:', my_gps.longitude()) - print('Latitude', my_gps.latitude()) - print('UTC Time:', my_gps.utc) - print('Speed:', my_gps.speed()) - print('Date Stamp:', my_gps.date) - print('Course', my_gps.course) - print('Data is Valid:', my_gps._valid) - print('Compass Direction:', my_gps.compass_direction()) - print('') - - for sentence in test_GLL: - sentence_count += 1 - await swriter.awrite(sentence) - await my_gps.data_received(position=True) - print('Longitude:', my_gps.longitude()) - print('Latitude', my_gps.latitude()) - print('UTC Time:', my_gps.utc) - print('Data is Valid:', my_gps._valid) - print('') - - for sentence in test_VTG: - print('Test VTG', sentence) - sentence_count += 1 - await swriter.awrite(sentence) - await asyncio.sleep_ms(200) # Can't wait for course because of position check - print('Speed:', my_gps.speed()) - print('Course', my_gps.course) - print('Compass Direction:', my_gps.compass_direction()) - print('') - - for sentence in test_GGA: - sentence_count += 1 - await swriter.awrite(sentence) - await my_gps.data_received(position=True) - print('Longitude', my_gps.longitude()) - print('Latitude', my_gps.latitude()) - print('UTC Time:', my_gps.utc) -# print('Fix Status:', my_gps.fix_stat) - print('Altitude:', my_gps.altitude) - print('Height Above Geoid:', my_gps.geoid_height) - print('Horizontal Dilution of Precision:', my_gps.hdop) - print('Satellites in Use by Receiver:', my_gps.satellites_in_use) - print('') - - for sentence in test_GSA: - sentence_count += 1 - await swriter.awrite(sentence) - await asyncio.sleep_ms(200) - print('Satellites Used', my_gps.satellites_used) - print('Horizontal Dilution of Precision:', my_gps.hdop) - print('Vertical Dilution of Precision:', my_gps.vdop) - print('Position Dilution of Precision:', my_gps.pdop) - print('') - - for sentence in test_GSV: - sentence_count += 1 - await swriter.awrite(sentence) - await asyncio.sleep_ms(200) - print('SV Sentences Parsed', my_gps._last_sv_sentence) - print('SV Sentences in Total', my_gps._total_sv_sentences) - print('# of Satellites in View:', my_gps.satellites_in_view) - data_valid = my_gps._total_sv_sentences > 0 and my_gps._total_sv_sentences == my_gps._last_sv_sentence - print('Is Satellite Data Valid?:', data_valid) - if data_valid: - print('Satellite Data:', my_gps._satellite_data) - print('Satellites Visible:', list(my_gps._satellite_data.keys())) - print('') - - print("Pretty Print Examples:") - print('Latitude (degs):', my_gps.latitude_string(as_GPS.DD)) - print('Longitude (degs):', my_gps.longitude_string(as_GPS.DD)) - print('Latitude (dms):', my_gps.latitude_string(as_GPS.DMS)) - print('Longitude (dms):', my_gps.longitude_string(as_GPS.DMS)) - print('Latitude (kml):', my_gps.latitude_string(as_GPS.KML)) - print('Longitude (kml):', my_gps.longitude_string(as_GPS.KML)) - print('Latitude (degs, mins):', my_gps.latitude_string()) - print('Longitude (degs, mins):', my_gps.longitude_string()) - print('Speed:', my_gps.speed_string(as_GPS.KPH), 'or', - my_gps.speed_string(as_GPS.MPH), 'or', - my_gps.speed_string(as_GPS.KNOT)) - print('Date (Long Format):', my_gps.date_string(as_GPS.LONG)) - print('Date (Short D/M/Y Format):', my_gps.date_string(as_GPS.DMY)) - print('Date (Short M/D/Y Format):', my_gps.date_string(as_GPS.MDY)) - print('Time:', my_gps.time_string()) - print() - - print('### Final Results ###') - print('Sentences Attempted:', sentence_count) - print('Sentences Found:', my_gps.clean_sentences) - print('Sentences Parsed:', my_gps.parsed_sentences) - print('Unsupported sentences:', my_gps.unsupported_sentences) - print('CRC_Fails:', my_gps.crc_fails) - -loop = asyncio.get_event_loop() -loop.run_until_complete(run_tests()) diff --git a/v2/gps/log.kml b/v2/gps/log.kml deleted file mode 100644 index 31d1076..0000000 --- a/v2/gps/log.kml +++ /dev/null @@ -1,128 +0,0 @@ - - - - -#yellowPoly - -1 -1 -absolute - --2.102780,53.297553,162.2 --2.102777,53.297548,164.6 --2.102772,53.297539,165.4 --2.102763,53.297534,165.8 --2.102763,53.297534,165.8 --2.102763,53.297534,165.8 --2.102763,53.297534,165.7 --2.102758,53.297534,165.7 --2.102750,53.297534,165.7 --2.102738,53.297524,165.7 --2.102735,53.297515,165.7 --2.102733,53.297515,165.7 --2.102667,53.297505,165.7 --2.102215,53.297677,165.7 --2.101582,53.297644,165.7 --2.101537,53.297944,165.7 --2.102668,53.298240,165.7 --2.103305,53.298321,165.7 --2.104530,53.297915,165.7 --2.106058,53.297248,165.7 --2.107628,53.296633,165.7 --2.108622,53.295879,165.7 --2.109327,53.295202,165.7 --2.110145,53.294253,165.7 --2.110045,53.293753,165.7 --2.110323,53.293729,165.7 --2.110578,53.293681,165.7 --2.110587,53.293648,165.7 --2.110592,53.293653,165.7 --2.110593,53.293653,165.7 --2.110593,53.293653,165.7 --2.110593,53.293653,165.7 --2.110593,53.293653,165.7 --2.110595,53.293657,165.7 --2.110595,53.293657,165.7 --2.110595,53.293657,165.7 --2.110593,53.293657,165.7 --2.110593,53.293657,165.7 --2.110593,53.293657,165.7 --2.110593,53.293657,165.7 --2.110593,53.293657,165.7 --2.110593,53.293657,165.7 --2.110595,53.293657,165.7 --2.110595,53.293657,165.7 --2.110595,53.293657,165.7 --2.110595,53.293657,165.7 --2.110593,53.293667,165.7 --2.110597,53.293676,165.7 --2.110597,53.293676,165.7 --2.110597,53.293676,165.7 --2.110597,53.293676,165.7 --2.110597,53.293676,165.7 --2.110597,53.293676,165.7 --2.110597,53.293676,165.7 --2.110597,53.293681,165.7 --2.110545,53.293624,165.7 --2.110288,53.293591,165.7 --2.110288,53.293595,165.7 --2.110147,53.294272,165.7 --2.109365,53.295212,165.7 --2.108420,53.296084,165.7 --2.107292,53.296876,165.7 --2.105490,53.297467,165.7 --2.104190,53.298225,165.7 --2.102533,53.298411,165.7 --2.100548,53.298159,165.7 --2.098730,53.298378,165.7 --2.097297,53.298297,165.7 --2.096425,53.298078,165.7 --2.095933,53.298249,165.7 --2.095803,53.298254,165.7 --2.095803,53.298254,165.7 --2.095803,53.298254,165.7 --2.095803,53.298254,165.7 --2.095803,53.298254,165.7 --2.095803,53.298254,165.7 --2.095803,53.298254,165.7 --2.095805,53.298254,165.7 --2.095805,53.298254,165.7 --2.095805,53.298254,165.7 --2.095805,53.298254,165.7 --2.095805,53.298254,165.7 --2.095807,53.298259,165.7 --2.095873,53.298278,165.7 --2.095777,53.298335,165.7 --2.095338,53.298645,165.7 --2.095562,53.298788,165.7 --2.096558,53.298659,165.7 --2.097402,53.298526,165.7 --2.097873,53.298349,165.7 --2.099518,53.298202,165.7 --2.101260,53.298235,165.7 --2.102687,53.298383,165.7 --2.102098,53.298144,165.7 --2.101278,53.297801,165.7 --2.101830,53.297644,165.7 --2.102540,53.297577,165.7 --2.102727,53.297496,165.7 --2.102738,53.297515,165.7 --2.102743,53.297524,165.7 --2.102742,53.297524,165.7 --2.102742,53.297524,165.7 --2.102742,53.297524,165.7 --2.102740,53.297524,165.7 --2.102740,53.297524,165.7 - - - - - diff --git a/v2/gps/log_kml.py b/v2/gps/log_kml.py deleted file mode 100644 index 3d13548..0000000 --- a/v2/gps/log_kml.py +++ /dev/null @@ -1,77 +0,0 @@ -# log_kml.py Log GPS data to a kml file for display on Google Earth - -# Copyright (c) Peter Hinch 2018-2020 -# MIT License (MIT) - see LICENSE file -# Test program for asynchronous GPS device driver as_pyGPS -# KML file format: https://developers.google.com/kml/documentation/kml_tut -# http://www.toptechboy.com/arduino/lesson-25-display-your-gps-data-as-track-on-google-earth/ - -# Remove blue LED for Pyboard D - -# Logging stops and the file is closed when the user switch is pressed. - -import as_GPS -import uasyncio as asyncio -import pyb - -str_start = ''' - - - -#yellowPoly - -1 -1 -absolute - -''' - -str_end = ''' - - - - -''' - -red, green, yellow = pyb.LED(1), pyb.LED(2), pyb.LED(3) -sw = pyb.Switch() - -# Toggle the red LED -def toggle_led(*_): - red.toggle() - -async def log_kml(fn='/sd/log.kml', interval=10): - yellow.on() # Waiting for data - uart = pyb.UART(4, 9600, read_buf_len=200) # Data on X2 - sreader = asyncio.StreamReader(uart) - gps = as_GPS.AS_GPS(sreader, fix_cb=toggle_led) - await gps.data_received(True, True, True, True) - yellow.off() - with open(fn, 'w') as f: - f.write(str_start) - while not sw.value(): - f.write(gps.longitude_string(as_GPS.KML)) - f.write(',') - f.write(gps.latitude_string(as_GPS.KML)) - f.write(',') - f.write(str(gps.altitude)) - f.write('\r\n') - for _ in range(interval * 10): - await asyncio.sleep_ms(100) - if sw.value(): - break - - f.write(str_end) - red.off() - green.on() - -loop = asyncio.get_event_loop() -loop.run_until_complete(log_kml()) diff --git a/v2/htu21d/README.md b/v2/htu21d/README.md deleted file mode 100644 index 947a679..0000000 --- a/v2/htu21d/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# The HTU21D temperature/humidity sensor. - -A breakout board is available from -[Sparkfun](https://www.sparkfun.com/products/12064). - -This driver was derived from the synchronous Pyboard-specific driver -[here](https://github.com/manitou48/pyboard/blob/master/htu21d.py). It is -designed to be multi-platform and uses `uasyncio` to achieve asynchronous (non- -blocking) operation. The driver maintains `temperature` and `humidity` bound -variables as a non-blocking background task. Consequently reading the values is -effectively instantaneous. - -###### [Main README](../README.md) - -# Files - - 1. `htu21d_mc.py` The asynchronous driver. - 2. `htu_test.py` Test/demo program. - -# The driver - -This provides a single class `HTU21D`. - -Constructor. -This takes two args, `i2c` (mandatory) and an optional `read_delay=10`. The -former must be an initialised I2C bus instance. The `read_delay` (secs) -determines how frequently the data values are updated. - -Public bound values - 1. `temperature` Latest value in Celcius. - 2. `humidity` Latest value of relative humidity (%). - -Initial readings will not be complete until about 120ms after the class is -instantiated. Prior to this the values will be `None`. To avoid such invalid -readings the class is awaitable and may be used as follows. - -```python -async def show_values(): - htu = htu21d_mc.HTU21D(i2c) - await htu # Will pause ~120ms - # Data is now valid - while True: - fstr = 'Temp {:5.1f} Humidity {:5.1f}' - print(fstr.format(htu.temperature, htu.humidity)) - await asyncio.sleep(5) -``` - -Thermal inertia of the chip packaging means that there is a lag between the -occurrence of a temperature change and the availability of accurate readings. -There is therefore little practical benefit in reducing the `read_delay`. diff --git a/v2/htu21d/htu21d_mc.py b/v2/htu21d/htu21d_mc.py deleted file mode 100644 index d071075..0000000 --- a/v2/htu21d/htu21d_mc.py +++ /dev/null @@ -1,64 +0,0 @@ -# htu21d_mc.py Portable, asynchronous micropython driver for HTU21D temp/humidity I2C sensor -# https://www.sparkfun.com/products/12064 I2C 3.3v -# https://raw.githubusercontent.com/randymxj/Adafruit-Raspberry-Pi-Python-Code/master/Adafruit_HTU21D/Adafruit_HTU21D.py -# Based on https://github.com/manitou48/pyboard/blob/master/htu21d.py - -# Author: Peter Hinch -# Copyright Peter Hinch 2018 Released under the MIT license - -import machine -import ustruct -import uasyncio as asyncio -from micropython import const - -_ADDRESS = const(0x40) # HTU21D Address -_PAUSE_MS = const(60) # HTU21D acquisition delay -_READ_USER_REG = const(0xE7) - -# CRC8 calculation notes. See https://github.com/sparkfun/HTU21D_Breakout -# Reads 3 temperature/humidity bytes from the sensor -# value[0], value[1] = Raw temp/hum data, value[2] = CRC -# Polynomial = 0x0131 = x^8 + x^5 + x^4 + 1 - -class HTU21D: - START_TEMP_MEASURE = b'\xF3' # Commands - START_HUMD_MEASURE = b'\xF5' - - def __init__(self, i2c, read_delay=10): - self.i2c = i2c - if _ADDRESS not in self.i2c.scan(): - raise OSError('No HTU21D device found.') - self.temperature = None - self.humidity = None - loop = asyncio.get_event_loop() - loop.create_task(self._run(read_delay)) - - async def _run(self, read_delay): - while True: - raw_temp = await self._get_data(self.START_TEMP_MEASURE) - self.temperature = -46.85 + (175.72 * raw_temp / 65536) # Calculate temp - raw_rh = await self._get_data(self.START_HUMD_MEASURE) - self.humidity = -6 + (125.0 * raw_rh / 65536) # Calculate RH - await asyncio.sleep(read_delay) - - def __iter__(self): # Await 1st reading - while self.humidity is None: - yield - - async def _get_data(self, cmd, divisor=0x131 << 15, bit=1 << 23): - self.i2c.writeto(_ADDRESS, cmd) # Start reading - await asyncio.sleep_ms(_PAUSE_MS) # Wait for device - value = self.i2c.readfrom(_ADDRESS, 3) # Read result, check CRC8 - data, crc = ustruct.unpack('>HB', value) - remainder = (data << 8) | crc - while bit > 128: - if(remainder & bit): - remainder ^= divisor - divisor >>= 1 - bit >>= 1 - if remainder: - raise OSError('HTU21D CRC Fail') - return data & 0xFFFC # Clear the status bits - - def user_register(self): # Read the user register byte (should be 2) - return self.i2c.readfrom_mem(_ADDRESS, _READ_USER_REG, 1)[0] diff --git a/v2/htu21d/htu_test.py b/v2/htu21d/htu_test.py deleted file mode 100644 index 401aac1..0000000 --- a/v2/htu21d/htu_test.py +++ /dev/null @@ -1,34 +0,0 @@ -# htu_test.py Demo program for portable asynchronous HTU21D driver - -# Author: Peter Hinch -# Copyright Peter Hinch 2018 Released under the MIT license - -import uasyncio as asyncio -import sys -from machine import Pin, I2C -import htu21d_mc - -if sys.platform == 'pyboard': - i2c = I2C(1) # scl=X9 sda=X10 -else: - # Specify pullup: on my ESP32 board pullup resistors are not fitted :-( - scl_pin = Pin(22, pull=Pin.PULL_UP, mode=Pin.OPEN_DRAIN) - sda_pin = Pin(23, pull=Pin.PULL_UP, mode=Pin.OPEN_DRAIN) - # Standard port - i2c = I2C(-1, scl=scl_pin, sda=sda_pin) - # Loboris port (soon this special treatment won't be needed). - # https://forum.micropython.org/viewtopic.php?f=18&t=3553&start=390 - #i2c = I2C(scl=scl_pin, sda=sda_pin) - -htu = htu21d_mc.HTU21D(i2c, read_delay=2) # read_delay=2 for test purposes - -async def main(): - await htu - while True: - fstr = 'Temp {:5.1f} Humidity {:5.1f}' - print(fstr.format(htu.temperature, htu.humidity)) - await asyncio.sleep(5) - -loop = asyncio.get_event_loop() -loop.create_task(main()) -loop.run_forever() diff --git a/v2/i2c/README.md b/v2/i2c/README.md deleted file mode 100644 index 27fa8cb..0000000 --- a/v2/i2c/README.md +++ /dev/null @@ -1,420 +0,0 @@ -# A communication link using I2C - -This library implements an asynchronous bidirectional communication link -between MicroPython targets using I2C. It presents a UART-like interface -supporting `StreamReader` and `StreamWriter` classes. In doing so, it emulates -the behaviour of a full duplex link despite the fact that the underlying I2C -link is half duplex. - -One use case is to provide a UART-like interface to an ESP8266 while leaving -the one functional UART free for the REPL. - -The blocking nature of the MicroPython I2C device driver is mitigated by -hardware synchronisation on two wires. This ensures that the slave is -configured for a transfer before the master attempts to access it. - -The Pyboard or similar STM based boards are currently the only targets -supporting I2C slave mode. Consequently at least one end of the interface -(known as the`Initiator`) must be a Pyboard or other board supporting the `pyb` -module. The `Responder` may be any hardware running MicroPython and supporting -`machine`. - -If the `Responder` (typically an ESP8266) crashes the resultant I2C failure is -detected by the `Initiator` which can issue a hardware reboot to the -`Responder` enabling the link to recover. This can occur transparently to the -application and is covered in detail -[in section 5.3](./README.md#53-responder-crash-detection). - -## Changes - -V0.17 Dec 2018 Initiator: add optional "go" and "fail" user coroutines. -V0.16 Minor improvements and bugfixes. Eliminate `timeout` option which caused -failures where `Responder` was a Pyboard. -V0.15 RAM allocation reduced. Flow control implemented. -V0.1 Initial release. - -###### [Main README](../README.md) - -# Contents - - 1. [Files](./README.md#1-files) - 2. [Wiring](./README.md#2-wiring) - 3. [Design](./README.md#3-design) - 4. [API](./README.md#4-api) - 4.1 [Channel class](./README.md#41-channel-class) - 4.2 [Initiator class](./README.md#42-initiator-class) - 4.2.1 [Configuration](./README.md#421-configuration) Fine-tuning the interface. - 4.2.2 [Optional coroutines](./README.md#422-optional-coroutines) - 4.3 [Responder class](./README.md#43-responder-class) - 5. [Limitations](./README.md#5-limitations) - 5.1 [Blocking](./README.md#51-blocking) - 5.2 [Buffering and RAM usage](./README.md#52-buffering-and-ram-usage) - 5.3 [Responder crash detection](./README.md#53-responder-crash-detection) - 6. [Hacker notes](./README.md#6-hacker-notes) For anyone wanting to hack on - the code. - -# 1. Files - - 1. `asi2c.py` Module for the `Responder` target. - 2. `asi2c_i.py` The `Initiator` target requires this and `asi2c.py`. - 3. `i2c_init.py` Initiator test/demo to run on a Pyboard. - 4. `i2c_resp.py` Responder test/demo to run on a Pyboard. - 5. `i2c_esp.py` Responder test/demo for ESP8266. - -Dependency: - 1. `uasyncio` Official library or my fork. - -# 2. Wiring - -| Pyboard | Target | Comment | -|:-------:|:------:|:-------:| -| gnd | gnd | | -| sda | sda | I2C | -| scl | scl | I2C | -| sync | sync | Any pin may be used. | -| ack | ack | Any pin. | -| rs_out | rst | Optional reset link. | - -The `sync` and `ack` wires provide synchronisation: pins used are arbitrary. In -addition provision may be made for the Pyboard to reset the target if it -crashes and fails to respond. If this is required, link a Pyboard pin to the -target's `reset` pin. - -I2C requires the devices to be connected via short links and to share a common -ground. The `sda` and `scl` lines also require pullup resistors. On the Pyboard -V1.x these are fitted. If pins lacking these resistors are used, pullups to -3.3V should be supplied. A typical value is 4.7KΩ. - -###### [Contents](./README.md#contents) - -# 3. Design - -The I2C specification is asymmetrical: only master devices can initiate -transfers. This library enables slaves to initiate a data exchange by -interrupting the master which then starts the I2C transactions. There is a -timing issue in that the I2C master requires that the slave be ready before it -initiates a transfer. Further, in the MicroPython implementation, a slave which -is ready will block until the transfer is complete. - -To meet the timing constraint the slave must initiate all exchanges; it does -this by interrupting the master. The slave is therefore termed the `Initiator` -and the master `Responder`. The `Initiator` must be a Pyboard or other STM -board supporting slave mode via the `pyb` module. - -To enable `Responder` to start an unsolicited data transfer, `Initiator` -periodically interrupts `Responder` to cause a data exchange. If either -participant has no data to send it sends an empty string. Strings are exchanged -at a fixed rate to limit the interrupt overhead on `Responder`. This implies a -latency on communications in either direction; the rate (maximum latency) is -under application control. By default it is 100ms. - -The module will run under official or `fast_io` builds of `uasyncio`. Owing to -the latency discussed above, the choice has little effect on the performance of -this interface. - -A further issue common to most communications protocols is synchronisation: -the devices won't boot simultaneously. Initially, and after the `Initiator` -reboots the `Responder`, both ends run a synchronisation phase. The interface -starts to run once each end has determined that its counterpart is ready. - -The design assumes exclusive use of the I2C interface. Hard or soft I2C may be -used. - -###### [Contents](./README.md#contents) - -# 4. API - -The following scripts demonstrate basic usage. They may be copied and pasted at -the REPL. They assume a Pyboard linked to an ESP8266 as follows: - -| Pyboard | ESP8266 | Notes | -|:-------:|:-------:|:--------:| -| gnd | gnd | | -| X9 | 0 | I2C scl | -| X10 | 2 | I2C sda | -| X11 | 5 | syn | -| X12 | rst | Optional | -| Y8 | 4 | ack | - -On Pyboard: - -```python -import uasyncio as asyncio -from pyb import I2C # Only pyb supports slave mode -from machine import Pin -import asi2c_i - -i2c = I2C(1, mode=I2C.SLAVE) -syn = Pin('X11') -ack = Pin('Y8') -rst = (Pin('X12'), 0, 200) -chan = asi2c_i.Initiator(i2c, syn, ack, rst) - -async def receiver(): - sreader = asyncio.StreamReader(chan) - while True: - res = await sreader.readline() - print('Received', int(res)) - -async def sender(): - swriter = asyncio.StreamWriter(chan, {}) - n = 0 - while True: - await swriter.awrite('{}\n'.format(n)) - n += 1 - await asyncio.sleep_ms(800) - -loop = asyncio.get_event_loop() -loop.create_task(receiver()) -loop.create_task(sender()) -try: - loop.run_forever() -finally: - chan.close() # for subsequent runs -``` - -On ESP8266: - -```python -import uasyncio as asyncio -from machine import Pin, I2C -import asi2c - -i2c = I2C(scl=Pin(0),sda=Pin(2)) # software I2C -syn = Pin(5) -ack = Pin(4) -chan = asi2c.Responder(i2c, syn, ack) - -async def receiver(): - sreader = asyncio.StreamReader(chan) - while True: - res = await sreader.readline() - print('Received', int(res)) - -async def sender(): - swriter = asyncio.StreamWriter(chan, {}) - n = 1 - while True: - await swriter.awrite('{}\n'.format(n)) - n += 1 - await asyncio.sleep_ms(1500) - -loop = asyncio.get_event_loop() -loop.create_task(receiver()) -loop.create_task(sender()) -try: - loop.run_forever() -finally: - chan.close() # for subsequent runs -``` - -###### [Contents](./README.md#contents) - -## 4.1 Channel class - -This is the base class for `Initiator` and `Responder` subclasses and provides -support for the streaming API. Applications do not instantiate `Channel` -objects. - -Method: - 1. `close` No args. Restores the interface to its power-up state. - -Coroutine: - 1. `ready` No args. Pause until synchronisation has been achieved. - -## 4.2 Initiator class - -##### Constructor args: - 1. `i2c` An `I2C` instance. - 2. `pin` A `Pin` instance for the `sync` signal. - 3. `pinack` A `Pin` instance for the `ack` signal. - 4. `reset=None` Optional tuple defining a reset pin (see below). - 5. `verbose=True` If `True` causes debug messages to be output. - 6. `cr_go=False` Optional coroutine to run at startup. See - [4.2.2](./README.md#422-optional-coroutines). - 7. `go_args=()` Optional tuple of args for above coro. - 8. `cr_fail=False` Optional coro to run on ESP8266 fail or reboot. - 9. `f_args=()` Optional tuple of args for above. - -The `reset` tuple consists of (`pin`, `level`, `time`). If provided, and the -`Responder` times out, `pin` will be set to `level` for duration `time` ms. A -Pyboard or ESP8266 target with an active low reset might have: - -```python -(machine.Pin('X12'), 0, 200) -``` - -If the `Initiator` has no `reset` tuple and the `Responder` times out, an -`OSError` will be raised. - -`Pin` instances passed to the constructor must be instantiated by `machine`. - -##### Class variables: - 1. `t_poll=100` Interval (ms) for `Initiator` polling `Responder`. - 2. `rxbufsize=200` Size of receive buffer. This should exceed the maximum - message length. - -See [Section 4.2.1](./README.md#421-configuration). - -##### Instance variables: - -The `Initiator` maintains instance variables which may be used to measure its -peformance. See [Section 4.2.1](./README.md#421-configuration). - -##### Coroutine: - 1. `reboot` If a `reset` tuple was provided, reboot the `Responder`. - -## 4.2.1 Configuration - -The `Initiator` class variables determine the behaviour of the interface. Where -these are altered, it should be done before instantiating `Initiator` or -`Responder`. - -`Initiator.t_poll` This defines the polling interval for incoming data. Shorter -values reduce the latency when the `Responder` sends data; at the cost of a -raised CPU overhead (at both ends) in processing `Responder` polling. - -Times are in ms. - -To measure performance when running application code these `Initiator` instance -variables may be read: - 1. `nboots` Number of times `Responder` has failed and been rebooted. - 2. `block_max` Maximum blocking time in μs. - 3. `block_sum` Cumulative total of blocking time (μs). - 4. `block_cnt` Transfer count: mean blocking time is `block_sum/block_cnt`. - -See test program `i2c_init.py` for an example of using the above. - -## 4.2.2 Optional coroutines - -These are intended for applications where the `Responder` may reboot at runtime -either because I2C failure was detected or because the application issues an -explicit reboot command. - -The `cr_go` and `cr_fail` coroutines provide for applications which implement -an application-level initialisation sequence on first and subsequent boots of -the `Responder`. Such applications need to ensure that the initialisation -sequence does not conflict with other coros accessing the channel. - -The `cr_go` coro runs after synchronisation has been achieved. It runs -concurrently with the coro which keeps the link open (`Initiator._run()`), but -should run to completion reasonably quickly. Typically it performs any app -level synchronisation, starts or re-enables application coros, and quits. - -The `cr_fail` routine will prevent the automatic reboot from occurring until -it completes. This may be used to prevent user coros from accessing the channel -until reboot is complete. This may be done by means of locks or task -cancellation. Typically `cr_fail` will terminate when this is done, so that -`cr_go` has unique access to the channel. - -If an explicit `.reboot()` is issued, a reset tuple was provided, and `cr_fail` -exists, it will run and the physical reboot will be postponed until it -completes. - -Typical usage: -```python -chan = asi2c_i.Initiator(i2c, syn, ack, rst, verbose, self._go, (), self._fail) -``` - -###### [Contents](./README.md#contents) - -## 4.3 Responder class - -##### Constructor args: - 1. `i2c` An `I2C` instance. - 2. `pin` A `Pin` instance for the `sync` signal. - 3. `pinack` A `Pin` instance for the `ack` signal. - 4. `verbose=True` If `True` causes debug messages to be output. - -`Pin` instances passed to the constructor must be instantiated by `machine`. - -##### Class variables: - 1. `addr=0x12` Address of I2C slave. This should be set before instantiating - `Initiator` or `Responder`. If the default address (0x12) is to be overriden, - `Initiator` application code must instantiate the I2C accordingly. - 2. `rxbufsize=200` Size of receive buffer. This should exceed the maximum - message length. Consider reducing this in ESP8266 applications to save RAM. - -###### [Contents](./README.md#contents) - -# 5. Limitations - -## 5.1 Blocking - -Exchanges of data occur via `Initiator._sendrx()`, a synchronous method. This -blocks the schedulers at each end for a duration dependent on the number of -bytes being transferred. Tests were conducted with the supplied test scripts -and the official version of `uasyncio`. Note that these scripts send short -strings. - -With `Responder` running on a Pyboard V1.1 the duration of the ISR was up to -1.3ms. - -With `Responder` on an ESP8266 running at 80MHz, `Initiator` blocked for up to -10ms with a mean time of 2.7ms; at 160MHz the figures were 7.5ms and 2.1ms. The -ISR uses soft interrupts, and blocking commences as soon as the interrupt pin -is asserted. Consequently the time for which `Initiator` blocks depends on -`Responder`'s interrupt latency; this may be extended by garbage collection. - -Figures are approximate: actual blocking time is dependent on the length of the -strings, the speed of the processors, soft interrupt latency and the behaviour -of other coroutines. If blocking time is critical it should be measured while -running application code. - -## 5.2 Buffering and RAM usage - -The protocol implements flow control: the `StreamWriter` at one end of the link -will pause until the last string transmitted has been read by the corresponding -`StreamReader`. - -Outgoing data is unbuffered. `StreamWriter.awrite` will pause until pending -data has been transmitted. - -Incoming data is stored in a buffer whose length is set by the `rxbufsize` -constructor arg. If an incoming payload is too long to fit the buffer a -`ValueError` will be thrown. - -## 5.3 Responder crash detection - -The `Responder` protocol executes in a soft interrupt context. This means that -the application code might fail (for example executing an infinite loop) while -the ISR continues to run; `Initiator` would therefore see no problem. To trap -this condition regular messages should be sent from `Responder`, with -`Initiator` application code timing out on their absence and issuing `reboot`. - -This also has implications when testing. If a `Responder` application is -interrupted with `ctrl-c` the ISR will continue to run. To test crash detection -issue a soft or hard reset to the `Responder`. - -###### [Contents](./README.md#contents) - -# 6. Hacker notes - -I tried a variety of approaches before settling on a synchronous method for -data exchange coupled with 2-wire hardware handshaking. The chosen approach -minimises the time for which the schedulers are blocked. Blocking occurs -because of the need to initiate a blocking transfer on the I2C slave before the -master can initiate a transfer. - -A one-wire handshake using open drain outputs is feasible but involves explicit -delays. I took the view that a 2-wire solution is easier should anyone want to -port the `Responder` to a platform such as the Raspberry Pi. The design has no -timing constraints and uses normal push-pull I/O pins. - -I experienced a couple of obscure issues affecting reliability. Calling `pyb` -`I2C` methods with an explicit timeout caused rare failures when the target was -also a Pyboard. Using `micropython.schedule` to defer RAM allocation also -provoked rare failures. This may be the reason why I never achieved reliable -operation with hard IRQ's on ESP8266. - -I created a version which eliminated RAM allocation by the `Responder` ISR to -use hard interrupts. This reduced blocking further. Unfortunately I failed to -achieve reliable operation on an ESP8266 target. This version introduced some -complexity into the code so was abandoned. If anyone feels like hacking, the -branch `i2c_hard_irq` exists. - -The main branch aims to minimise allocation while achieving reliability. - -PR's to reduce allocation and enable hard IRQ's welcome. I will expect them to -run the two test programs for >10,000 messages with ESP8266 and Pyboard -targets. Something I haven't yet achieved (with hard IRQ's). diff --git a/v2/i2c/asi2c.py b/v2/i2c/asi2c.py deleted file mode 100644 index c41e5f1..0000000 --- a/v2/i2c/asi2c.py +++ /dev/null @@ -1,206 +0,0 @@ -# asi2c.py A communications link using I2C slave mode on Pyboard. - -# The MIT License (MIT) -# -# Copyright (c) 2018 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import uasyncio as asyncio -import machine -import utime -from micropython import const -import io - -_MP_STREAM_POLL_RD = const(1) -_MP_STREAM_POLL_WR = const(4) -_MP_STREAM_POLL = const(3) -_MP_STREAM_ERROR = const(-1) -# Delay compensates for short Responder interrupt latency. Must be >= max delay -# between Initiator setting a pin and initiating an I2C transfer: ensure -# Initiator sets up first. -_DELAY = const(20) # μs - - -# Base class provides user interface and send/receive object buffers -class Channel(io.IOBase): - def __init__(self, i2c, own, rem, verbose, rxbufsize): - self.rxbufsize = rxbufsize - self.verbose = verbose - self.synchronised = False - # Hardware - self.i2c = i2c - self.own = own - self.rem = rem - own.init(mode=machine.Pin.OUT, value=1) - rem.init(mode=machine.Pin.IN, pull=machine.Pin.PULL_UP) - # I/O - self.txbyt = b'' # Data to send - self.txsiz = bytearray(2) # Size of .txbyt encoded as 2 bytes - self.rxbyt = b'' - self.rxbuf = bytearray(rxbufsize) - self.rx_mv = memoryview(self.rxbuf) - self.cantx = True # Remote can accept data - - async def _sync(self): - self.verbose and print('Synchronising') - self.own(0) - while self.rem(): - await asyncio.sleep_ms(100) - # Both pins are now low - await asyncio.sleep(0) - self.verbose and print('Synchronised') - self.synchronised = True - - def waitfor(self, val): # Initiator overrides - while not self.rem() == val: - pass - - # Get incoming bytes instance from memoryview. - def _handle_rxd(self, msg): - self.rxbyt = bytes(msg) - - def _txdone(self): - self.txbyt = b'' - self.txsiz[0] = 0 - self.txsiz[1] = 0 - - # Stream interface - - def ioctl(self, req, arg): - ret = _MP_STREAM_ERROR - if req == _MP_STREAM_POLL: - ret = 0 - if self.synchronised: - if arg & _MP_STREAM_POLL_RD: - if self.rxbyt: - ret |= _MP_STREAM_POLL_RD - if arg & _MP_STREAM_POLL_WR: - if (not self.txbyt) and self.cantx: - ret |= _MP_STREAM_POLL_WR - return ret - - def readline(self): - n = self.rxbyt.find(b'\n') - if n == -1: - t = self.rxbyt[:] - self.rxbyt = b'' - else: - t = self.rxbyt[: n + 1] - self.rxbyt = self.rxbyt[n + 1:] - return t.decode() - - def read(self, n): - t = self.rxbyt[:n] - self.rxbyt = self.rxbyt[n:] - return t.decode() - - # Set .txbyt to the required data. Return its size. So awrite returns - # with transmission occurring in tha background. - def write(self, buf, off, sz): - if self.synchronised: - if self.txbyt: # Initial call from awrite - return 0 # Waiting for existing data to go out - # If awrite is called without off or sz args, avoid allocation - if off == 0 and sz == len(buf): - d = buf - else: - d = buf[off: off + sz] - d = d.encode() - l = len(d) - self.txbyt = d - self.txsiz[0] = l & 0xff - self.txsiz[1] = l >> 8 - return l - return 0 - - # User interface - - # Wait for sync - async def ready(self): - while not self.synchronised: - await asyncio.sleep_ms(100) - - # Leave pin high in case we run again - def close(self): - self.own(1) - - -# Responder is I2C master. It is cross-platform and uses machine. -# It does not handle errors: if I2C fails it dies and awaits reset by initiator. -# send_recv is triggered by Interrupt from Initiator. - -class Responder(Channel): - addr = 0x12 - rxbufsize = 200 - - def __init__(self, i2c, pin, pinack, verbose=True): - super().__init__(i2c, pinack, pin, verbose, self.rxbufsize) - loop = asyncio.get_event_loop() - loop.create_task(self._run()) - - async def _run(self): - await self._sync() # own pin ->0, wait for remote pin == 0 - self.rem.irq(handler=self._handler, trigger=machine.Pin.IRQ_RISING) - - # Request was received: immediately read payload size, then payload - # On Pyboard blocks for 380μs to 1.2ms for small amounts of data - def _handler(self, _, sn=bytearray(2), txnull=bytearray(2)): - addr = Responder.addr - self.rem.irq(handler=None, trigger=machine.Pin.IRQ_RISING) - utime.sleep_us(_DELAY) # Ensure Initiator has set up to write. - self.i2c.readfrom_into(addr, sn) - self.own(1) - self.waitfor(0) - self.own(0) - n = sn[0] + ((sn[1] & 0x7f) << 8) # no of bytes to receive - if n > self.rxbufsize: - raise ValueError('Receive data too large for buffer.') - self.cantx = not bool(sn[1] & 0x80) # Can Initiator accept a payload? - if n: - self.waitfor(1) - utime.sleep_us(_DELAY) - mv = memoryview(self.rx_mv[0: n]) # allocates - self.i2c.readfrom_into(addr, mv) - self.own(1) - self.waitfor(0) - self.own(0) - self._handle_rxd(mv) - - self.own(1) # Request to send - self.waitfor(1) - utime.sleep_us(_DELAY) - dtx = self.txbyt != b'' and self.cantx # Data to send - siz = self.txsiz if dtx else txnull - if self.rxbyt: - siz[1] |= 0x80 # Hold off Initiator TX - else: - siz[1] &= 0x7f - self.i2c.writeto(addr, siz) # Was getting ENODEV occasionally on Pyboard - self.own(0) - self.waitfor(0) - if dtx: - self.own(1) - self.waitfor(1) - utime.sleep_us(_DELAY) - self.i2c.writeto(addr, self.txbyt) - self.own(0) - self.waitfor(0) - self._txdone() # Invalidate source - self.rem.irq(handler=self._handler, trigger=machine.Pin.IRQ_RISING) diff --git a/v2/i2c/asi2c_i.py b/v2/i2c/asi2c_i.py deleted file mode 100644 index b3f7ddb..0000000 --- a/v2/i2c/asi2c_i.py +++ /dev/null @@ -1,142 +0,0 @@ -# asi2c_i.py A communications link using I2C slave mode on Pyboard. -# Initiator class - -# The MIT License (MIT) -# -# Copyright (c) 2018 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import uasyncio as asyncio -import machine -import utime -import gc -from asi2c import Channel - - -# The initiator is an I2C slave. It runs on a Pyboard. I2C uses pyb for slave -# mode, but pins are instantiated using machine. -# reset (if provided) is a means of resetting Responder in case of error: it -# is (pin, active_level, ms) -class Initiator(Channel): - t_poll = 100 # ms between Initiator polling Responder - rxbufsize = 200 - - def __init__(self, i2c, pin, pinack, reset=None, verbose=True, - cr_go=False, go_args=(), cr_fail=False, f_args=()): - super().__init__(i2c, pin, pinack, verbose, self.rxbufsize) - self.reset = reset - self.cr_go = cr_go - self.go_args = go_args - self.cr_fail = cr_fail - self.f_args = f_args - if reset is not None: - reset[0].init(mode=machine.Pin.OUT, value=not (reset[1])) - # Self measurement - self.nboots = 0 # No. of reboots of Responder - self.block_max = 0 # Blocking times: max - self.block_sum = 0 # Total - self.block_cnt = 0 # Count - self.loop = asyncio.get_event_loop() - self.loop.create_task(self._run()) - - def waitfor(self, val): # Wait for response for 1 sec - tim = utime.ticks_ms() - while not self.rem() == val: - if utime.ticks_diff(utime.ticks_ms(), tim) > 1000: - raise OSError - - async def reboot(self): - self.close() # Leave own pin high - if self.reset is not None: - rspin, rsval, rstim = self.reset - self.verbose and print('Resetting target.') - rspin(rsval) # Pulse reset line - await asyncio.sleep_ms(rstim) - rspin(not rsval) - - async def _run(self): - while True: - # If hardware link exists reboot Responder - await self.reboot() - self.txbyt = b'' - self.rxbyt = b'' - await self._sync() - await asyncio.sleep(1) # Ensure Responder is ready - if self.cr_go: - self.loop.create_task(self.cr_go(*self.go_args)) - while True: - gc.collect() - try: - tstart = utime.ticks_us() - self._sendrx() - t = utime.ticks_diff(utime.ticks_us(), tstart) - except OSError: - break - await asyncio.sleep_ms(Initiator.t_poll) - self.block_max = max(self.block_max, t) # self measurement - self.block_cnt += 1 - self.block_sum += t - self.nboots += 1 - if self.cr_fail: - await self.cr_fail(*self.f_args) - if self.reset is None: # No means of recovery - raise OSError('Responder fail.') - - # Send payload length (may be 0) then payload (if any) - def _sendrx(self, sn=bytearray(2), txnull=bytearray(2)): - siz = self.txsiz if self.cantx else txnull - if self.rxbyt: - siz[1] |= 0x80 # Hold off further received data - else: - siz[1] &= 0x7f - # CRITICAL TIMING. Trigger interrupt on responder immediately before - # send. Send must start before RX begins. Fast responders may need to - # do a short blocking wait to guarantee this. - self.own(1) # Trigger interrupt. - self.i2c.send(siz) # Blocks until RX complete. - self.waitfor(1) - self.own(0) - self.waitfor(0) - if self.txbyt and self.cantx: - self.own(1) - self.i2c.send(self.txbyt) - self.waitfor(1) - self.own(0) - self.waitfor(0) - self._txdone() # Invalidate source - # Send complete - self.waitfor(1) # Wait for responder to request send - self.own(1) # Acknowledge - self.i2c.recv(sn) - self.waitfor(0) - self.own(0) - n = sn[0] + ((sn[1] & 0x7f) << 8) # no of bytes to receive - if n > self.rxbufsize: - raise ValueError('Receive data too large for buffer.') - self.cantx = not bool(sn[1] & 0x80) - if n: - self.waitfor(1) # Wait for responder to request send - # print('setting up receive', n,' bytes') - self.own(1) # Acknowledge - mv = memoryview(self.rx_mv[0: n]) - self.i2c.recv(mv) - self.waitfor(0) - self.own(0) - self._handle_rxd(mv) diff --git a/v2/i2c/i2c_esp.py b/v2/i2c/i2c_esp.py deleted file mode 100644 index 881dfb9..0000000 --- a/v2/i2c/i2c_esp.py +++ /dev/null @@ -1,69 +0,0 @@ -# i2c_esp.py Test program for asi2c.py -# Tests Responder on ESP8266 - -# The MIT License (MIT) -# -# Copyright (c) 2018 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# pyb esp8266 -# scl X9 - 0 -# sda X10 - 2 -# sync X11 - 5 -# ack Y8 - 4 -# gnd - gnd - -import uasyncio as asyncio -from machine import Pin, I2C -import asi2c -import ujson - -i2c = I2C(scl=Pin(0),sda=Pin(2)) # software I2C -syn = Pin(5) -ack = Pin(4) -chan = asi2c.Responder(i2c, syn, ack) - -async def receiver(): - sreader = asyncio.StreamReader(chan) - await chan.ready() - print('started') - for _ in range(5): # Test flow control - res = await sreader.readline() - print('Received', ujson.loads(res)) - await asyncio.sleep(4) - while True: - res = await sreader.readline() - print('Received', ujson.loads(res)) - -async def sender(): - swriter = asyncio.StreamWriter(chan, {}) - txdata = [0, 0] - while True: - await swriter.awrite(''.join((ujson.dumps(txdata), '\n'))) - txdata[1] += 1 - await asyncio.sleep_ms(1500) - -loop = asyncio.get_event_loop() -loop.create_task(receiver()) -loop.create_task(sender()) -try: - loop.run_forever() -finally: - chan.close() # for subsequent runs diff --git a/v2/i2c/i2c_init.py b/v2/i2c/i2c_init.py deleted file mode 100644 index 12f24d8..0000000 --- a/v2/i2c/i2c_init.py +++ /dev/null @@ -1,81 +0,0 @@ -# i2c_init.py Test program for asi2c.py -# Tests Initiator on a Pyboard - -# The MIT License (MIT) -# -# Copyright (c) 2018 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# scl = X9 - X9 -# sda = X10 - X10 -# sync = X11 - X11 -# rst = X12 - rst (optional) -# ack = Y8 - Y8 - -import uasyncio as asyncio -from pyb import I2C # Only pyb supports slave mode -from machine import Pin -import asi2c_i -import ujson - -i2c = I2C(1, mode=I2C.SLAVE) -syn = Pin('X11') -ack = Pin('Y8') -# Reset on Pyboard and ESP8266 is active low. Use 200ms pulse. -rst = (Pin('X12'), 0, 200) -chan = asi2c_i.Initiator(i2c, syn, ack, rst) - -async def receiver(): - sreader = asyncio.StreamReader(chan) - for _ in range(5): # Test flow control - res = await sreader.readline() - print('Received', ujson.loads(res)) - await asyncio.sleep(4) - while True: - res = await sreader.readline() - print('Received', ujson.loads(res)) - -async def sender(): - swriter = asyncio.StreamWriter(chan, {}) - txdata = [0, 0] - await swriter.awrite(''.join((ujson.dumps('this is a test 1'), '\n'))) - await swriter.awrite(''.join((ujson.dumps('this is a test 2'), '\n'))) - await swriter.awrite(''.join((ujson.dumps('this is a test 3'), '\n'))) - while True: - await swriter.awrite(''.join((ujson.dumps(txdata), '\n'))) - txdata[0] += 1 - await asyncio.sleep_ms(800) - -async def test(loop): - loop.create_task(receiver()) - loop.create_task(sender()) - while True: - await chan.ready() - await asyncio.sleep(10) - print('Blocking time {:d}μs max. {:d}μs mean.'.format( - chan.block_max, int(chan.block_sum/chan.block_cnt))) - print('Reboots: ', chan.nboots) - -loop = asyncio.get_event_loop() -loop.create_task(test(loop)) -try: - loop.run_forever() -finally: - chan.close() # for subsequent runs diff --git a/v2/i2c/i2c_resp.py b/v2/i2c/i2c_resp.py deleted file mode 100644 index 645c79e..0000000 --- a/v2/i2c/i2c_resp.py +++ /dev/null @@ -1,68 +0,0 @@ -# i2c_resp.py Test program for asi2c.py -# Tests Responder on a Pyboard. - -# The MIT License (MIT) -# -# Copyright (c) 2018 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# scl = X9 -# sda = X10 -# sync = X11 -# ack = Y8 - Y8 - -import uasyncio as asyncio -from machine import Pin, I2C -import asi2c -import ujson - -i2c = I2C(1) -#i2c = I2C(scl=Pin('X9'),sda=Pin('X10')) # software I2C -syn = Pin('X11') -ack = Pin('Y8') -chan = asi2c.Responder(i2c, syn, ack) - -async def receiver(): - sreader = asyncio.StreamReader(chan) - await chan.ready() - print('started') - for _ in range(5): # Test flow control - res = await sreader.readline() - print('Received', ujson.loads(res)) - await asyncio.sleep(4) - while True: - res = await sreader.readline() - print('Received', ujson.loads(res)) - -async def sender(): - swriter = asyncio.StreamWriter(chan, {}) - txdata = [0, 0] - while True: - await swriter.awrite(''.join((ujson.dumps(txdata), '\n'))) - txdata[1] += 1 - await asyncio.sleep_ms(1500) - -loop = asyncio.get_event_loop() -loop.create_task(receiver()) -loop.create_task(sender()) -try: - loop.run_forever() -finally: - chan.close() # for subsequent runs diff --git a/v2/io.py b/v2/io.py deleted file mode 100644 index 348f1a3..0000000 --- a/v2/io.py +++ /dev/null @@ -1,39 +0,0 @@ -# io.py Failed attempt to use uasyncio IORead mechanism in a custom class. -# It turns out that the necessary support has not been implemented, and -# it is unlikely that this will occur. -import uasyncio as asyncio - -MP_STREAM_POLL_RD = 1 -MP_STREAM_POLL = 3 - -import uasyncio as asyncio -class Device(): - def __init__(self): - self.ready = False - - def fileno(self): - return 999 - - def ioctl(self, cmd, flags): - res = 0 - print('Got here') - if cmd == MP_STREAM_POLL and (flags & MP_STREAM_POLL_RD): - if self.ready: - res = MP_STREAM_POLL_RD - return res - - def read(self): - return - def write(self): - return - - async def readloop(self): - while True: - print('About to yield') - yield asyncio.IORead(self) - print('Should never happen') - -loop = asyncio.get_event_loop() -device = Device() -loop.create_task(device.readloop()) -loop.run_forever() diff --git a/v2/iorw.py b/v2/iorw.py deleted file mode 100644 index f3a8502..0000000 --- a/v2/iorw.py +++ /dev/null @@ -1,101 +0,0 @@ -# iorw.py Emulate a device which can read and write one character at a time. - -# This requires the modified version of uasyncio (fast_io directory). -# Slow hardware is emulated using timers. -# MyIO.write() ouputs a single character and sets the hardware not ready. -# MyIO.readline() returns a single character and sets the hardware not ready. -# Timers asynchronously set the hardware ready. - -import io, pyb -import uasyncio as asyncio -import micropython -micropython.alloc_emergency_exception_buf(100) - -MP_STREAM_POLL_RD = const(1) -MP_STREAM_POLL_WR = const(4) -MP_STREAM_POLL = const(3) -MP_STREAM_ERROR = const(-1) - -def printbuf(this_io): - print(bytes(this_io.wbuf[:this_io.wprint_len]).decode(), end='') - -class MyIO(io.IOBase): - def __init__(self, read=False, write=False): - self.ready_rd = False # Read and write not ready - self.rbuf = b'ready\n' # Read buffer - self.ridx = 0 - pyb.Timer(4, freq = 5, callback = self.do_input) - self.wch = b'' - self.wbuf = bytearray(100) # Write buffer - self.wprint_len = 0 - self.widx = 0 - pyb.Timer(5, freq = 10, callback = self.do_output) - - # Read callback: emulate asynchronous input from hardware. - # Typically would put bytes into a ring buffer and set .ready_rd. - def do_input(self, t): - self.ready_rd = True # Data is ready to read - - # Write timer callback. Emulate hardware: if there's data in the buffer - # write some or all of it - def do_output(self, t): - if self.wch: - self.wbuf[self.widx] = self.wch - self.widx += 1 - if self.wch == ord('\n'): - self.wprint_len = self.widx # Save for schedule - micropython.schedule(printbuf, self) - self.widx = 0 - self.wch = b'' - - - def ioctl(self, req, arg): # see ports/stm32/uart.c - ret = MP_STREAM_ERROR - if req == MP_STREAM_POLL: - ret = 0 - if arg & MP_STREAM_POLL_RD: - if self.ready_rd: - ret |= MP_STREAM_POLL_RD - if arg & MP_STREAM_POLL_WR: - if not self.wch: - ret |= MP_STREAM_POLL_WR # Ready if no char pending - return ret - - # Test of device that produces one character at a time - def readline(self): - self.ready_rd = False # Cleared by timer cb do_input - ch = self.rbuf[self.ridx] - if ch == ord('\n'): - self.ridx = 0 - else: - self.ridx += 1 - return chr(ch) - - # Emulate unbuffered hardware which writes one character: uasyncio waits - # until hardware is ready for the next. Hardware ready is emulated by write - # timer callback. - def write(self, buf, off, sz): - self.wch = buf[off] # Hardware starts to write a char - return 1 # 1 byte written. uasyncio waits on ioctl write ready - -async def receiver(myior): - sreader = asyncio.StreamReader(myior) - while True: - res = await sreader.readline() - print('Received', res) - -async def sender(myiow): - swriter = asyncio.StreamWriter(myiow, {}) - await asyncio.sleep(5) - count = 0 - while True: - count += 1 - tosend = 'Wrote Hello MyIO {}\n'.format(count) - await swriter.awrite(tosend.encode('UTF8')) - await asyncio.sleep(2) - -myio = MyIO() -loop = asyncio.get_event_loop() -loop.create_task(receiver(myio)) -loop.create_task(sender(myio)) -loop.run_forever() diff --git a/v2/lowpower/README.md b/v2/lowpower/README.md deleted file mode 100644 index acaedc8..0000000 --- a/v2/lowpower/README.md +++ /dev/null @@ -1,496 +0,0 @@ -# A low power usayncio adaptation - -Release 0.13 17th Oct 2019 - -API changes: low power applications must now import `rtc_time_cfg` and set its -`enabled` flag. -`Latency` is now a functor rather than a class. - -This module is specific to Pyboards including the D series. - - 1. [Introduction](./README.md#1-introduction) - 2. [Installation](./README.md#2-installation) - 2.1 [Files](./README.md#21-files) - 3. [Low power uasyncio operation](./README.md#3-low-power-uasyncio-operation) - 3.1 [The official uasyncio package](./README.md#31-the-official-uasyncio-package) - 3.2 [The low power adaptation](./README.md#32-the-low-power-adaptation) - 3.2.1 [Consequences of stop mode](./README.md#321-consequences-of-stop-mode) - 3.2.1.1 [Timing Accuracy and rollover](./README.md#3211-timing-accuracy-and-rollover) - 3.2.1.2 [USB](./README.md#3212-usb) - 3.2.2 [Measured results Pyboard 1](./README.md#322-measured-results-pyboard-1) - 3.2.3 [Current waveforms Pyboard 1](./README.md#323-current-waveforms-pyboard-1) - 3.2.4 [Pyboard D measurements](./README.md#324-pyboard-d-measurements) - 4. [The rtc_time module](./README.md#4-the-rtc_time-module) - 4.1 [rtc_time_cfg](./README.md#41-rtc_time_cfg) - 5. [Application design](./README.md#5-application-design) - 5.1 [Hardware](./README.md#51-hardware) - 5.2 [Application Code](./README.md#52-application-code) - 6. [Note on the design](./README.md#6-note-on-the-design) - -###### [Main README](../README.md) - -# 1. Introduction - -This adaptation is specific to the Pyboard and compatible platforms, namely -those capable of running the `pyb` module; this supports two low power modes -`standby` and `stop` -[see docs](http://docs.micropython.org/en/latest/pyboard/library/pyb.html). - -Use of `standby` is simple in concept: the application runs and issues -`standby`. The board goes into a very low power mode until it is woken by one -of a limited set of external events, when it behaves similarly to after a hard -reset. In that respect a `uasyncio` application is no different from any other. -If the application can cope with the fact that execution state is lost during -the delay, it will correctly resume. - -This adaptation modifies `uasyncio` such that it can enter `stop` mode for much -of the time, minimising power consumption while retaining state. The two -approaches can be combined, with a device waking from `shutdown` to run a low -power `uasyncio` application before again entering `shutdown`. - -The adaptation trades a reduction in scheduling performance for a substantial -reduction in power consumption. This tradeoff can be dynamically altered at -runtime. An application can wait with low power consumption for a trigger such -as a button push. Or it could periodically self-trigger by issuing -`await ayncio.sleep(long_time)`. For the duration of running the scheduler -latency can be reduced to improve performance at the cost of temporarily -higher power consumption, with the code reverting to low power mode while -waiting for a new trigger. - -Some general notes on low power Pyboard applications may be found -[here](https://github.com/peterhinch/micropython-micropower). - -###### [Contents](./README.md#a-low-power-usayncio-adaptation) - -# 2. Installation - -Ensure that the version of `uasyncio` in this repository is installed and -tested. Copy the files `rtc_time.py` and `rtc_time_cfg.py` to the device so -that they are on `sys.path`. - -## 2.1 Files - - * `rtc_time.py` Low power library. - * `rtc_time_cfg` Configuration file to enable `uasyncio` to use above. - * `lpdemo.py` A basic application which waits for a pushbutton to be pressed - before running. A second button press terminates it. While "off" and waiting - very low power is consumed. A normally open pushbutton should be connected - between `X1` and `Gnd`. This program is intended as a basic template for - similar applications. - * `howlow.py` A lower power version of the above. Polls the switch every 200ms - rather than running debouncing code. - * `lp_uart.py` Send and receive messages on UART4, echoing received messages - to UART1 at a different baudrate. This consumes about 1.4mA and serves to - demonstrate that interrupt-driven devices operate correctly. Requires a link - between pins X1 and X2 to enable UART 4 to receive data via a loopback. - * `mqtt_log.py` A publish-only MQTT application for Pyboard D. See below. - -`mqtt_log.py` requires the `umqtt.simple` library. This may be installed with -upip. See [Installing library modules](https://github.com/peterhinch/micropython-samples/tree/master/micropip). -``` ->>> import upip ->>> upip.install('micropython-umqtt.simple') -``` -Owing to [this issue](https://github.com/micropython/micropython/issues/5152) -this test is currently broken and I suspect that any usage of WiFi in low power -mode will fail. - -This test is "experimental". Pyboard D support for low power WiFi is currently -incomplete. I have seen anomolous results where power was low initially before -jumping to ~30mA after a few hours. The application continued to run, but the -evidence suggested that the WiFi chip was consuming power. See Damien's comment -in [this issue](https://github.com/micropython/micropython/issues/4686). -An option would be to shut down the WiFi chip after each connection. The need -periodically to reconnect would consume power, but in applications which log at -low rates this should avoid the above issue. Or wait for the firmware to mature. - -###### [Contents](./README.md#a-low-power-usayncio-adaptation) - -# 3 Low power uasyncio operation - -## 3.1 The official uasyncio package - -The official `uasyncio` library is unsuited to low power operation for two -reasons. Firstly because of its method of I/O polling. In periods when no task -is ready for execution, it determines the time when the most current task will -be ready to run. It then calls `select.poll`'s `ipoll` method with a timeout -calculated on that basis. This consumes power. - -The second issue is that it uses `utime`'s millisecond timing utilities for -timing. This ensures portability across MicroPython platforms. Unfortunately on -the Pyboard the clock responsible for `utime` stops for the duration of -`pyb.stop()`. If an application were to use `pyb.stop` to conserve power it -would cause `uasyncio` timing to become highly inaccurate. - -## 3.2 The low power adaptation - -If running on a Pyboard the version of `uasyncio` in this repo attempts to -import the file `rtc_time.py`. If this succeeds and there is no USB connection -to the board it derives its millisecond timing from the RTC; this continues to -run through `stop`. So code such as the following will behave as expected: -```python -async def foo(): - await asyncio.sleep(10) - bar() - await asyncio.sleep_ms(100) -``` -Libraries and applications using `uasyncio` will run unmodified. Code adapted -to invoke power saving (as described below) may exhibit reduced performance: -there is a tradeoff beween power consumption and speed. - -To avoid the power drain caused by `select.poll` the user code must issue the -following: - -```python -import rtc_time_cfg -rtc_time_cfg.enabled = True # Must be done before importing uasyncio - -import uasyncio as asyncio -try: - if asyncio.version[0] != 'fast_io': - raise AttributeError -except AttributeError: - raise OSError('This requires fast_io fork of uasyncio.') -from rtc_time import Latency - # Instantiate event loop with any args before running code that uses it -loop = asyncio.get_event_loop() -Latency(100) # Define latency in ms -``` - -`Latency` is a functor: its only interface is with function call syntax, which -takes a single argument being the `lightsleep` duration in ms. If the lowpower -mode is in operation the first call instantiates a coroutine with a -continuously running loop that executes `pyb.stop` before yielding with a zero -delay. The duration of the `lightsleep` condition can be dynamically varied by -further `Latency(time_in_ms)` calls. If the arg is zero the scheduler will run -at full speed. The `yield` allows each pending task to run once before the -scheduler is again paused (if the current latency value is > 0). - -The line -```python -rtc_time_cfg.enabled = True -``` -must be issued before importing `uasyncio` and before importing any modules -which use it, otherwise low-power mode will not be engaged. It is wise to do -this at the start of application code. - -###### [Contents](./README.md#a-low-power-usayncio-adaptation) - -### 3.2.1 Consequences of stop mode - -#### 3.2.1.1 Timing Accuracy and rollover - -A minor limitation is that the Pyboard 1.x RTC cannot resolve times of less -than 4ms so there is a theoretical reduction in the accuracy of delays. This -does not apply to the Pyboard D. This is somewhat academic. As explained in the -[tutorial](../TUTORIAL.md), issuing - -```python -await asyncio.sleep_ms(t) -``` - -specifies a minimum delay: the maximum may be substantially higher depending on -the behaviour of other tasks. Also the `latency` value will be added to `t`. - -RTC time rollover is at 7 days. The maximum supported `asyncio.sleep()` value -is 302399999 seconds (3.5 days - 1s). - -#### 3.2.1.2 USB - -Programs using `pyb.stop` disable the USB connection to the PC. This is -inconvenient for debugging so `rtc_time.py` detects an active USB connection -and disables power saving. This enables an application to be developed normally -via a USB connected PC. The board can then be disconnected from the PC and run -from a separate power source for power measurements, the application being -started by `main.py`. - -An active USB connection is one where a PC application is accessing the port: -an unused port can power the Pyboard and the library will assume low-power -mode. If the Pyboard is booted in safe mode to bypass `main.py` and the -application is started at the REPL, USB detection will disable low power mode -to keep the connection open. - -Applications can detect which timebase is in use by issuing: - -```python -import rtc_time_cfg -rtc_time_cfg.enabled = True # Must be done before importing uasyncio - -import uasyncio as asyncio -try: - if asyncio.version[0] != 'fast_io': - raise AttributeError -except AttributeError: - raise OSError('This requires fast_io fork of uasyncio.') -import rtc_time -if rtc_time.use_utime: - # Timebase is utime: either a USB connection exists or not a Pyboard -else: - # Running on RTC timebase with no USB connection -``` - -Debugging at low power is facilitated by using `pyb.repl_uart` with an FTDI -adaptor. - -###### [Contents](./README.md#a-low-power-usayncio-adaptation) - -### 3.2.2 Measured results Pyboard 1 - -The `lpdemo.py` script consumes a mean current of 980μA with 100ms latency, and -730μA with 200ms latency, while awaiting a button press. - -The following script consumes about 380μA between wakeups (usb is disabled in -`boot.py`): - -```python -import pyb -for pin in [p for p in dir(pyb.Pin.board) if p[0] in 'XY']: - pin_x = pyb.Pin(pin, pyb.Pin.IN, pyb.Pin.PULL_UP) -rtc = pyb.RTC() -rtc.wakeup(10000) -while True: - pyb.stop() -``` - -This accords with the 500μA maximum specification for `stop`. So current -consumption can be estimated by -`i = ib + n/latency` -`ib` is the stopped current (in my case 380μA). -`n` is a factor dependent on the amount of code which runs when the latency -period expires. - -A data logging application might tolerate latencies of many seconds while -waiting for a long delay to expire: getting close to `ib` may be practicable -for such applications during their waiting period. - -### 3.2.3 Current waveforms Pyboard 1 - -Running `lpdemo.py` while it waits for a button press with latency = 200ms. -It consumes 380μA except for brief peaks while polling the switch. -Vertical 20mA/div -Horizontal 50ms/div -![Image](./current.png) - -The following shows that peak on a faster timebase. This type of waveform is -typical that experienced when Python code is running. -Vertical 20mA/div -Horizontal 500μs/div -![Image](./current1.png) - -### 3.2.4 Pyboard D measurements - -As of this release the `lpdemo.py` script consumes around 1.1mA. I believe this -can be reduced because some unused pins are floating. When I discover which -pins can be set to input with pullups as per the Pyboard 1.x implementation I -hope to see figures comparable to Pyboard 1.x. - -###### [Contents](./README.md#a-low-power-usayncio-adaptation) - -# 4. The rtc_time module - -This provides the following. - -Variables (treat as read-only): - * `use_utime` `True` if the `uasyncio` timebase is `utime`, `False` if it is - the RTC. - * `d_series` `True` if running on Pyboard D, `False` if on Pyboard 1.x. - -Functions: -If the timebase is `utime` these are references to the corresponding `utime` -functions. Otherwise they are direct replacements but using the RTC as their -timebase. See the `utime` -[official documentation](http://docs.micropython.org/en/latest/pyboard/library/utime.html) -for these. - * `ticks_ms` - * `ticks_add` - * `ticks_diff` - -It also exposes `sleep_ms`. This is always a reference to `utime.sleep_ms`. The -reason is explained in the code comments. It is recommended to use the `utime` -method explicitly if needed. - -Latency Class: - * Constructor: Positional args `loop` - the event loop, `t_ms=100` - period - for which the scheduler enters `stop` i.e. initial latency period. - * Method: `value` Arg `val=None`. Controls period for which scheduler - stops. It returns the period in ms prior to any change in value. If the - default `None` is passed the value is unchanged. If 0 is passed the scheduler - runs at full speed. A value > 0 sets the stop period in ms. - -The higher the value, the greater the latency experienced by other tasks and -by I/O. Smaller values will result in higher power consumption with other tasks -being scheduled more frequently. - -The class is a singleton consequently there is no need to pass an instance -around or to make it global. Once instantiated, latency may be changed by - -```python -Latency(t) -``` - -## 4.1 rtc_time_cfg - -This consists of the following: -```python -enabled = False -disable_3v3 = False -disable_leds = False -disable_pins = False -``` -These variables may selectively be set `True` by the application prior to -importing `uasyncio`. Setting `enabled` is mandatory if low power mode is to be -engaged. The other variables control the 3.3V regulator, the LED drivers and -GPIO pins: the latter may be set to inputs with pulldown resistors to minimise -current draw. Unfortunately at the time of writing this feature seems to have -a fatal effect. I am investigating. - -###### [Contents](./README.md#a-low-power-usayncio-adaptation) - -# 5. Application design - -Attention to detail is required to minimise power consumption, both in terms of -hardware and code. The only *required* change to application code is to add - -```python -import rtc_time_cfg -rtc_time_cfg.enabled = True # Must be done before importing uasyncio - -import uasyncio as asyncio -try: - if asyncio.version[0] != 'fast_io': - raise AttributeError -except AttributeError: - raise OSError('This requires fast_io fork of uasyncio.') - # Do this import before configuring any pins or I/O: -from rtc_time import Latency - # Instantiate event loop with any args before running code that uses it: -loop = asyncio.get_event_loop() -Latency(100) # Define latency in ms - # Run application code -``` - -However optimising the power draw/performance tradeoff benefits from further -optimisations. - -## 5.1 Hardware - -Hardware issues are covered [here](https://github.com/peterhinch/micropython-micropower). -To summarise an SD card consumes on the order of 150μA. For lowest power -consumption use the onboard flash memory. Peripherals usually consume power -even when not in use: consider switching their power source under program -control. - -Floating Pyboard I/O pins can consume power. Further there are 4.7KΩ pullups on -the I2C pins. The `rtc_time` module sets all pins as inputs with internal -pullups. The application should import `rtc_time` before configuring any pins -or instantiating any drivers which use pins. If I2C is to be used there are -implications regarding the onboard pullups: see the above reference. - -## 5.2 Application Code - -The Pyboard has only one RTC and the `Latency` class needs sole use of -`pyb.stop` and `rtc.wakeup`; these functions should not be used in application -code. Setting the RTC at runtime is likely to be problematic: the effect on -scheduling can be assumed to be malign. If required, the RTC should be set -prior to instantiating the event loop. - -For short delays use `utime.sleep_ms` or `utime.sleep_us`. Such delays use -power and hog execution preventing other tasks from running. - -A task only consumes power when it runs: power may be reduced by using larger -values of `t` in - -```python -await asyncio.sleep(t) -``` - -The implications of the time value of the `Latency` instance should be -considered. During periods when the Pyboard is in a `stop` state, other tasks -will not be scheduled. I/O from interrupt driven devices such as UARTs will be -buffered for processing when stream I/O is next scheduled. The size of buffers -needs to be determined in conjunction with data rates and the latency period. - -Long values of latency affect the minimum time delays which can be expected of -`await asyncio.sleep_ms`. Such values will affect the aggregate amount of CPU -time any task will acquire in any period. If latency is 200ms the task - -```python -async def foo(): - while True: - # Do some processing - await asyncio.sleep(0) -``` - -will execute (at best) at a rate of 5Hz; possibly less, depending on the -behaviour of competing tasks. Likewise with 200ms latency - -```python -async def bar(): - while True: - # Do some processing - await asyncio.sleep_ms(10) -``` - -the 10ms sleep will be >=200ms dependent on other application tasks. - -Latency may be changed dynamically by issuing `Latency(time_in_ms)`. A typical -application (as in `howlow.py`) might wait on a "Start" button with a high -latency value, before running the application code with a lower (or zero) -latency. On completion it could revert to waiting for "Start" with high latency -to conserve battery. Logging applications might pause for a duration or wait on -a specific RTC time with a high latency value. - -Pyboard D users should note that firmware support for low power WiFi is -incomplete. Consider turning off the WiFi chip when not in use: -``` -sta_if = network.WLAN() -while True: - # Wait for trigger - sta_if.active(True) # Enable WiFi - sta_if.connect(SSID, PW) - # Use the network - sta_if.deinit() # Turns off WiFi chip -``` -[ref](https://github.com/micropython/micropython/issues/4681) - -###### [Contents](./README.md#a-low-power-usayncio-adaptation) - -# 6. Note on the design - -This module uses the old `pyb` in preference to `machine`. This is because the -Pyboard 1.x `machine` module does not have an `RTC` class. - -The `rtc_time` module represents a compromise designed to minimise changes to -`uasyncio`. The aim is to have zero effect on the performance of applications -not using `rtc_time` or ones running on non-Pyboard hardware. - -An alternative approach is to modify the `PollEventLoop` `wait` method to -invoke `stop` conditions when required. It would have the advantage of removing -the impact of latency on `sleep_ms` times. It proved rather involved and was -abandoned on the grounds of its impact on performance of normal applications. -Despite its name, `.wait` is time-critical in the common case of a zero delay; -increased code is best avoided. - -The approach used ensures that there is always at least one task waiting on a -zero delay. This guarantees that `PollEventLoop` `wait` is always called with a -zero delay: consequently `self.poller.ipoll(delay, 1)` will always return -immediately minimising power consumption. Consequently there is no change to -the design of the scheduler beyond the use of a different timebase. It does, -however, rely on the fact that the scheduler algorithm behaves as described -above. - -By default `uasyncio` uses the `utime` module for timing. For the timing to be -derived from the RTC the following conditions must be met: - * Hardware must be a Pyboard 1.x, Pyboard D or compatible (i.e. able to use - the `pyb` module). - * The application must import `rtc_time_cfg` and set its `enabled` flag `True` - before importing `uasyncio`. - * `uasyncio` must be the `fast_io` version 2.4 or later. - * The `rtc_time` module must be on the MicroPython search path. - * There must be no active USB connection. - -These constraints ensure there is no performance penalty unless an application -specifically requires micropower operation. They also enable a USB connection -to work if required for debugging. - -###### [Contents](./README.md#a-low-power-usayncio-adaptation) diff --git a/v2/lowpower/current.png b/v2/lowpower/current.png deleted file mode 100644 index df45b56bf72c3cfadb6ffce1e38e34d1a549745f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9794 zcmeHNc~nzp77s*7fGA022^azd6$_X?7BwIU5|V%mf`Sc*HY|y6C~C0ih+5eqI|&v= zL8^fuLW&Bw6dBJc2_Rsg5|mwUhO z{_gMozMJCX&6uujtc^e*rh9pMtVSTznFxehCR!c-r0>g=%Lv3=EiaFyemn9)S*#w{ zRJwegF--*OWw54Q-l&kv&O9F$AhXSn%G+oXV)bJ=hL4e6dOr8xPz!sa};6A~4ePou-nBN8;DvHPCW4Q1)% zY@_uy_Ek(yRC&|GkQ{lPohXexmM_{3WEe+4>2~zBX3=KiHhcfu&g1xop-17)Gw(w_ zvQ*K`=djg;7ARb;jNMrdy8Qc+;7k-QV9R(pHS|h2J{FtHn=+C8e!X7dHFeEn4BPNG zynfWaG`3=D3CCq$4iE4oFJTL|D%JPNyua^XzKOht_tkIk)#6LX^3u{Gvga zXA0(bGt%RXaYq$+^=jv58*ODN5oIv*IO59a2}$v-c`2c8m= zYs_>>6?eydOZMmd?T{RE6kSy_KGqT@yXY!hB|sDcUdI<`#x9=7MJgw@fdcLshUfRh zwoEbFoCU7sQ2`;md#knRSG&NS#Fdwkxko;vzztNu!fiB4RV6DbQUWl85Gc{3Xa^VF z!`;qK49eKdv&K>C3uu=ms0uBLO6!RM{gXs#I&?!r%)vShVJXV{GL$5gB@yrgrB8EH zIi}#{tnn>_O1Md0XJcPm{jkwaWGPEOVq?FvqG`f!e0Ek~LIf_ksgjFz=!cN#w(k?4 zdnD|YMmz2x_Ut%MJ-(Yxe|Uw8NS~3xf1k}bneWRA`9S_M5ck?*bkYV2Ona7WhBpSh zx;F|R$n)i+G2aNPNW{`(p+>^PYW+4S-`|8!Cgax!0YL_au@!e?Y8&lfFj68A#z1B! z{%P!_W_a-!tj?8nc2rESsY?K-J1j?@#ip;yHk!Jbiow)A$YLk9G}GJoa8uY5(~{_6||}MOdfgo__1zX62KW* zB7rFeOg|HrPmfe{>F!-1=7foj23f|&0&D5gj#Cguq)`t_Yy-I%mPAqwsMHf7%uDbu zV?sQgcIjo1NEl>0c&G-+F z|HOiDhe4?laHCAyKIn@pY}2+@b?WI>yoLy|7(c|6-lq|%xa~W18}#QvNo%(aQfqEN z5aQBK2xl3IF$a{812An8iWMWfhzbFtKm)>i$(Jgqst)vqm2?pIB<<>y*WNfv+8zC55OzI1ymZ2R9LwHsl^%Yl=eG@t{4%Md>K!*2Lp%Wcra>+Y8?9 z_yTsl4^qhrh4ZuHVpYu&1-+9e^4jU(r7M{2@^-XFqP1`QcE*X+r%;P{1|(4S4^N!} z?z?t#b6_P;+1qQTv9Z_xIbjW{reg5L6W+LdeC(O5e;~}WNw^UzC<-Zyv{ioz_3J^` zDt)?5JHm`8(o_BFkV#=sgnpZFo4uX;L85vq^vMK2lUvc0siw>pxEr?vEoE<`(?m#C z{i?L>C&V+Sx{Rrgd)!n9cT1VsiyNX6d_+)stirXwKU+Z7>ti?uX!4&vHtFDRMSX6# zjz$6Rq{EVR5~9a-GIXp_fN`OBb}(W~0ZgM<_}D*XM{lyqc$IcEZ~zu$AnRp_(MgA7 zMv25XWWnz(2!GjfzTr_ZL$qaBS^t-cE0ujg#^>s{DXVu4s~j_J{7>Xy>gPeu{=@Z* zxMr{mmB9yn@6Vsa!YsDpD1L;)%0CTtrXMd;8a)v3XMQ}T_R9Jys2WgioSJr6GV5o# zvzdd`ZkW*A@aQeNH!N-=Tp|N;3RCy%ZP1{Pmxp{@0+$#`D%Q8}T{(L%(L9f^n)T++ z7@fm^gIySC-V@Fb^h^$W1T|)2Y*n^G^+WuEEj@2idzz^ZA zjLtqzGCNt(qkG~@kY~32s=JQS+^j+!V`sg?_Qlve8!coZ;@KO%D5>yc_?_X?$qb!v zP42PZkD_OlJSLLXP>@Kqx>~#^hIE%yYHWKm47A+9i}1+*8q)Rnqo&&J+XGQDr;m6oc5Y#95r8LNi$gP4ngM3>d3{I-#iuB_}T`~r=^;y~q z*EloV1Lo;9Ew7+y26bQ}(28EXVnGAx(9ayVvwXF~p=D0UHQ1~;e_0IXH;W&7W7OB% zk~dmdC!96{2#H=K>x>8r~2w~hLsm$waPkQ3Yvd{Qgr-FQ=w3u zIV)lrhHIaLh^vwv*IMle|5yXf#O=oLcBdJTnVb4z)Q_8EiX*$AY1^raETT4)k{qEK zvH8_14^UBm|J$$wu%c*vu_Gdk&0UC0tO2~j>X_3d;TA=q+;Uq9G_$9a%IU2)&}cBW z)xU=V!xi_?)|$6DAx`?%)#5(aa}O`FHTI+d-9xh#LVfd^Oy}h7dg<|PgOd`?3CKgJ zq*mP5a6^n)JR~K6G|#Ox$~$8Yw=4+1;`qbR3_dnP!?*A;ulHzSrWkJgrluRlER4Q= z`ZG14GL9#m|5pPl?-o#VoPOJf&TW~-GgR|l1-MkitghhG=WhU}@N5{ui{|Zd(w!Cm EUmxY~UH||9 diff --git a/v2/lowpower/current1.png b/v2/lowpower/current1.png deleted file mode 100644 index feec09142598a3ad6b5438f90803d4700d0bf67b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10620 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 0: - pyb.stop() - # Pending tasks run once, may change self._t_ms - yield - if t_ms != self._t_ms: # Has changed: update wakeup - t_ms = self._t_ms - if t_ms > 0: - rtc.wakeup(t_ms) - else: - rtc.wakeup(None) - - def __call__(self, t_ms=None): - v = self._t_ms - if t_ms is not None: - self._t_ms = max(t_ms, 0) - return v diff --git a/v2/lowpower/rtc_time_cfg.py b/v2/lowpower/rtc_time_cfg.py deleted file mode 100644 index c7c7d5e..0000000 --- a/v2/lowpower/rtc_time_cfg.py +++ /dev/null @@ -1,5 +0,0 @@ -# rtc_time_cfg.py -enabled = False -disable_3v3 = False -disable_leds = False -disable_pins = False diff --git a/v2/nec_ir/README.md b/v2/nec_ir/README.md deleted file mode 100644 index 33be026..0000000 --- a/v2/nec_ir/README.md +++ /dev/null @@ -1,133 +0,0 @@ -# Decoder for IR Remote Controls using the NEC protocol - -This protocol is widely used. An example remote is [this one](https://www.adafruit.com/products/389). -To interface the device a receiver chip such as the Vishay TSOP4838 or the -[adafruit one](https://www.adafruit.com/products/157) is required. This -demodulates the 38KHz IR pulses and passes the demodulated pulse train to the -microcontroller. - -The driver and test programs run on the Pyboard and ESP8266. - -# Files - - 1. `aremote.py` The device driver. - 2. `art.py` A test program to characterise a remote. - 3. `art1.py` Control an onboard LED using a remote. The data and addresss - values need changing to match your characterised remote. - -# Dependencies - -The driver requires the `uasyncio` library and the file `asyn.py` from this -repository. - -# Usage - -The pin used to connect the decoder chip to the target is arbitrary but the -test programs assume pin X3 on the Pyboard and pin 13 on the ESP8266. - -The driver is event driven. Pressing a button on the remote causes a user -defined callback to be run. The NEC protocol returns a data value and an -address. These are passed to the callback as the first two arguments (further -user defined arguments may be supplied). The address is normally constant for a -given remote, with the data corresponding to the button. Applications should -check the address to ensure that they only respond to the correct remote. - -Data values are 8 bit. Addresses may be 8 or 16 bit depending on whether the -remote uses extended addressing. - -If a button is held down a repeat code is sent. In this event the driver -returns a data value of `REPEAT` and the address associated with the last -valid data block. - -To characterise a remote run `art.py` and note the data value for each button -which is to be used. If the address is less than 256, extended addressing is -not in use. - -# Reliability - -IR reception is inevitably subject to errors, notably if the remote is operated -near the limit of its range, if it is not pointed at the receiver or if its -batteries are low. So applications must check for, and usually ignore, errors. -These are flagged by data values < `REPEAT`. - -On the ESP8266 there is a further source of errors. This results from the large -and variable interrupt latency of the device which can exceed the pulse -duration. This causes pulses to be missed. This tendency is slightly reduced by -running the chip at 160MHz. - -In general applications should provide user feedback of correct reception. -Users tend to press the key again if no acknowledgement is received. - -# The NEC_IR class - -The constructor takes the following positional arguments. - - 1. `pin` A `Pin` instance for the decoder chip. - 2. `cb` The user callback function. - 3. `extended` Set `False` to enable extra error checking if the remote - returns an 8 bit address. - 4. Further arguments, if provided, are passed to the callback. - -The callback receives the following positional arguments: - - 1. The data value returned from the remote. - 2. The address value returned from the remote. - 3. Any further arguments provided to the `NEC_IR` constructor. - -Negative data values are used to signal repeat codes and transmission errors. - -The test program `art1.py` provides an example of a minimal application. - -# How it works - -The NEC protocol is described in these references. -[altium](http://techdocs.altium.com/display/FPGA/NEC+Infrared+Transmission+Protocol) -[circuitvalley](http://www.circuitvalley.com/2013/09/nec-protocol-ir-infrared-remote-control.html) - -A normal burst comprises exactly 68 edges, the exception being a repeat code -which has 4. An incorrect number of edges is treated as an error. All bursts -begin with a 9ms pulse. In a normal code this is followed by a 4.5ms space; a -repeat code is identified by a 2.25ms space. A data burst lasts for 67.5ms. - -Data bits comprise a 562.5µs mark followed by a space whose length determines -the bit value. 562.5µs denotes 0 and 1.6875ms denotes 1. - -In 8 bit address mode the complement of the address and data values is sent to -provide error checking. This also ensures that the number of 1's and 0's in a -burst is constant, giving a constant burst length of 67.5ms. In extended -address mode this constancy is lost. The burst length can (by my calculations) -run to 76.5ms. - -A pin interrupt records the time of every state change (in µs). The first -interrupt in a burst sets an event, passing the time of the state change. A -coroutine waits on the event, yields for the duration of a data burst, then -decodes the stored data before calling the user-specified callback. - -Passing the time to the `Event` instance enables the coro to compensate for -any asyncio latency when setting its delay period. - -The algorithm promotes interrupt handler speed over RAM use: the 276 bytes used -for the data array could be reduced to 69 bytes by computing and saving deltas -in the interrupt service routine. - -# Error returns - -Data values passed to the callback are normally positive. Negative values -indicate a repeat code or an error. - -`REPEAT` A repeat code was received. - -Any data value < `REPEAT` denotes an error. In general applications do not -need to decode these, but they may be of use in debugging. For completeness -they are listed below. - -`BADSTART` A short (<= 4ms) start pulse was received. May occur due to IR -interference, e.g. from fluorescent lights. The TSOP4838 is prone to producing -200µs pulses on occasion, especially when using the ESP8266. -`BADBLOCK` A normal data block: too few edges received. Occurs on the ESP8266 -owing to high interrupt latency. -`BADREP` A repeat block: an incorrect number of edges were received. -`OVERRUN` A normal data block: too many edges received. -`BADDATA` Data did not match check byte. -`BADADDR` Where `extended` is `False` the 8-bit address is checked -against the check byte. This code is returned on failure. diff --git a/v2/nec_ir/aremote.py b/v2/nec_ir/aremote.py deleted file mode 100644 index 4ba91fc..0000000 --- a/v2/nec_ir/aremote.py +++ /dev/null @@ -1,124 +0,0 @@ -# aremote.py Decoder for NEC protocol IR remote control -# e.g.https://www.adafruit.com/products/389 - -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license - -from sys import platform -import uasyncio as asyncio -from asyn import Event -from micropython import const -from array import array -from utime import ticks_us, ticks_diff -if platform == 'pyboard': - from pyb import Pin, ExtInt -else: - from machine import Pin - -ESP32 = platform == 'esp32' or platform == 'esp32_LoBo' - -# Save RAM -# from micropython import alloc_emergency_exception_buf -# alloc_emergency_exception_buf(100) - -# Result codes (accessible to application) -# Repeat button code -REPEAT = -1 -# Error codes -BADSTART = -2 -BADBLOCK = -3 -BADREP = -4 -OVERRUN = -5 -BADDATA = -6 -BADADDR = -7 - -_EDGECOUNT = const(68) # No. of edges in data block - - -# On 1st edge start a block timer. When it times out decode the data. Time must -# exceed the worst case block transmission time, but (with asyncio latency) be -# less than the interval between a block start and a repeat code start (108ms) -# Value of 73 allows for up to 35ms latency. -class NEC_IR(): - def __init__(self, pin, callback, extended, *args): # Optional args for callback - self._ev_start = Event() - self._callback = callback - self._extended = extended - self._addr = 0 - self.block_time = 80 if extended else 73 # Allow for some tx tolerance (?) - self._args = args - self._times = array('i', (0 for _ in range(_EDGECOUNT + 1))) # +1 for overrun - if platform == 'pyboard': - ExtInt(pin, ExtInt.IRQ_RISING_FALLING, Pin.PULL_NONE, self._cb_pin) - else: # PR5962 ESP8266 hard IRQ's not supported - pin.irq(handler = self._cb_pin, trigger = (Pin.IRQ_FALLING | Pin.IRQ_RISING)) - #elif ESP32: - #pin.irq(handler = self._cb_pin, trigger = (Pin.IRQ_FALLING | Pin.IRQ_RISING)) - #else: - #pin.irq(handler = self._cb_pin, trigger = (Pin.IRQ_FALLING | Pin.IRQ_RISING), hard = True) - self._edge = 0 - self._ev_start.clear() - loop = asyncio.get_event_loop() - loop.create_task(self._run()) - - async def _run(self): - loop = asyncio.get_event_loop() - while True: - await self._ev_start # Wait until data collection has started - # Compensate for asyncio latency - latency = ticks_diff(loop.time(), self._ev_start.value()) - await asyncio.sleep_ms(self.block_time - latency) # Data block should have ended - self._decode() # decode, clear event, prepare for new rx, call cb - - # Pin interrupt. Save time of each edge for later decode. - def _cb_pin(self, line): - t = ticks_us() - # On overrun ignore pulses until software timer times out - if self._edge <= _EDGECOUNT: # Allow 1 extra pulse to record overrun - if not self._ev_start.is_set(): # First edge received - loop = asyncio.get_event_loop() - self._ev_start.set(loop.time()) # asyncio latency compensation - self._times[self._edge] = t - self._edge += 1 - - def _decode(self): - overrun = self._edge > _EDGECOUNT - val = OVERRUN if overrun else BADSTART - if not overrun: - width = ticks_diff(self._times[1], self._times[0]) - if width > 4000: # 9ms leading mark for all valid data - width = ticks_diff(self._times[2], self._times[1]) - if width > 3000: # 4.5ms space for normal data - if self._edge < _EDGECOUNT: - # Haven't received the correct number of edges - val = BADBLOCK - else: - # Time spaces only (marks are always 562.5µs) - # Space is 1.6875ms (1) or 562.5µs (0) - # Skip last bit which is always 1 - val = 0 - for edge in range(3, _EDGECOUNT - 2, 2): - val >>= 1 - if ticks_diff(self._times[edge + 1], self._times[edge]) > 1120: - val |= 0x80000000 - elif width > 1700: # 2.5ms space for a repeat code. Should have exactly 4 edges. - val = REPEAT if self._edge == 4 else BADREP - addr = 0 - if val >= 0: # validate. Byte layout of val ~cmd cmd ~addr addr - addr = val & 0xff - cmd = (val >> 16) & 0xff - if addr == ((val >> 8) ^ 0xff) & 0xff: # 8 bit address OK - val = cmd if cmd == (val >> 24) ^ 0xff else BADDATA - self._addr = addr - else: - addr |= val & 0xff00 # pass assumed 16 bit address to callback - if self._extended: - val = cmd if cmd == (val >> 24) ^ 0xff else BADDATA - self._addr = addr - else: - val = BADADDR - if val == REPEAT: - addr = self._addr # Last valid addresss - self._edge = 0 # Set up for new data burst and run user callback - self._ev_start.clear() - self._callback(val, addr, *self._args) diff --git a/v2/nec_ir/art.py b/v2/nec_ir/art.py deleted file mode 100644 index c861a50..0000000 --- a/v2/nec_ir/art.py +++ /dev/null @@ -1,47 +0,0 @@ -# art.py Test program for IR remote control decoder aremote.py -# Supports Pyboard and ESP8266 - -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license - -# Run this to characterise a remote. - -from sys import platform -import uasyncio as asyncio -ESP32 = platform == 'esp32' or platform == 'esp32_LoBo' - -if platform == 'pyboard': - from pyb import Pin -elif platform == 'esp8266' or ESP32: - from machine import Pin, freq -else: - print('Unsupported platform', platform) - -from aremote import * - -errors = {BADSTART : 'Invalid start pulse', BADBLOCK : 'Error: bad block', - BADREP : 'Error: repeat', OVERRUN : 'Error: overrun', - BADDATA : 'Error: invalid data', BADADDR : 'Error: invalid address'} - -def cb(data, addr): - if data == REPEAT: - print('Repeat') - elif data >= 0: - print(hex(data), hex(addr)) - else: - print('{} Address: {}'.format(errors[data], hex(addr))) - -def test(): - print('Test for IR receiver. Assumes NEC protocol.') - if platform == 'pyboard': - p = Pin('X3', Pin.IN) - elif platform == 'esp8266': - freq(160000000) - p = Pin(13, Pin.IN) - elif ESP32: - p = Pin(23, Pin.IN) - ir = NEC_IR(p, cb, True) # Assume r/c uses extended addressing - loop = asyncio.get_event_loop() - loop.run_forever() - -test() diff --git a/v2/nec_ir/art1.py b/v2/nec_ir/art1.py deleted file mode 100644 index ae1978d..0000000 --- a/v2/nec_ir/art1.py +++ /dev/null @@ -1,60 +0,0 @@ -# art1.py Test program for IR remote control decoder aremote.py -# Supports Pyboard and ESP8266 - -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license - -# This uses a pair of buttons to turn an on-board LED on and off. Its aim is -# to enable you to decide if the reliability on the ESP8266 is adequate for -# your needs. - -from sys import platform -import uasyncio as asyncio -ESP32 = platform == 'esp32' or platform == 'esp32_LoBo' -if platform == 'pyboard': - from pyb import Pin, LED -elif platform == 'esp8266' or ESP32: - from machine import Pin, freq -else: - print('Unsupported platform', platform) - -from aremote import NEC_IR, REPEAT - -def cb(data, addr, led): - if addr == 0x40: # Adapt for your remote - if data == 1: # Button 1. Adapt for your remote/buttons - print('LED on') - if platform == 'pyboard': - led.on() - else: - led(0) - elif data == 2: - print('LED off') - if platform == 'pyboard': - led.off() - else: - led(1) - elif data < REPEAT: - print('Bad IR data') - else: - print('Incorrect remote') - -def test(): - print('Test for IR receiver. Assumes NEC protocol. Turn LED on or off.') - if platform == 'pyboard': - p = Pin('X3', Pin.IN) - led = LED(2) - elif platform == 'esp8266': - freq(160000000) - p = Pin(13, Pin.IN) - led = Pin(2, Pin.OUT) - led(1) - elif ESP32: - p = Pin(23, Pin.IN) - led = Pin(21, Pin.OUT) # LED with 220Ω series resistor between 3.3V and pin 21 - led(1) - ir = NEC_IR(p, cb, True, led) # Assume extended address mode r/c - loop = asyncio.get_event_loop() - loop.run_forever() - -test() diff --git a/v2/roundrobin.py b/v2/roundrobin.py deleted file mode 100644 index 0bb8b0d..0000000 --- a/v2/roundrobin.py +++ /dev/null @@ -1,37 +0,0 @@ -# roundrobin.py Test/demo of round-robin scheduling -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license - -# Result on Pyboard with print('Foo', n) commented out -# executions/second: -# Using yield: 4249 -# Using sleep_ms(0) 2750 -# Note using yield in a coro is "unofficial" and may not -# work in future uasyncio revisions. - -import uasyncio as asyncio - -count = 0 -period = 5 - - -async def foo(n): - global count - while True: -# yield - await asyncio.sleep_ms(0) - count += 1 - print('Foo', n) - - -async def main(delay): - print('Testing for {} seconds'.format(delay)) - await asyncio.sleep(delay) - - -loop = asyncio.get_event_loop() -loop.create_task(foo(1)) -loop.create_task(foo(2)) -loop.create_task(foo(3)) -loop.run_until_complete(main(period)) -print('Coro executions per sec =', count/period) diff --git a/v2/sock_nonblock.py b/v2/sock_nonblock.py deleted file mode 100644 index 2f44464..0000000 --- a/v2/sock_nonblock.py +++ /dev/null @@ -1,110 +0,0 @@ -# sock_nonblock.py Illustration of the type of code required to use nonblocking -# sockets. It is not a working demo and probably has silly errors. -# It is intended as an outline of requirements and also to illustrate some of the -# nasty hacks required on current builds of ESP32 firmware. Platform detection is -# done at runtime. -# If running on ESP8266 these hacks can be eliminated. -# Working implementations may be found in the asynchronous MQTT library. -# https://github.com/peterhinch/micropython-mqtt - -# Author: Peter Hinch -# Copyright Peter Hinch 2018 Released under the MIT license - -import usocket as socket -import network -import machine -import sys -from micropython import const -from uerrno import EINPROGRESS, ETIMEDOUT -from utime import ticks_ms, ticks_diff, sleep_ms - -ESP32 = sys.platform == 'esp32' - -BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT] - -# ESP32. It is not enough to regularly yield to RTOS with machine.idle(). There are -# two cases where an explicit sleep() is required. Where data has been written to the -# socket and a response is awaited, a timeout may occur without a >= 20ms sleep. -# Secondly during WiFi connection sleeps are required to prevent hangs. -if ESP32: - # https://forum.micropython.org/viewtopic.php?f=16&t=3608&p=20942#p20942 - BUSY_ERRORS += [118, 119] # Add in weird ESP32 errors - # 20ms seems about the minimum before we miss data read from a socket. - def esp32_pause(): # https://github.com/micropython/micropython-esp32/issues/167 - sleep_ms(20) # This is horrible. -else: - esp32_pause = lambda *_ : None # Do nothing on sane platforms - -# How long to delay between polls. Too long affects throughput, too short can -# starve other coroutines. -_SOCKET_POLL_DELAY = const(5) # ms -_RESPONSE_TIME = const(30000) # ms. max server latency before timeout - -class FOO: - def __init__(self, server, port): - # On ESP32 need to submit WiFi credentials - self._sta_if = network.WLAN(network.STA_IF) - self._sta_if.active(True) - # Note that the following blocks, potentially for seconds, owing to DNS lookup - self._addr = socket.getaddrinfo(server, port)[0][-1] - self._sock = socket.socket() - self._sock.setblocking(False) - try: - self._sock.connect(addr) - except OSError as e: - if e.args[0] not in BUSY_ERRORS: - raise - if ESP32: # Revolting kludge :-( - loop = asyncio.get_event_loop() - loop.create_task(self._idle_task()) - - def _timeout(self, t): - return ticks_diff(ticks_ms(), t) > _RESPONSE_TIME - - # Read and return n bytes. Raise OSError on timeout ( caught by superclass). - async def _as_read(self, n): - sock = self._sock - data = b'' - t = ticks_ms() - while len(data) < n: - esp32_pause() # Necessary on ESP32 or we can time out. - if self._timeout(t) or not self._sta_if.isconnected(): - raise OSError(-1) - try: - msg = sock.read(n - len(data)) - except OSError as e: # ESP32 issues weird 119 errors here - msg = None - if e.args[0] not in BUSY_ERRORS: - raise - if msg == b'': # Connection closed by host (?) - raise OSError(-1) - if msg is not None: # data received - data = b''.join((data, msg)) - t = ticks_ms() # reset timeout - await asyncio.sleep_ms(_SOCKET_POLL_DELAY) - return data - - # Write a buffer - async def _as_write(self, bytes_wr): - sock = self._sock - t = ticks_ms() - while bytes_wr: - if self._timeout(t) or not self._sta_if.isconnected(): - raise OSError(-1) - try: - n = sock.write(bytes_wr) - except OSError as e: # ESP32 issues weird 119 errors here - n = 0 - if e.args[0] not in BUSY_ERRORS: - raise - if n: # Bytes still to write - t = ticks_ms() # Something was written: reset t/o - bytes_wr = bytes_wr[n:] - esp32_pause() # Precaution. How to prove whether it's necessary? - await asyncio.sleep_ms(_SOCKET_POLL_DELAY) - - # ESP32 kludge :-( - async def _idle_task(self): - while True: - await asyncio.sleep_ms(10) - machine.idle() # Yield to underlying RTOS diff --git a/v2/syncom_as/README.md b/v2/syncom_as/README.md deleted file mode 100644 index e32a943..0000000 --- a/v2/syncom_as/README.md +++ /dev/null @@ -1,242 +0,0 @@ -# Communication between MicroPython hardware boards - -This provides a means of communication between two devices, each running -MicroPython, where a UART cannot be used. An example is where one device is an -ESP8266 board. While this has one bidirectional UART, this may be in use either -as a REPL console, for viewing debug output, or for other puposes. - -It is intended for use in asynchronous programs and uses uasyncio. - -The module offers a bidirectional full duplex communication channel between two -hardware devices. Its unit of communication is an arbitrary Python object -making for simple application. In an alternative mode for resource constrained -devices, the unit of communication is a string. - -Physically it uses a 4-wire interface plus an additional wire to enable the -host to issue a hardware reset to the target in the event that the target -crashes or becomes unresponsive. Where the target is an ESP8266 this can occur -for various reasons including network issues where sockets can block -indefinitely. - -The module will run on devices with minimal features and makes no assumptions -about processing performance: at a physical level the interface is synchronous. -If each device has two pins which can be used for output and two for input and -supports uasyncio it should work. - -###### [Main README](./README.md) - -## Example usage - -```python -import uasyncio as asyncio -from syncom import SynCom -from machine import Pin - - # Task just echoes objects back -async def passive_task(chan): - while True: - obj = await chan.await_obj() - chan.send(obj) - -mtx = Pin(14, Pin.OUT, value = 0) # Define pins -mckout = Pin(15, Pin.OUT, value = 0) # clock must be initialised to zero. -mrx = Pin(13, Pin.IN) -mckin = Pin(12, Pin.IN) - -channel = SynCom(True, mckin, mckout, mrx, mtx) -loop = asyncio.get_event_loop() -loop.create_task(channel.start(passive_task)) -try: - loop.run_forever() -except KeyboardInterrupt: - pass -finally: - mckout(0) # For a subsequent run -``` - -## Advantages - - * Readily portable to any MicroPython platform. - * It does not use hardware features such as interrupts or timers. - * Hardware requirement: two arbitrary output pins and two input pins on each - device. - * The interface is synchronous, having no timing dependencies. - * It supports full duplex communications (concurrent send and receive). - * The unit of transmission is an arbitrary Python object. - * All methods are non-blocking. - * Small: <200 lines of Python. - -## Limitations - - * The interface is an alternative to I2C or SPI and is intended for directly - linked devices sharing a common power supply. - * It is slow. With a Pyboard linked to an ESP8266 clocked at 160MHz, the - peak bit rate is 1.6Kbps. Mean throughput is about 800bps. - In practice throughput will depend on the performance of the slowest device - and the behaviour of other tasks. - -## Rationale - -The obvious question is why not use I2C or SPI. The reason is the nature of the -slave interfaces: these protocols are designed for the case where the slave is -a hardware device which guarantees a timely response. The MicroPython slave -drivers achieve this by means of blocking system calls. Synchronising master -and slave is difficult because the master needs to ensure that the slave is -running the blocking call before transmitting. For the slave to do anything -useful the code must be designed to ensure that the call exits at the end of a -message. - -Further such blocking calls are incompatible with asynchronous programming. - -The two ends of the link are defined as ``initiator`` and ``passive``. These -describe their roles in initialisation. Once running the protocol is -symmetrical and the choice as to which unit to assign to each role is -arbitrary: the test programs assume that the Pyboard is the initiator. - -# Files - - * syncom.py The library. - * sr_init.py Test program configured for Pyboard: run with sr_passive.py on - the other device. - * sr_passive.py Test program configured for ESP8266: sr_init.py runs on other - end of link. - -# Hardware connections - -Each device has the following logical connections, ``din``, ``dout``, ``ckin``, -``ckout``. The ``din`` (data in) of one device is linked to ``dout`` (data out) -of the other, and vice versa. Likewise the clock signals ``ckin`` and ``ckout``. - -To enable a response to crash detection a pin on the Pyboard is connected to -the Reset pin on the target. The polarity of the reset pulse is definable in -code by virtue of the ``Signal`` object. The pins below are those used in the -test programs. - - -| Initiator | Passive | Pyboard | ESP8266 | -|:-----------:|:-----------:|:-------:|:-------:| -| reset (o/p) | reset (i/p) | Y4 | reset | -| dout (o/p) | din (i/p) | Y5 | 14 | -| ckout (o/p) | ckin (i/p) | Y6 | 15 | -| din (i/p) | dout (o/p) | Y7 | 13 | -| ckin (i/p) | ckout (o/p) | Y8 | 12 | - - -# Dependency - -Unless using string mode the Pickle module is required. - -[pickle.py](https://github.com/micropython/micropython-lib/tree/master/pickle) - -# class SynCom - -A SynCom instance is idle until its ``start`` task is scheduled. The driver -causes the host device to resets the target and wait for synchronisation. When -the interface is running the passed user task is launched; unless an error -occurs this runs forever using the interface as required by the application. If -crash detection is required the user task should check for a timeout. In this -event the user task should return. This causes the target to be reset and the -interface to re-synchronise. The user task is then re-launched. - -## Constructor - -Positional arguments: - - 1. ``passive`` Boolean. One end of the link sets this ``True``, the other - ``False``. - 2. ``ckin`` An initialised input ``Pin`` instance. - 3. ``ckout`` An initialised output ``Pin`` instance. It should be set to zero. - 4. ``din`` An initialised input ``Pin`` instance. - 5. ``dout`` An initialised output ``Pin`` instance. - 6. ``sig_reset`` (optional) default ``None``. A ``Signal`` instance. Should be - configured so that when ``True`` the target will be reset. - 7. ``timeout`` (optional) default 0. Units ms. See below. - 8. ``string_mode`` (optional) default ``False``. See String Mode below. - 9. ``verbose`` (optional) default ``True``. If set, debug messages will be - output to the REPL. - -## Synchronous Methods - - * ``send`` Argument an arbitrary Python object (or a string in string mode). - Puts the item on the queue for transmission. - * ``any`` No args. - Returns the number of received objects on the receive queue. - * ``running`` No args. - Returns ``True`` if the channel is running, ``False`` if the target has timed - out. - -## Asynchronous Methods (tasks) - - * ``await_obj`` Argument ``t_ms`` default 10ms. See below. - Wait for reception of a Python object or string and return it. If the - interface times out (because the target has crashed) return ``None``. - * ``start`` Optional args ``user_task``, ``fail_delay``. - Starts the interface. If a user_task is provided this will be launched when - synchronisation is achived. The user task should return if a timeout is - detected (by ``await_obj`` returning ``None``). On return the driver will wait - for ``fail_delay`` (see below) before asserting the reset signal to reset the - target. The user task will be re-launched when synchronisation is achieved. - The user_task is passed a single argument: the SynCom instance. If the user - task is a bound method it should therefore be declared as taking two args: - ``self`` and the channel. - -The ``fail_delay`` (in seconds) is a convenience to allow user tasks to -terminate before the user task is restarted. On detection of a timeout an -application should set a flag to cause tasks instantiated by the user task to -terminate, then issue ``return``. This avoids unlimited growth of the task -queue. - -The ``t_ms`` argument to ``await_obj`` determines how long the task pauses -between checks for received data. Longer intervals increase latency but -(possibly) improve raw throughput. - -# Notes - -## Synchronisation - -When the host launches the ``start`` coroutine it runs forever. It resets the -target which instantiates a SynCom object and launches its ``start`` coroutine. -The two then synchronise by repeatedly transmitting a ``_SYN`` character. Once -this has been received the link is synchronised and the user task is launched. - -The user task runs forever on the target. On the host it may return if a target -timeout is detected. In this instance the host's ``start`` task waits for the -optional ``fail_delay`` before resetting the target and re-synchronising the -interface. The user task, which ran to completion, is re-launched. - -## String Mode - -On resource constrained platforms the pickle module can be problematic: the -method used to convert a string to an arbitrary Python object involves invoking -the compiler which demands significant amounts of RAM. This can be avoided by -sending only strings to the resource constrained platform, which must then -interpret the strings as required by the application. The protocol places some -restrictions. The bytes must not include 0, and they are limited to 7 bits. The -latter limitation can be removed (with small performance penalty) by changing -the value of ``_BITS_PER_CH`` to 8. The limitations allow for normal UTF8 -strings. - -## Timing - -The timing measurements in Limitations above were performed as follows. A logic -analyser was connected to one of the clock signals and the time for one -character (7 bits) to be transferred was measured (note that a bit is -transferred on each edge of the clock). This produced figures for the raw bits -per second throughput of the bitbanged interface. - -The value produced by the test programs (sr_init.py and sr_passive.py) is the -total time to send an object and receive it having been echoed back by the -ESP8266. This includes encoding the object as a string, transmitting it, -decoding and modifying it, followed by similar processing to send it back. -Hence converting the figures to bps will produce a lower figure (on the order -of 656bps at 160MHz). - -## The Pickle module - -In normal mode the library uses the Python pickle module for object -serialisation. This has some restrictions, notably on the serialisation of user -defined class instances. See the Python documentation. Currently there is a -MicroPython issue #2280 where a memory leak occurs if you pass a string which -varies regularly. Pickle saves a copy of the string (if it hasn't already -occurred) each time until RAM is exhausted. The workround is to use any data -type other than strings or bytes objects; or to use string mode. diff --git a/v2/syncom_as/main.py b/v2/syncom_as/main.py deleted file mode 100644 index 3397298..0000000 --- a/v2/syncom_as/main.py +++ /dev/null @@ -1,4 +0,0 @@ -import webrepl -webrepl.start() -import sr_passive -sr_passive.test() diff --git a/v2/syncom_as/sr_init.py b/v2/syncom_as/sr_init.py deleted file mode 100644 index 8953751..0000000 --- a/v2/syncom_as/sr_init.py +++ /dev/null @@ -1,86 +0,0 @@ -# sr_init.py Test of synchronous comms library. Initiator end. - -# The MIT License (MIT) -# -# Copyright (c) 2016 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# Run on Pyboard -from machine import Pin, Signal -from pyb import LED -import uasyncio as asyncio -from utime import ticks_ms, ticks_diff -from syncom import SynCom, SynComError - - -async def initiator_task(channel): - while True: - so = ['test', 0, 0] - for x in range(4): # Test full duplex by sending 4 in succession - so[1] = x - channel.send(so) - await asyncio.sleep_ms(0) - while True: # Receive the four responses - si = await channel.await_obj() # Deal with queue - if si is None: - print('Timeout: restarting.') - return - print('initiator received', si) - if si[1] == 3: # received last one - break - while True: # At 2 sec intervals send an object and get response - await asyncio.sleep(2) - print('sending', so) - channel.send(so) - tim = ticks_ms() - so = await channel.await_obj() # wait for response - duration = ticks_diff(ticks_ms(), tim) - if so is None: - print('Timeout: restarting.') - return - print('initiator received', so, 'timing', duration) - -async def heartbeat(): - led = LED(1) - while True: - await asyncio.sleep_ms(500) - led.toggle() - -def test(): - dout = Pin(Pin.board.Y5, Pin.OUT_PP, value = 0) # Define pins - ckout = Pin(Pin.board.Y6, Pin.OUT_PP, value = 0) # Don't assert clock until data is set - din = Pin(Pin.board.Y7, Pin.IN) - ckin = Pin(Pin.board.Y8, Pin.IN) - reset = Pin(Pin.board.Y4, Pin.OPEN_DRAIN) - sig_reset = Signal(reset, invert = True) - - channel = SynCom(False, ckin, ckout, din, dout, sig_reset, 10000) - - loop = asyncio.get_event_loop() - loop.create_task(heartbeat()) - loop.create_task(channel.start(initiator_task)) - try: - loop.run_forever() - except KeyboardInterrupt: - pass - finally: - ckout.value(0) - -test() diff --git a/v2/syncom_as/sr_passive.py b/v2/syncom_as/sr_passive.py deleted file mode 100644 index 652d8b5..0000000 --- a/v2/syncom_as/sr_passive.py +++ /dev/null @@ -1,64 +0,0 @@ -# sr_passive.py Test of synchronous comms library. Passive end. - -# The MIT License (MIT) -# -# Copyright (c) 2016 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# Run on ESP8266 -import uasyncio as asyncio -from syncom import SynCom -from machine import Pin, freq -import gc - -async def passive_task(chan): - while True: - obj = await chan.await_obj() - if obj is not None: # Ignore timeouts -# print('passive received: ', obj) - obj[2] += 1 # modify object and send it back - chan.send(obj) - -async def heartbeat(): - led = Pin(2, Pin.OUT) - while True: - await asyncio.sleep_ms(500) - led(not led()) - gc.collect() - -def test(): - freq(160000000) - dout = Pin(14, Pin.OUT, value = 0) # Define pins - ckout = Pin(15, Pin.OUT, value = 0) # clocks must be initialised to zero. - din = Pin(13, Pin.IN) - ckin = Pin(12, Pin.IN) - - channel = SynCom(True, ckin, ckout, din, dout) - loop = asyncio.get_event_loop() - loop.create_task(heartbeat()) - loop.create_task(channel.start(passive_task)) - try: - loop.run_forever() - except KeyboardInterrupt: - pass - finally: - ckout(0) - -test() diff --git a/v2/syncom_as/syncom.py b/v2/syncom_as/syncom.py deleted file mode 100644 index 4ecb489..0000000 --- a/v2/syncom_as/syncom.py +++ /dev/null @@ -1,246 +0,0 @@ -# syncom.py Synchronous communication channel between two MicroPython -# platforms. 4 June 2017 -# Uses uasyncio. - -# The MIT License (MIT) -# -# Copyright (c) 2017 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# Timing: was 4.5mS per char between Pyboard and ESP8266 i.e. ~1.55Kbps. But -# this version didn't yield on every bit, invalidating t/o detection. -# New asyncio version yields on every bit. -# Instantaneous bit rate running ESP8266 at 160MHz: 1.6Kbps -# Mean throughput running test programs 8.8ms per char (800bps). - -from utime import ticks_diff, ticks_ms -import uasyncio as asyncio -from micropython import const - -_BITS_PER_CH = const(7) -_BITS_SYN = const(8) -_SYN = const(0x9d) -_RX_BUFLEN = const(100) - -class SynComError(Exception): - pass - -class SynCom(object): - def __init__(self, passive, ckin, ckout, din, dout, sig_reset=None, - timeout=0, string_mode=False, verbose=True): - self.passive = passive - self.string_mode = string_mode - if not string_mode: - global pickle - import pickle - self._running = False # _run coro is down - self._synchronised = False - self.verbose = verbose - self.idstr = 'passive' if self.passive else 'initiator' - - self.ckin = ckin # Interface pins - self.ckout = ckout - self.din = din - self.dout = dout - self.sig_reset = sig_reset - - self._timeout = timeout # In ms. 0 == No timeout. - self.lsttx = [] # Queue of strings to send - self.lstrx = [] # Queue of received strings - -# Start interface and initiate an optional user task. If a timeout and reset -# signal are specified and the target times out, the target is reset and the -# interface restarted. If a user task is provided, this must return if a -# timeout occurs (i.e. not running() or await_obj returns None). -# If it returns for other (error) reasons, a timeout event is forced. - async def start(self, user_task=None, awaitable=None): - loop = asyncio.get_event_loop() - while True: - if not self._running: # Restarting - self.lstrx = [] # Clear down queues - self.lsttx = [] - self._synchronised = False - loop.create_task(self._run()) # Reset target (if possible) - while not self._synchronised: # Wait for sync - await asyncio.sleep_ms(100) - if user_task is None: - while self._running: - await asyncio.sleep_ms(100) - else: - await user_task(self) # User task must quit on timeout - # If it quit for other reasons force a t/o exception - self.stop() - await asyncio.sleep_ms(0) - if awaitable is not None: # User code may use an ExitGate - await awaitable # to ensure all coros have quit - -# Can be used to force a failure - def stop(self): - self._running = False - self.dout(0) - self.ckout(0) - -# Queue an object for tx. Convert to string NOW: snapshot of current -# object state - def send(self, obj): - if self.string_mode: - self.lsttx.append(obj) # strings are immutable - else: - self.lsttx.append(pickle.dumps(obj)) - -# Number of queued objects (None on timeout) - def any(self): - if self._running: - return len(self.lstrx) - -# Wait for an object. Return None on timeout. -# If in string mode returns a string (or None on t/o) - async def await_obj(self, t_ms=10): - while self._running: - await asyncio.sleep_ms(t_ms) - if len(self.lstrx): - return self.lstrx.pop(0) - -# running() is False if the target has timed out. - def running(self): - return self._running - -# Private methods - def _vbprint(self, *args): - if self.verbose: - print(*args) - - async def _run(self): - self.indata = 0 # Current data bits - self.inbits = 0 - self.odata = _SYN - self.phase = 0 # Interface initial conditions - if self.passive: - self.dout(0) - self.ckout(0) - else: - self.dout(self.odata & 1) - self.ckout(1) - self.odata >>= 1 # we've sent that bit - self.phase = 1 - if self.sig_reset is not None: - self._vbprint(self.idstr, ' resetting target...') - self.sig_reset.on() - await asyncio.sleep_ms(100) - self.sig_reset.off() - await asyncio.sleep(1) # let target settle down - - self._vbprint(self.idstr, ' awaiting sync...') - try: - self._running = True # False on failure: can be cleared by other tasks - while self.indata != _SYN: # Don't hog CPU while waiting for start - await self._synchronise() - self._synchronised = True - self._vbprint(self.idstr, ' synchronised.') - - sendstr = '' # string for transmission - send_idx = None # character index. None: no current string - getstr = '' # receive string - rxbuf = bytearray(_RX_BUFLEN) - rxidx = 0 - while True: - if send_idx is None: - if len(self.lsttx): - sendstr = self.lsttx.pop(0) # oldest first - send_idx = 0 - if send_idx is not None: - if send_idx < len(sendstr): - self.odata = ord(sendstr[send_idx]) - send_idx += 1 - else: - send_idx = None - if send_idx is None: # send zeros when nothing to send - self.odata = 0 - if self.passive: - await self._get_byte_passive() - else: - await self._get_byte_active() - if self.indata: # Optimisation: buffer reduces allocations. - if rxidx >= _RX_BUFLEN: # Buffer full: append to string. - getstr = ''.join((getstr, bytes(rxbuf).decode())) - rxidx = 0 - rxbuf[rxidx] = self.indata - rxidx += 1 - elif rxidx or len(getstr): # Got 0 but have data so string is complete. - # Append buffer. - getstr = ''.join((getstr, bytes(rxbuf[:rxidx]).decode())) - if self.string_mode: - self.lstrx.append(getstr) - else: - try: - self.lstrx.append(pickle.loads(getstr)) - except: # Pickle fail means target has crashed - raise SynComError - getstr = '' # Reset for next string - rxidx = 0 - - except SynComError: - if self._running: - self._vbprint('SynCom Timeout.') - else: - self._vbprint('SynCom was stopped.') - finally: - self.stop() - - async def _get_byte_active(self): - inbits = 0 - for _ in range(_BITS_PER_CH): - inbits = await self._get_bit(inbits) # LSB first - self.indata = inbits - - async def _get_byte_passive(self): - self.indata = await self._get_bit(self.inbits) # MSB is outstanding - inbits = 0 - for _ in range(_BITS_PER_CH - 1): - inbits = await self._get_bit(inbits) - self.inbits = inbits - - async def _synchronise(self): # wait for clock - t = ticks_ms() - while self.ckin() == self.phase ^ self.passive ^ 1: - # Other tasks can clear self._running by calling stop() - if (self._timeout and ticks_diff(ticks_ms(), t) > self._timeout) or not self._running: - raise SynComError - await asyncio.sleep_ms(0) - self.indata = (self.indata | (self.din() << _BITS_SYN)) >> 1 - odata = self.odata - self.dout(odata & 1) - self.odata = odata >> 1 - self.phase ^= 1 - self.ckout(self.phase) # set clock - - async def _get_bit(self, dest): - t = ticks_ms() - while self.ckin() == self.phase ^ self.passive ^ 1: - if (self._timeout and ticks_diff(ticks_ms(), t) > self._timeout) or not self._running: - raise SynComError - yield # Faster than await asyncio.sleep_ms() - dest = (dest | (self.din() << _BITS_PER_CH)) >> 1 - obyte = self.odata - self.dout(obyte & 1) - self.odata = obyte >> 1 - self.phase ^= 1 - self.ckout(self.phase) - return dest diff --git a/v3/as_demos/stream_to.py b/v3/as_demos/stream_to.py new file mode 100644 index 0000000..a0d7267 --- /dev/null +++ b/v3/as_demos/stream_to.py @@ -0,0 +1,74 @@ +# stream_to.py Demo of StreamReader with timeout. +# Hardware: Pico or Pico W with pin GPIO0 linked to GPIO1 +# Copyright Peter Hinch 2024 Released under the MIT license + +import asyncio +from primitives import Delay_ms +from machine import UART + +_uart = UART(0, 115200, tx=0, rx=1, timeout=0) # Adapt for other hardware + +# Class extends StreamReader to enable read with timeout +class StreamReaderTo(asyncio.StreamReader): + def __init__(self, source): + super().__init__(source) + self._delay_ms = Delay_ms() # Allocate once only + + # Task cancels itself if timeout elapses without a byte being received + async def readintotim(self, buf: bytearray, toms: int) -> int: # toms: timeout in ms + mvb = memoryview(buf) + timer = self._delay_ms + timer.callback(asyncio.current_task().cancel) + timer.trigger(toms) # Start cancellation timer + n = 0 + nbytes = len(buf) + try: + while n < nbytes: + n += await super().readinto(mvb[n:]) + timer.trigger(toms) # Retrigger when bytes received + except asyncio.CancelledError: + pass + timer.stop() + return n + + +# Simple demo +EOT = b"QUIT" # End of transmission + + +async def sender(writer): + s = "The quick brown fox jumps over the lazy dog!" + for _ in range(2): + writer.write(s) + writer.drain() + await asyncio.sleep(1) # < reader timeout + writer.write(s) + writer.drain() + await asyncio.sleep(4) # > reader timeout + writer.write(EOT) + writer.drain() + + +async def receiver(reader): + buf = bytearray(16) # Read in blocks of 16 cbytes + print("Receiving. Demo runs for ~15s...") + while not buf.startswith(EOT): + n = await reader.readintotim(buf, 3000) + if n < len(buf): + print("Timeout: ", end="") + print(bytes(buf[:n])) + if n < len(buf): + print("") + print("Demo complete.") + + +async def main(): + reader = StreamReaderTo(_uart) + writer = asyncio.StreamWriter(_uart, {}) + await asyncio.gather(sender(writer), receiver(reader)) + + +try: + asyncio.run(main()) +finally: + _ = asyncio.new_event_loop() diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 0caf44c..68bd663 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -800,14 +800,17 @@ yet officially supported by MicroPython. ### 3.3.1 gather -This official `asyncio` asynchronous method causes a number of tasks to run, -pausing until all have either run to completion or been terminated by +This official `asyncio` asynchronous method causes a number of awaitables to +run, pausing until all have either run to completion or been terminated by cancellation or timeout. It returns a list of the return values of each task. Its call signature is ```python -res = await asyncio.gather(*tasks, return_exceptions=False) +res = await asyncio.gather(*awaitables, return_exceptions=False) ``` +`awaitables` may comprise tasks or coroutines, the latter being converted to +tasks. + The keyword-only boolean arg `return_exceptions` determines the behaviour in the event of a cancellation or timeout of tasks. If `False`, the `gather` terminates immediately, raising the relevant exception which should be trapped @@ -2039,6 +2042,40 @@ buffers incoming characters. To avoid data loss the size of the read buffer should be set based on the maximum latency caused by other tasks along with the baudrate. The buffer size can be reduced if hardware flow control is available. +##### StreamReader read timeout + +It is possible to apply a timeout to a stream. One approach is to subclass +`StreamReader` as follows: +```python +class StreamReaderTo(asyncio.StreamReader): + def __init__(self, source): + super().__init__(source) + self._delay_ms = Delay_ms() # Allocate once only + + # Task cancels itself if timeout elapses without a byte being received + async def readintotim(self, buf: bytearray, toms: int) -> int: # toms: timeout in ms + mvb = memoryview(buf) + timer = self._delay_ms + timer.callback(asyncio.current_task().cancel) + timer.trigger(toms) # Start cancellation timer + n = 0 + nbytes = len(buf) + try: + while n < nbytes: + n += await super().readinto(mvb[n:]) + timer.trigger(toms) # Retrigger when bytes received + except asyncio.CancelledError: + pass + timer.stop() + return n +``` +This adds a `.readintotim` asynchronous method. Like `.readinto` it reads into a +supplied buffer but the read is subject to a timeout `to` in ms. The read pauses +until either the buffer is full or until bytes stop arriving for a time longer +than `to`. The method returns the number of bytes received. If fewer bytes were +received than would fill the buffer, a timeout occurred. The script +[stream_to.py](../as_demos/stream_to.py) demonstrates this. + ### 6.3.1 A UART driver example The program [auart_hd.py](../as_demos/auart_hd.py) illustrates a method of From 6549e81b9f2a42135709d6eb02d377cde572da50 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 16 Feb 2024 14:08:06 +0000 Subject: [PATCH 276/305] Finish removal of V2 relics. --- README.md | 30 ++-- aswitch.py | 231 -------------------------- asyn.py | 470 ----------------------------------------------------- 3 files changed, 10 insertions(+), 721 deletions(-) delete mode 100644 aswitch.py delete mode 100644 asyn.py diff --git a/README.md b/README.md index 9457625..31e643c 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,24 @@ # Asynchronous programming in MicroPython CPython supports asynchronous programming via the `asyncio` library. -MicroPython provides `uasyncio` which is a subset of this, optimised for small +MicroPython provides `asyncio` which is a subset of this, optimised for small code size and high performance on bare metal targets. This repository provides documentation, tutorial material and code to aid in its effective use. -# uasyncio version 3 +# asyncio version 3 -Damien has completely rewritten `uasyncio` which was released as V3.0. See -[PR5332](https://github.com/micropython/micropython/pull/5332). This is now -incorporated in release build V1.13 and subsequent daily builds. +Damien has completely rewritten `asyncio` which was released as V3.0. This is +incorporated in all recent firmware builds. The resources in this repo may be found in the +`v3` directory. These include a tutorial, synchronisation primitives, drivers, +applications and demos. -Resources for V3 may be found in the `v3` directory. These include a guide to -porting applications from V2, an updated tutorial, synchronisation primitives -and various applications and demos. +# Concurrency -V2 should now be regarded as obsolete for almost all applications with the -possible exception mentioned below. +Other documents provide hints on asynchronous programming techniques including +threading and multi-core coding. ### [Go to V3 docs](./v3/README.md) # uasyncio version 2 -The official version 2 is entirely superseded by V3, which improves on it in -every respect. - -I produced a modified `fast_io` variant of V2 which is in use for some -specialist purposes. It enables I/O to be scheduled at high priority. Currently -this schedules I/O significantly faster than V3; the maintainers plan to -improve `uasyncio` I/O scheduling. When this is complete I intend to delete all -V2 material. - -All V2 resources are in the V2 subdirectory: [see this README](./v2/README.md). +This is obsolete: code and docs have been removed. diff --git a/aswitch.py b/aswitch.py deleted file mode 100644 index 4269ce9..0000000 --- a/aswitch.py +++ /dev/null @@ -1,231 +0,0 @@ -# aswitch.py Switch and pushbutton classes for asyncio -# Delay_ms A retriggerable delay class. Can schedule a coro on timeout. -# Switch Simple debounced switch class for normally open grounded switch. -# Pushbutton extend the above to support logical state, long press and -# double-click events -# Tested on Pyboard but should run on other microcontroller platforms -# running MicroPython and uasyncio. - -# The MIT License (MIT) -# -# Copyright (c) 2017 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import uasyncio as asyncio -import utime as time -# Remove dependency on asyn to save RAM: -# launch: run a callback or initiate a coroutine depending on which is passed. -async def _g(): - pass -type_coro = type(_g()) - -# If a callback is passed, run it and return. -# If a coro is passed initiate it and return. -# coros are passed by name i.e. not using function call syntax. -def launch(func, tup_args): - res = func(*tup_args) - if isinstance(res, type_coro): - loop = asyncio.get_event_loop() - loop.create_task(res) - - -class Delay_ms: - verbose = False - def __init__(self, func=None, args=(), can_alloc=True, duration=1000): - self.func = func - self.args = args - self.can_alloc = can_alloc - self.duration = duration # Default duration - self._tstop = None # Killer not running - self._running = False # Timer not running - self.loop = asyncio.get_event_loop() - if not can_alloc: - self.loop.create_task(self._run()) - - async def _run(self): - while True: - if not self._running: # timer not running - await asyncio.sleep_ms(0) - else: - await self._killer() - - def stop(self): - self._running = False - # If uasyncio is ever fixed we should cancel .killer - - def trigger(self, duration=0): # Update end time - self._running = True - if duration <= 0: - duration = self.duration - tn = time.ticks_add(time.ticks_ms(), duration) # new end time - self.verbose and self._tstop is not None and self._tstop > tn \ - and print("Warning: can't reduce Delay_ms time.") - # Start killer if can allocate and killer is not running - sk = self.can_alloc and self._tstop is None - # The following indicates ._killer is running: it will be - # started either here or in ._run - self._tstop = tn - if sk: # ._killer stops the delay when its period has elapsed - self.loop.create_task(self._killer()) - - def running(self): - return self._running - - __call__ = running - - async def _killer(self): - twait = time.ticks_diff(self._tstop, time.ticks_ms()) - while twait > 0: # Must loop here: might be retriggered - await asyncio.sleep_ms(twait) - if self._tstop is None: - break # Return if stop() called during wait - twait = time.ticks_diff(self._tstop, time.ticks_ms()) - if self._running and self.func is not None: - launch(self.func, self.args) # Timed out: execute callback - self._tstop = None # killer not running - self._running = False # timer is stopped - -class Switch: - debounce_ms = 50 - def __init__(self, pin): - self.pin = pin # Should be initialised for input with pullup - self._open_func = False - self._close_func = False - self.switchstate = self.pin.value() # Get initial state - loop = asyncio.get_event_loop() - loop.create_task(self.switchcheck()) # Thread runs forever - - def open_func(self, func, args=()): - self._open_func = func - self._open_args = args - - def close_func(self, func, args=()): - self._close_func = func - self._close_args = args - - # Return current state of switch (0 = pressed) - def __call__(self): - return self.switchstate - - async def switchcheck(self): - while True: - state = self.pin.value() - if state != self.switchstate: - # State has changed: act on it now. - self.switchstate = state - if state == 0 and self._close_func: - launch(self._close_func, self._close_args) - elif state == 1 and self._open_func: - launch(self._open_func, self._open_args) - # Ignore further state changes until switch has settled - await asyncio.sleep_ms(Switch.debounce_ms) - -# An alternative Pushbutton solution with lower RAM use is available here -# https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py -class Pushbutton: - debounce_ms = 50 - long_press_ms = 1000 - double_click_ms = 400 - def __init__(self, pin, suppress=False): - self.pin = pin # Initialise for input - self._supp = suppress - self._dblpend = False # Doubleclick waiting for 2nd click - self._dblran = False # Doubleclick executed user function - self._tf = False - self._ff = False - self._df = False - self._lf = False - self._ld = False # Delay_ms instance for long press - self._dd = False # Ditto for doubleclick - self.sense = pin.value() # Convert from electrical to logical value - self.state = self.rawstate() # Initial state - loop = asyncio.get_event_loop() - loop.create_task(self.buttoncheck()) # Thread runs forever - - def press_func(self, func, args=()): - self._tf = func - self._ta = args - - def release_func(self, func, args=()): - self._ff = func - self._fa = args - - def double_func(self, func, args=()): - self._df = func - self._da = args - - def long_func(self, func, args=()): - self._lf = func - self._la = args - - # Current non-debounced logical button state: True == pressed - def rawstate(self): - return bool(self.pin.value() ^ self.sense) - - # Current debounced state of button (True == pressed) - def __call__(self): - return self.state - - def _ddto(self): # Doubleclick timeout: no doubleclick occurred - self._dblpend = False - if self._supp and not self.state: - if not self._ld or (self._ld and not self._ld()): - launch(self._ff, self._fa) - - async def buttoncheck(self): - if self._lf: # Instantiate timers if funcs exist - self._ld = Delay_ms(self._lf, self._la) - if self._df: - self._dd = Delay_ms(self._ddto) - while True: - state = self.rawstate() - # State has changed: act on it now. - if state != self.state: - self.state = state - if state: # Button pressed: launch pressed func - if self._tf: - launch(self._tf, self._ta) - if self._lf: # There's a long func: start long press delay - self._ld.trigger(Pushbutton.long_press_ms) - if self._df: - if self._dd(): # Second click: timer running - self._dd.stop() - self._dblpend = False - self._dblran = True # Prevent suppressed launch on release - launch(self._df, self._da) - else: - # First click: start doubleclick timer - self._dd.trigger(Pushbutton.double_click_ms) - self._dblpend = True # Prevent suppressed launch on release - else: # Button release. Is there a release func? - if self._ff: - if self._supp: - d = self._ld - # If long delay exists, is running and doubleclick status is OK - if not self._dblpend and not self._dblran: - if (d and d()) or not d: - launch(self._ff, self._fa) - else: - launch(self._ff, self._fa) - if self._ld: - self._ld.stop() # Avoid interpreting a second click as a long push - self._dblran = False - # Ignore state changes until switch has settled - await asyncio.sleep_ms(Pushbutton.debounce_ms) diff --git a/asyn.py b/asyn.py deleted file mode 100644 index c87c175..0000000 --- a/asyn.py +++ /dev/null @@ -1,470 +0,0 @@ -# asyn.py 'micro' synchronisation primitives for uasyncio -# Test/demo programs asyntest.py, barrier_test.py -# Provides Lock, Event, Barrier, Semaphore, BoundedSemaphore, Condition, -# NamedTask and Cancellable classes, also sleep coro. -# Updated 31 Dec 2017 for uasyncio.core V1.6 and to provide task cancellation. - -# The MIT License (MIT) -# -# Copyright (c) 2017 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# CPython 3.5 compatibility -# (ignore RuntimeWarning: coroutine '_g' was never awaited) - -try: - import uasyncio as asyncio -except ImportError: - import asyncio - - -async def _g(): - pass -type_coro = type(_g()) - -# If a callback is passed, run it and return. -# If a coro is passed initiate it and return. -# coros are passed by name i.e. not using function call syntax. -def launch(func, tup_args): - res = func(*tup_args) - if isinstance(res, type_coro): - loop = asyncio.get_event_loop() - loop.create_task(res) - - -# To access a lockable resource a coro should issue -# async with lock_instance: -# access the locked resource - -# Alternatively: -# await lock.acquire() -# try: -# do stuff with locked resource -# finally: -# lock.release -# Uses normal scheduling on assumption that locks are held briefly. -class Lock(): - def __init__(self, delay_ms=0): - self._locked = False - self.delay_ms = delay_ms - - def locked(self): - return self._locked - - async def __aenter__(self): - await self.acquire() - return self - - async def __aexit__(self, *args): - self.release() - await asyncio.sleep(0) - - async def acquire(self): - while True: - if self._locked: - await asyncio.sleep_ms(self.delay_ms) - else: - self._locked = True - break - - def release(self): - if not self._locked: - raise RuntimeError('Attempt to release a lock which has not been set') - self._locked = False - - -# A coro waiting on an event issues await event -# A coro rasing the event issues event.set() -# When all waiting coros have run -# event.clear() should be issued -class Event(): - def __init__(self, delay_ms=0): - self.delay_ms = delay_ms - self.clear() - - def clear(self): - self._flag = False - self._data = None - - async def wait(self): # CPython comptaibility - while not self._flag: - await asyncio.sleep_ms(self.delay_ms) - - def __await__(self): - while not self._flag: - await asyncio.sleep_ms(self.delay_ms) - - __iter__ = __await__ - - def is_set(self): - return self._flag - - def set(self, data=None): - self._flag = True - self._data = data - - def value(self): - return self._data - -# A Barrier synchronises N coros. Each issues await barrier. -# Execution pauses until all other participant coros are waiting on it. -# At that point the callback is executed. Then the barrier is 'opened' and -# execution of all participants resumes. - -# The nowait arg is to support task cancellation. It enables usage where one or -# more coros can register that they have reached the barrier without waiting -# for it. Any coros waiting normally on the barrier will pause until all -# non-waiting coros have passed the barrier and all waiting ones have reached -# it. The use of nowait promotes efficiency by enabling tasks which have been -# cancelled to leave the task queue as soon as possible. - -class Barrier(): - def __init__(self, participants, func=None, args=()): - self._participants = participants - self._func = func - self._args = args - self._reset(True) - - def __await__(self): - self._update() - if self._at_limit(): # All other threads are also at limit - if self._func is not None: - launch(self._func, self._args) - self._reset(not self._down) # Toggle direction to release others - return - - direction = self._down - while True: # Wait until last waiting thread changes the direction - if direction != self._down: - return - await asyncio.sleep_ms(0) - - __iter__ = __await__ - - def trigger(self): - self._update() - if self._at_limit(): # All other threads are also at limit - if self._func is not None: - launch(self._func, self._args) - self._reset(not self._down) # Toggle direction to release others - - def _reset(self, down): - self._down = down - self._count = self._participants if down else 0 - - def busy(self): - if self._down: - done = self._count == self._participants - else: - done = self._count == 0 - return not done - - def _at_limit(self): # Has count reached up or down limit? - limit = 0 if self._down else self._participants - return self._count == limit - - def _update(self): - self._count += -1 if self._down else 1 - if self._count < 0 or self._count > self._participants: - raise ValueError('Too many tasks accessing Barrier') - -# A Semaphore is typically used to limit the number of coros running a -# particular piece of code at once. The number is defined in the constructor. -class Semaphore(): - def __init__(self, value=1): - self._count = value - - async def __aenter__(self): - await self.acquire() - return self - - async def __aexit__(self, *args): - self.release() - await asyncio.sleep(0) - - async def acquire(self): - while self._count == 0: - await asyncio.sleep_ms(0) - self._count -= 1 - - def release(self): - self._count += 1 - -class BoundedSemaphore(Semaphore): - def __init__(self, value=1): - super().__init__(value) - self._initial_value = value - - def release(self): - if self._count < self._initial_value: - self._count += 1 - else: - raise ValueError('Semaphore released more than acquired') - -# Task Cancellation -try: - StopTask = asyncio.CancelledError # More descriptive name -except AttributeError: - raise OSError('asyn.py requires uasyncio V1.7.1 or above.') - -class TaskId(): - def __init__(self, taskid): - self.taskid = taskid - - def __call__(self): - return self.taskid - -# Sleep coro breaks up a sleep into shorter intervals to ensure a rapid -# response to StopTask exceptions. Only relevant to official uasyncio V2.0. -async def sleep(t, granularity=100): # 100ms default - if granularity <= 0: - raise ValueError('sleep granularity must be > 0') - t = int(t * 1000) # ms - if t <= granularity: - await asyncio.sleep_ms(t) - else: - n, rem = divmod(t, granularity) - for _ in range(n): - await asyncio.sleep_ms(granularity) - await asyncio.sleep_ms(rem) - -# Anonymous cancellable tasks. These are members of a group which is identified -# by a user supplied name/number (default 0). Class method cancel_all() cancels -# all tasks in a group and awaits confirmation. Confirmation of ending (whether -# normally or by cancellation) is signalled by a task calling the _stopped() -# class method. Handled by the @cancellable decorator. - - -class Cancellable(): - task_no = 0 # Generated task ID, index of tasks dict - tasks = {} # Value is [coro, group, barrier] indexed by integer task_no - - @classmethod - def _cancel(cls, task_no): - task = cls.tasks[task_no][0] - asyncio.cancel(task) - - @classmethod - async def cancel_all(cls, group=0, nowait=False): - tokill = cls._get_task_nos(group) - barrier = Barrier(len(tokill) + 1) # Include this task - for task_no in tokill: - cls.tasks[task_no][2] = barrier - cls._cancel(task_no) - if nowait: - barrier.trigger() - else: - await barrier - - @classmethod - def _is_running(cls, group=0): - tasks = cls._get_task_nos(group) - if tasks == []: - return False - for task_no in tasks: - barrier = cls.tasks[task_no][2] - if barrier is None: # Running, not yet cancelled - return True - if barrier.busy(): - return True - return False - - @classmethod - def _get_task_nos(cls, group): # Return task nos in a group - return [task_no for task_no in cls.tasks if cls.tasks[task_no][1] == group] - - @classmethod - def _get_group(cls, task_no): # Return group given a task_no - return cls.tasks[task_no][1] - - @classmethod - def _stopped(cls, task_no): - if task_no in cls.tasks: - barrier = cls.tasks[task_no][2] - if barrier is not None: # Cancellation in progress - barrier.trigger() - del cls.tasks[task_no] - - def __init__(self, gf, *args, group=0, **kwargs): - task = gf(TaskId(Cancellable.task_no), *args, **kwargs) - if task in self.tasks: - raise ValueError('Task already exists.') - self.tasks[Cancellable.task_no] = [task, group, None] - self.task_no = Cancellable.task_no # For subclass - Cancellable.task_no += 1 - self.task = task - - def __call__(self): - return self.task - - def __await__(self): # Return any value returned by task. - return (yield from self.task) - - __iter__ = __await__ - - -# @cancellable decorator - -def cancellable(f): - def new_gen(*args, **kwargs): - if isinstance(args[0], TaskId): # Not a bound method - task_id = args[0] - g = f(*args[1:], **kwargs) - else: # Task ID is args[1] if a bound method - task_id = args[1] - args = (args[0],) + args[2:] - g = f(*args, **kwargs) - try: - res = await g - return res - finally: - NamedTask._stopped(task_id) - return new_gen - -# The NamedTask class enables a coro to be identified by a user defined name. -# It constrains Cancellable to allow groups of one coro only. -# It maintains a dict of barriers indexed by name. -class NamedTask(Cancellable): - instances = {} - - @classmethod - async def cancel(cls, name, nowait=True): - if name in cls.instances: - await cls.cancel_all(group=name, nowait=nowait) - return True - return False - - @classmethod - def is_running(cls, name): - return cls._is_running(group=name) - - @classmethod - def _stopped(cls, task_id): # On completion remove it - name = cls._get_group(task_id()) # Convert task_id to task_no - if name in cls.instances: - instance = cls.instances[name] - barrier = instance.barrier - if barrier is not None: - barrier.trigger() - del cls.instances[name] - Cancellable._stopped(task_id()) - - def __init__(self, name, gf, *args, barrier=None, **kwargs): - if name in self.instances: - raise ValueError('Task name "{}" already exists.'.format(name)) - super().__init__(gf, *args, group=name, **kwargs) - self.barrier = barrier - self.instances[name] = self - - -# @namedtask -namedtask = cancellable # compatibility with old code - -# Condition class - -class Condition(): - def __init__(self, lock=None): - self.lock = Lock() if lock is None else lock - self.events = [] - - async def acquire(self): - await self.lock.acquire() - -# enable this syntax: -# with await condition [as cond]: - def __await__(self): - yield from self.lock.acquire() - return self - - __iter__ = __await__ - - def __enter__(self): - return self - - def __exit__(self, *_): - self.lock.release() - - def locked(self): - return self.lock.locked() - - def release(self): - self.lock.release() # Will raise RuntimeError if not locked - - def notify(self, n=1): # Caller controls lock - if not self.lock.locked(): - raise RuntimeError('Condition notify with lock not acquired.') - for _ in range(min(n, len(self.events))): - ev = self.events.pop() - ev.set() - - def notify_all(self): - self.notify(len(self.events)) - - async def wait(self): - if not self.lock.locked(): - raise RuntimeError('Condition wait with lock not acquired.') - ev = Event() - self.events.append(ev) - self.lock.release() - await ev - await self.lock.acquire() - assert ev not in self.events, 'condition wait assertion fail' - return True # CPython compatibility - - async def wait_for(self, predicate): - result = predicate() - while not result: - await self.wait() - result = predicate() - return result - -# Provide functionality similar to asyncio.gather() - -class Gather(): - def __init__(self, gatherables): - ncoros = len(gatherables) - self.barrier = Barrier(ncoros + 1) - self.results = [None] * ncoros - loop = asyncio.get_event_loop() - for n, gatherable in enumerate(gatherables): - loop.create_task(self.wrap(gatherable, n)()) - - def __iter__(self): - yield from self.barrier.__await__() - return self.results - - def wrap(self, gatherable, idx): - async def wrapped(): - coro, args, kwargs = gatherable() - try: - tim = kwargs.pop('timeout') - except KeyError: - self.results[idx] = await coro(*args, **kwargs) - else: - self.results[idx] = await asyncio.wait_for(coro(*args, **kwargs), tim) - self.barrier.trigger() - return wrapped - -class Gatherable(): - def __init__(self, coro, *args, **kwargs): - self.arguments = coro, args, kwargs - - def __call__(self): - return self.arguments From 0fb2f22d1b130d63be2ec4d66958c4f6eb8106b3 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 9 May 2024 09:12:24 +0100 Subject: [PATCH 277/305] Docs and code: Remove all references to uasyncio. --- v3/as_demos/aledflash.py | 15 +- v3/as_demos/apoll.py | 30 ++- v3/as_demos/auart.py | 2 +- v3/as_demos/auart_hd.py | 43 ++-- v3/as_demos/gather.py | 48 ++-- v3/as_demos/iorw.py | 40 ++-- v3/as_demos/rate.py | 15 +- v3/as_demos/roundrobin.py | 8 +- v3/as_drivers/as_GPS/as_GPS.py | 127 +++++----- v3/as_drivers/as_GPS/as_GPS_time.py | 87 ++++--- v3/as_drivers/as_GPS/as_rwGPS_time.py | 117 +++++---- v3/as_drivers/as_GPS/as_tGPS.py | 95 +++++--- v3/as_drivers/as_GPS/ast_pb.py | 54 +++-- v3/as_drivers/as_GPS/ast_pbrw.py | 104 ++++---- v3/as_drivers/as_GPS/astests.py | 228 +++++++++--------- v3/as_drivers/as_GPS/astests_pyb.py | 197 ++++++++------- v3/as_drivers/as_GPS/baud.py | 13 +- v3/as_drivers/as_GPS/log_kml.py | 24 +- v3/as_drivers/client_server/heartbeat.py | 22 +- v3/as_drivers/client_server/uclient.py | 20 +- v3/as_drivers/client_server/userver.py | 31 +-- v3/as_drivers/hd44780/alcd.py | 16 +- v3/as_drivers/hd44780/alcdtest.py | 7 +- v3/as_drivers/htu21d/htu21d_mc.py | 15 +- v3/as_drivers/htu21d/htu_test.py | 10 +- v3/as_drivers/i2c/asi2c.py | 31 +-- v3/as_drivers/i2c/asi2c_i.py | 32 ++- v3/as_drivers/i2c/i2c_esp.py | 18 +- v3/as_drivers/i2c/i2c_init.py | 37 +-- v3/as_drivers/i2c/i2c_resp.py | 23 +- v3/as_drivers/metrics/metrics.py | 14 +- v3/as_drivers/nec_ir/aremote.py | 31 +-- v3/as_drivers/nec_ir/art.py | 41 ++-- v3/as_drivers/nec_ir/art1.py | 36 +-- v3/as_drivers/sched/asynctest.py | 20 +- v3/as_drivers/sched/primitives/__init__.py | 10 +- v3/as_drivers/sched/sched.py | 2 +- v3/as_drivers/syncom/sr_init.py | 31 +-- v3/as_drivers/syncom/sr_passive.py | 16 +- v3/as_drivers/syncom/syncom.py | 100 ++++---- v3/docs/DRIVERS.md | 6 +- v3/docs/GPS.md | 28 +-- v3/docs/HTU21D.md | 6 +- v3/docs/I2C.md | 18 +- v3/docs/INTERRUPTS.md | 68 +++--- v3/docs/NEC_IR.md | 8 +- v3/docs/SCHEDULE.md | 6 +- v3/docs/SYNCOM.md | 6 +- v3/docs/THREADING.md | 82 +++---- v3/docs/hd44780.md | 6 +- v3/primitives/__init__.py | 5 +- v3/primitives/aadc.py | 7 +- v3/primitives/barrier.py | 14 +- v3/primitives/condition.py | 18 +- v3/primitives/delay_ms.py | 9 +- v3/primitives/encoder.py | 2 +- v3/primitives/events.py | 2 +- v3/primitives/pushbutton.py | 2 +- v3/primitives/queue.py | 17 +- v3/primitives/ringbuf_queue.py | 2 +- v3/primitives/semaphore.py | 10 +- v3/primitives/switch.py | 6 +- v3/primitives/tests/adctest.py | 22 +- v3/primitives/tests/asyntest.py | 263 ++++++++++++++------- v3/primitives/tests/delay_test.py | 133 ++++++----- v3/primitives/tests/encoder_stop.py | 15 +- v3/primitives/tests/encoder_test.py | 10 +- v3/primitives/tests/event_test.py | 29 ++- v3/primitives/tests/switches.py | 96 ++++---- v3/threadsafe/context.py | 4 +- v3/threadsafe/message.py | 6 +- v3/threadsafe/threadsafe_event.py | 3 +- v3/threadsafe/threadsafe_queue.py | 2 +- 73 files changed, 1566 insertions(+), 1155 deletions(-) diff --git a/v3/as_demos/aledflash.py b/v3/as_demos/aledflash.py index 3d961b5..6e8fb12 100644 --- a/v3/as_demos/aledflash.py +++ b/v3/as_demos/aledflash.py @@ -5,30 +5,35 @@ # Run on MicroPython board bare hardware import pyb -import uasyncio as asyncio +import asyncio + async def toggle(objLED, time_ms): while True: await asyncio.sleep_ms(time_ms) objLED.toggle() + # TEST FUNCTION + async def main(duration): print("Flash LED's for {} seconds".format(duration)) - leds = [pyb.LED(x) for x in range(1,4)] # Initialise three on board LED's + leds = [pyb.LED(x) for x in range(1, 4)] # Initialise three on board LED's for x, led in enumerate(leds): # Create a task for each LED - t = int((0.2 + x/2) * 1000) + t = int((0.2 + x / 2) * 1000) asyncio.create_task(toggle(leds[x], t)) await asyncio.sleep(duration) + def test(duration=10): try: asyncio.run(main(duration)) except KeyboardInterrupt: - print('Interrupted') + print("Interrupted") finally: asyncio.new_event_loop() - print('as_demos.aledflash.test() to run again.') + print("as_demos.aledflash.test() to run again.") + test() diff --git a/v3/as_demos/apoll.py b/v3/as_demos/apoll.py index 2dbfeeb..abb609d 100644 --- a/v3/as_demos/apoll.py +++ b/v3/as_demos/apoll.py @@ -5,22 +5,24 @@ # Author: Peter Hinch # Copyright Peter Hinch 2017 Released under the MIT license -import uasyncio as asyncio +import asyncio import pyb import utime as time + class Accelerometer(object): threshold_squared = 16 + def __init__(self, accelhw, timeout): self.accelhw = accelhw self.timeout = timeout self.last_change = time.ticks_ms() self.coords = [accelhw.x(), accelhw.y(), accelhw.z()] - def dsquared(self, xyz): # Return the square of the distance between this and a passed - return sum(map(lambda p, q : (p-q)**2, self.coords, xyz)) # acceleration vector + def dsquared(self, xyz): # Return the square of the distance between this and a passed + return sum(map(lambda p, q: (p - q) ** 2, self.coords, xyz)) # acceleration vector - def poll(self): # Device is noisy. Only update if change exceeds a threshold + def poll(self): # Device is noisy. Only update if change exceeds a threshold xyz = [self.accelhw.x(), self.accelhw.y(), self.accelhw.z()] if self.dsquared(xyz) > Accelerometer.threshold_squared: self.coords = xyz @@ -31,31 +33,33 @@ def poll(self): # Device is noisy. Only update if change exc def vector(self): return self.coords - def timed_out(self): # Time since last change or last timeout report + def timed_out(self): # Time since last change or last timeout report if time.ticks_diff(time.ticks_ms(), self.last_change) > self.timeout: self.last_change = time.ticks_ms() return True return False + async def accel_coro(timeout=2000): - accelhw = pyb.Accel() # Instantiate accelerometer hardware - await asyncio.sleep_ms(30) # Allow it to settle + accelhw = pyb.Accel() # Instantiate accelerometer hardware + await asyncio.sleep_ms(30) # Allow it to settle accel = Accelerometer(accelhw, timeout) while True: result = accel.poll() - if result == 0: # Value has changed + if result == 0: # Value has changed x, y, z = accel.vector() print("Value x:{:3d} y:{:3d} z:{:3d}".format(x, y, z)) - elif accel.timed_out(): # Report every 2 secs + elif accel.timed_out(): # Report every 2 secs print("Timeout waiting for accelerometer change") - await asyncio.sleep_ms(100) # Poll every 100ms + await asyncio.sleep_ms(100) # Poll every 100ms async def main(delay): - print('Testing accelerometer for {} secs. Move the Pyboard!'.format(delay)) - print('Test runs for {}s.'.format(delay)) + print("Testing accelerometer for {} secs. Move the Pyboard!".format(delay)) + print("Test runs for {}s.".format(delay)) asyncio.create_task(accel_coro()) await asyncio.sleep(delay) - print('Test complete!') + print("Test complete!") + asyncio.run(main(20)) diff --git a/v3/as_demos/auart.py b/v3/as_demos/auart.py index 9119f41..1a312b0 100644 --- a/v3/as_demos/auart.py +++ b/v3/as_demos/auart.py @@ -4,7 +4,7 @@ # Link X1 and X2 to test. # We run with no UART timeout: UART read never blocks. -import uasyncio as asyncio +import asyncio from machine import UART uart = UART(4, 9600, timeout=0) diff --git a/v3/as_demos/auart_hd.py b/v3/as_demos/auart_hd.py index 82e544e..5a6783f 100644 --- a/v3/as_demos/auart_hd.py +++ b/v3/as_demos/auart_hd.py @@ -13,20 +13,20 @@ # To test link X1-X4 and X2-X3 from pyb import UART -import uasyncio as asyncio +import asyncio from primitives.delay_ms import Delay_ms # Dummy device waits for any incoming line and responds with 4 lines at 1 second # intervals. -class Device(): - def __init__(self, uart_no = 4): +class Device: + def __init__(self, uart_no=4): self.uart = UART(uart_no, 9600) self.swriter = asyncio.StreamWriter(self.uart, {}) self.sreader = asyncio.StreamReader(self.uart) asyncio.create_task(self._run()) async def _run(self): - responses = ['Line 1', 'Line 2', 'Line 3', 'Goodbye'] + responses = ["Line 1", "Line 2", "Line 3", "Goodbye"] while True: res = await self.sreader.readline() for response in responses: @@ -34,14 +34,15 @@ async def _run(self): # Demo the fact that the master tolerates slow response. await asyncio.sleep_ms(300) + # The master's send_command() method sends a command and waits for a number of # lines from the device. The end of the process is signified by a timeout, when # a list of lines is returned. This allows line-by-line processing. # A special test mode demonstrates the behaviour with a non-responding device. If # None is passed, no commend is sent. The master waits for a response which never # arrives and returns an empty list. -class Master(): - def __init__(self, uart_no = 2, timeout=4000): +class Master: + def __init__(self, uart_no=2, timeout=4000): self.uart = UART(uart_no, 9600) self.timeout = timeout self.swriter = asyncio.StreamWriter(self.uart, {}) @@ -59,32 +60,34 @@ async def _recv(self): async def send_command(self, command): self.response = [] # Discard any pending messages if command is None: - print('Timeout test.') + print("Timeout test.") else: await self.swriter.awrite("{}\r\n".format(command)) - print('Command sent:', command) + print("Command sent:", command) self.delay.trigger(self.timeout) # Re-initialise timer while self.delay.running(): await asyncio.sleep(1) # Wait for 4s after last msg received return self.response + async def main(): - print('This test takes 10s to complete.') + print("This test takes 10s to complete.") master = Master() device = Device() - for cmd in ['Run', None]: + for cmd in ["Run", None]: print() res = await master.send_command(cmd) # can use b''.join(res) if a single string is required. if res: - print('Result is:') + print("Result is:") for line in res: - print(line.decode('UTF8'), end='') + print(line.decode("UTF8"), end="") else: - print('Timed out waiting for result.') + print("Timed out waiting for result.") + def printexp(): - st = '''Expected output: + st = """Expected output: This test takes 10s to complete. Command sent: Run @@ -96,19 +99,21 @@ def printexp(): Timeout test. Timed out waiting for result. -''' - print('\x1b[32m') +""" + print("\x1b[32m") print(st) - print('\x1b[39m') + print("\x1b[39m") + def test(): printexp() try: asyncio.run(main()) except KeyboardInterrupt: - print('Interrupted') + print("Interrupted") finally: asyncio.new_event_loop() - print('as_demos.auart_hd.test() to run again.') + print("as_demos.auart_hd.test() to run again.") + test() diff --git a/v3/as_demos/gather.py b/v3/as_demos/gather.py index 86a9ba1..45205f9 100644 --- a/v3/as_demos/gather.py +++ b/v3/as_demos/gather.py @@ -3,42 +3,47 @@ # 2. A coro with a timeout # 3. A cancellable coro -import uasyncio as asyncio +import asyncio + async def barking(n): - print('Start normal coro barking()') + print("Start normal coro barking()") for _ in range(6): await asyncio.sleep(1) - print('Done barking.') + print("Done barking.") return 2 * n + async def foo(n): - print('Start timeout coro foo()') + print("Start timeout coro foo()") try: while True: await asyncio.sleep(1) n += 1 except asyncio.CancelledError: - print('Trapped foo timeout.') + print("Trapped foo timeout.") raise return n + async def bar(n): - print('Start cancellable bar()') + print("Start cancellable bar()") try: while True: await asyncio.sleep(1) n += 1 except asyncio.CancelledError: # Demo of trapping - print('Trapped bar cancellation.') + print("Trapped bar cancellation.") raise return n + async def do_cancel(task): await asyncio.sleep(5) - print('About to cancel bar') + print("About to cancel bar") task.cancel() + async def main(rex): bar_task = asyncio.create_task(bar(70)) # Note args here tasks = [] @@ -48,12 +53,12 @@ async def main(rex): try: res = await asyncio.gather(*tasks, return_exceptions=rex) except asyncio.TimeoutError: - print('foo timed out.') - res = 'No result' - print('Result: ', res) + print("foo timed out.") + res = "No result" + print("Result: ", res) -exp_false = '''Test runs for 10s. Expected output: +exp_false = """Test runs for 10s. Expected output: Start cancellable bar() Start normal coro barking() @@ -65,8 +70,8 @@ async def main(rex): foo timed out. Result: No result -''' -exp_true = '''Test runs for 10s. Expected output: +""" +exp_true = """Test runs for 10s. Expected output: Start cancellable bar() Start normal coro barking() @@ -77,12 +82,14 @@ async def main(rex): Trapped foo timeout. Result: [42, TimeoutError()] -''' +""" + def printexp(st): - print('\x1b[32m') + print("\x1b[32m") print(st) - print('\x1b[39m') + print("\x1b[39m") + def test(rex): st = exp_true if rex else exp_false @@ -90,11 +97,12 @@ def test(rex): try: asyncio.run(main(rex)) except KeyboardInterrupt: - print('Interrupted') + print("Interrupted") finally: asyncio.new_event_loop() print() - print('as_demos.gather.test() to run again.') - print('as_demos.gather.test(True) to see effect of return_exceptions.') + print("as_demos.gather.test() to run again.") + print("as_demos.gather.test(True) to see effect of return_exceptions.") + test(rex=False) diff --git a/v3/as_demos/iorw.py b/v3/as_demos/iorw.py index a5f9fa5..8d91f5d 100644 --- a/v3/as_demos/iorw.py +++ b/v3/as_demos/iorw.py @@ -6,8 +6,9 @@ # Timers asynchronously set the hardware ready. import io, pyb -import uasyncio as asyncio +import asyncio import micropython + micropython.alloc_emergency_exception_buf(100) MP_STREAM_POLL_RD = const(1) @@ -15,20 +16,22 @@ MP_STREAM_POLL = const(3) MP_STREAM_ERROR = const(-1) + def printbuf(this_io): - print(bytes(this_io.wbuf[:this_io.wprint_len]).decode(), end='') + print(bytes(this_io.wbuf[: this_io.wprint_len]).decode(), end="") + class MyIO(io.IOBase): def __init__(self, read=False, write=False): self.ready_rd = False # Read and write not ready - self.rbuf = b'ready\n' # Read buffer + self.rbuf = b"ready\n" # Read buffer self.ridx = 0 - pyb.Timer(4, freq = 5, callback = self.do_input) - self.wch = b'' + pyb.Timer(4, freq=5, callback=self.do_input) + self.wch = b"" self.wbuf = bytearray(100) # Write buffer self.wprint_len = 0 self.widx = 0 - pyb.Timer(5, freq = 10, callback = self.do_output) + pyb.Timer(5, freq=10, callback=self.do_output) # Read callback: emulate asynchronous input from hardware. # Typically would put bytes into a ring buffer and set .ready_rd. @@ -41,12 +44,11 @@ def do_output(self, t): if self.wch: self.wbuf[self.widx] = self.wch self.widx += 1 - if self.wch == ord('\n'): + if self.wch == ord("\n"): self.wprint_len = self.widx # Save for schedule micropython.schedule(printbuf, self) self.widx = 0 - self.wch = b'' - + self.wch = b"" def ioctl(self, req, arg): # see ports/stm32/uart.c ret = MP_STREAM_ERROR @@ -64,7 +66,7 @@ def ioctl(self, req, arg): # see ports/stm32/uart.c def readline(self): self.ready_rd = False # Set by timer cb do_input ch = self.rbuf[self.ridx] - if ch == ord('\n'): + if ch == ord("\n"): self.ridx = 0 else: self.ridx += 1 @@ -77,11 +79,13 @@ def write(self, buf, off=0, sz=0): self.wch = buf[off] # Hardware starts to write a char return 1 # 1 byte written. uasyncio waits on ioctl write ready + async def receiver(myior): sreader = asyncio.StreamReader(myior) while True: res = await sreader.readline() - print('Received', res) + print("Received", res) + async def sender(myiow): swriter = asyncio.StreamWriter(myiow, {}) @@ -89,12 +93,13 @@ async def sender(myiow): count = 0 while True: count += 1 - tosend = 'Wrote Hello MyIO {}\n'.format(count) - await swriter.awrite(tosend.encode('UTF8')) + tosend = "Wrote Hello MyIO {}\n".format(count) + await swriter.awrite(tosend.encode("UTF8")) await asyncio.sleep(2) + def printexp(): - st = '''Received b'ready\\n' + st = """Received b'ready\\n' Received b'ready\\n' Received b'ready\\n' Received b'ready\\n' @@ -107,10 +112,11 @@ def printexp(): Received b'ready\\n' ... Runs until interrupted (ctrl-c). -''' - print('\x1b[32m') +""" + print("\x1b[32m") print(st) - print('\x1b[39m') + print("\x1b[39m") + printexp() myio = MyIO() diff --git a/v3/as_demos/rate.py b/v3/as_demos/rate.py index ea27ba8..46cb5b2 100644 --- a/v3/as_demos/rate.py +++ b/v3/as_demos/rate.py @@ -17,7 +17,7 @@ # the reference board running MP V1.18. Results may vary with firmware # depending on the layout of code in RAM/IRAM -import uasyncio as asyncio +import asyncio num_coros = (100, 200, 500, 1000) iterations = [0, 0, 0, 0] @@ -25,17 +25,19 @@ count = 0 done = False + async def foo(): global count while True: await asyncio.sleep_ms(0) count += 1 + async def test(): global count, done old_n = 0 for n, n_coros in enumerate(num_coros): - print('Testing {} coros for {}secs'.format(n_coros, duration)) + print("Testing {} coros for {}secs".format(n_coros, duration)) count = 0 for _ in range(n_coros - old_n): asyncio.create_task(foo()) @@ -44,12 +46,17 @@ async def test(): iterations[n] = count done = True + async def report(): asyncio.create_task(test()) while not done: await asyncio.sleep(1) for x, n in enumerate(num_coros): - print('Coros {:4d} Iterations/sec {:5d} Duration {:3d}us'.format( - n, int(iterations[x]/duration), int(duration*1000000/iterations[x]))) + print( + "Coros {:4d} Iterations/sec {:5d} Duration {:3d}us".format( + n, int(iterations[x] / duration), int(duration * 1000000 / iterations[x]) + ) + ) + asyncio.run(report()) diff --git a/v3/as_demos/roundrobin.py b/v3/as_demos/roundrobin.py index 5a82bb8..79bc60d 100644 --- a/v3/as_demos/roundrobin.py +++ b/v3/as_demos/roundrobin.py @@ -9,7 +9,7 @@ # 4249 - with a hack where sleep_ms(0) was replaced with yield # Using sleep_ms(0) 2750 -import uasyncio as asyncio +import asyncio count = 0 period = 5 @@ -20,15 +20,15 @@ async def foo(n): while True: await asyncio.sleep_ms(0) count += 1 - print('Foo', n) + print("Foo", n) async def main(delay): for n in range(1, 4): asyncio.create_task(foo(n)) - print('Testing for {:d} seconds'.format(delay)) + print("Testing for {:d} seconds".format(delay)) await asyncio.sleep(delay) asyncio.run(main(period)) -print('Coro executions per sec =', count/period) +print("Coro executions per sec =", count / period) diff --git a/v3/as_drivers/as_GPS/as_GPS.py b/v3/as_drivers/as_GPS/as_GPS.py index b37a311..f1f553c 100644 --- a/v3/as_drivers/as_GPS/as_GPS.py +++ b/v3/as_drivers/as_GPS/as_GPS.py @@ -13,15 +13,12 @@ # Ported to uasyncio V3 OK. -try: - import uasyncio as asyncio -except ImportError: - import asyncio +import asyncio try: from micropython import const except ImportError: - const = lambda x : x + const = lambda x: x from math import modf @@ -66,10 +63,12 @@ class AS_GPS(object): # https://stackoverflow.com/questions/9847213/how-do-i-get-the-day-of-week-given-a-date-in-python?noredirect=1&lq=1 # Adapted for Python 3 and Pyboard RTC format. @staticmethod - def _week_day(year, month, day, offset = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]): + def _week_day( + year, month, day, offset=[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334] + ): aux = year - 1700 - (1 if month <= 2 else 0) # day_of_week for 1700/1/1 = 5, Friday - day_of_week = 5 + day_of_week = 5 # partial sum of days betweem current date and 1700/1/1 day_of_week += (aux + (1 if month <= 2 else 0)) * 365 # leap year correction @@ -89,12 +88,14 @@ def _crc_check(res, ascii_crc): return False x = 1 crc_xor = 0 - while res[x] != '*': + while res[x] != "*": crc_xor ^= ord(res[x]) x += 1 return crc_xor == crc - def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC, fix_cb_args=()): + def __init__( + self, sreader, local_offset=0, fix_cb=lambda *_: None, cb_mask=RMC, fix_cb_args=() + ): self._sreader = sreader # If None testing: update is called with simulated data self._fix_cb = fix_cb self.cb_mask = cb_mask @@ -104,6 +105,7 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC # CPython compatibility. Import utime or time for fix time handling. try: import utime + self._get_time = utime.ticks_ms self._time_diff = utime.ticks_diff self._localtime = utime.localtime @@ -112,19 +114,21 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC # Otherwise default to time module for non-embedded implementations # Should still support millisecond resolution. import time + self._get_time = time.time self._time_diff = lambda start, end: 1000 * (start - end) self._localtime = time.localtime self._mktime = time.mktime # Key: currently supported NMEA sentences. Value: parse method. - self.supported_sentences = {'RMC': self._gprmc, - 'GGA': self._gpgga, - 'VTG': self._gpvtg, - 'GSA': self._gpgsa, - 'GSV': self._gpgsv, - 'GLL': self._gpgll, - } + self.supported_sentences = { + "RMC": self._gprmc, + "GGA": self._gpgga, + "VTG": self._gpvtg, + "GSA": self._gpgsa, + "GSV": self._gpgsv, + "GLL": self._gpgll, + } ##################### # Object Status Flags @@ -149,8 +153,8 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC self.msecs = 0 # Position/Motion - self._latitude = [0, 0.0, 'N'] # (°, mins, N/S) - self._longitude = [0, 0.0, 'W'] # (°, mins, E/W) + self._latitude = [0, 0.0, "N"] # (°, mins, N/S) + self._longitude = [0, 0.0, "W"] # (°, mins, E/W) self._speed = 0.0 # Knot self.course = 0.0 # ° clockwise from N self.altitude = 0.0 # Metres @@ -184,7 +188,7 @@ async def _run(self): while True: res = await self._sreader.readline() try: - res = res.decode('utf8') + res = res.decode("utf8") except UnicodeError: # Garbage: can happen e.g. on baudrate change continue asyncio.create_task(self._update(res)) @@ -194,14 +198,14 @@ async def _run(self): async def _update(self, line): line = line.rstrip() # Copy line # Basic integrity check: may have received partial line e.g on power up - if not line.startswith('$') or not '*' in line or len(line) > self._SENTENCE_LIMIT: + if not line.startswith("$") or not "*" in line or len(line) > self._SENTENCE_LIMIT: return # 2.4ms on Pyboard: if self.FULL_CHECK and not all(10 <= ord(c) <= 126 for c in line): return # Bad character received - a = line.split(',') - segs = a[:-1] + a[-1].split('*') + a = line.split(",") + segs = a[:-1] + a[-1].split("*") await asyncio.sleep(0) if self.FULL_CHECK: # 6ms on Pyboard @@ -215,7 +219,7 @@ async def _update(self, line): segs = segs[:-1] # and checksum seg0 = segs[0] # e.g. GPGLL segx = seg0[2:] # e.g. GLL - if seg0.startswith('G') and segx in self.supported_sentences: + if seg0.startswith("G") and segx in self.supported_sentences: try: s_type = self.supported_sentences[segx](segs) # Parse except ValueError: @@ -258,7 +262,7 @@ def _fix(self, gps_segments, idx_lat, idx_long): lon_mins = float(l_string[3:]) lon_hemi = gps_segments[idx_long + 1] - if lat_hemi not in 'NS'or lon_hemi not in 'EW': + if lat_hemi not in "NS" or lon_hemi not in "EW": raise ValueError self._latitude[0] = lat_degs # In-place to avoid allocation self._latitude[1] = lat_mins @@ -297,12 +301,12 @@ def _set_date_time(self, utc_string, date_string): # Sentence Parsers ######################################## -# For all parsers: -# Initially the ._valid bit for the sentence type is cleared. -# On error a ValueError is raised: trapped by the caller. -# On successful parsing the ._valid bit is set. -# The ._valid mechanism enables the data_received coro to determine what -# sentence types have been received. + # For all parsers: + # Initially the ._valid bit for the sentence type is cleared. + # On error a ValueError is raised: trapped by the caller. + # On successful parsing the ._valid bit is set. + # The ._valid mechanism enables the data_received coro to determine what + # sentence types have been received. # Chip sends rubbish RMC messages before first PPS pulse, but these have # data valid set to 'V' (void) @@ -310,13 +314,13 @@ def _gprmc(self, gps_segments): # Parse RMC sentence self._valid &= ~RMC # Check Receiver Data Valid Flag ('A' active) if not self.battery: - if gps_segments[2] != 'A': + if gps_segments[2] != "A": raise ValueError # UTC Timestamp and date. Can raise ValueError. self._set_date_time(gps_segments[1], gps_segments[9]) # Check Receiver Data Valid Flag ('A' active) - if gps_segments[2] != 'A': + if gps_segments[2] != "A": raise ValueError # Data from Receiver is Valid/Has Fix. Longitude / Latitude @@ -328,10 +332,12 @@ def _gprmc(self, gps_segments): # Parse RMC sentence course = float(gps_segments[8]) if gps_segments[8] else 0.0 # Add Magnetic Variation if firmware supplies it if gps_segments[10]: - mv = float(gps_segments[10]) # Float conversions can throw ValueError, caught by caller. - if gps_segments[11] not in ('EW'): + mv = float( + gps_segments[10] + ) # Float conversions can throw ValueError, caught by caller. + if gps_segments[11] not in ("EW"): raise ValueError - self.magvar = mv if gps_segments[11] == 'E' else -mv + self.magvar = mv if gps_segments[11] == "E" else -mv # Update Object Data self._speed = spd_knt self.course = course @@ -341,7 +347,7 @@ def _gprmc(self, gps_segments): # Parse RMC sentence def _gpgll(self, gps_segments): # Parse GLL sentence self._valid &= ~GLL # Check Receiver Data Valid Flag - if gps_segments[6] != 'A': # Invalid. Don't update data + if gps_segments[6] != "A": # Invalid. Don't update data raise ValueError # Data from Receiver is Valid/Has Fix. Longitude / Latitude @@ -429,9 +435,13 @@ def _gpgsv(self, gps_segments): # Calculate Number of Satelites to pull data for and thus how many segment positions to read if num_sv_sentences == current_sv_sentence: - sat_segment_limit = ((sats_in_view % 4) * 4) + 4 # Last sentence may have 1-4 satellites + sat_segment_limit = ( + (sats_in_view % 4) * 4 + ) + 4 # Last sentence may have 1-4 satellites else: - sat_segment_limit = 20 # Non-last sentences have 4 satellites and thus read up to position 20 + sat_segment_limit = ( + 20 # Non-last sentences have 4 satellites and thus read up to position 20 + ) # Try to recover data for up to 4 satellites in sentence for sats in range(4, sat_segment_limit, 4): @@ -444,18 +454,18 @@ def _gpgsv(self, gps_segments): raise ValueError # Abandon try: # elevation can be null (no value) when not tracking - elevation = int(gps_segments[sats+1]) - except (ValueError,IndexError): + elevation = int(gps_segments[sats + 1]) + except (ValueError, IndexError): elevation = None try: # azimuth can be null (no value) when not tracking - azimuth = int(gps_segments[sats+2]) - except (ValueError,IndexError): + azimuth = int(gps_segments[sats + 2]) + except (ValueError, IndexError): azimuth = None try: # SNR can be null (no value) when not tracking - snr = int(gps_segments[sats+3]) - except (ValueError,IndexError): + snr = int(gps_segments[sats + 3]) + except (ValueError, IndexError): snr = None # If no PRN is found, then the sentence has no more satellites to read else: @@ -484,8 +494,7 @@ def _gpgsv(self, gps_segments): ######################################### # Data Validity. On startup data may be invalid. During an outage it will be absent. - async def data_received(self, position=False, course=False, date=False, - altitude=False): + async def data_received(self, position=False, course=False, date=False, altitude=False): self._valid = 0 # Assume no messages at start result = False while not result: @@ -517,7 +526,7 @@ def latitude(self, coord_format=DD): return [self._latitude[0], mins, seconds, self._latitude[2]] elif coord_format == DM: return self._latitude - raise ValueError('Unknown latitude format.') + raise ValueError("Unknown latitude format.") def longitude(self, coord_format=DD): # Format Longitude Data Correctly @@ -530,7 +539,7 @@ def longitude(self, coord_format=DD): return [self._longitude[0], mins, seconds, self._longitude[2]] elif coord_format == DM: return self._longitude - raise ValueError('Unknown longitude format.') + raise ValueError("Unknown longitude format.") def speed(self, units=KNOT): if units == KNOT: @@ -539,7 +548,7 @@ def speed(self, units=KNOT): return self._speed * 1.852 if units == MPH: return self._speed * 1.151 - raise ValueError('Unknown speed units.') + raise ValueError("Unknown speed units.") async def get_satellite_data(self): self._total_sv_sentences = 0 @@ -556,36 +565,37 @@ def time_since_fix(self): # ms since last valid fix def compass_direction(self): # Return cardinal point as string. from .as_GPS_utils import compass_direction + return compass_direction(self) def latitude_string(self, coord_format=DM): if coord_format == DD: - return '{:3.6f}° {:s}'.format(*self.latitude(DD)) + return "{:3.6f}° {:s}".format(*self.latitude(DD)) if coord_format == DMS: return """{:3d}° {:2d}' {:2d}" {:s}""".format(*self.latitude(DMS)) if coord_format == KML: form_lat = self.latitude(DD) - return '{:4.6f}'.format(form_lat[0] if form_lat[1] == 'N' else -form_lat[0]) + return "{:4.6f}".format(form_lat[0] if form_lat[1] == "N" else -form_lat[0]) return "{:3d}° {:3.4f}' {:s}".format(*self.latitude(coord_format)) def longitude_string(self, coord_format=DM): if coord_format == DD: - return '{:3.6f}° {:s}'.format(*self.longitude(DD)) + return "{:3.6f}° {:s}".format(*self.longitude(DD)) if coord_format == DMS: return """{:3d}° {:2d}' {:2d}" {:s}""".format(*self.longitude(DMS)) if coord_format == KML: form_long = self.longitude(DD) - return '{:4.6f}'.format(form_long[0] if form_long[1] == 'E' else -form_long[0]) + return "{:4.6f}".format(form_long[0] if form_long[1] == "E" else -form_long[0]) return "{:3d}° {:3.4f}' {:s}".format(*self.longitude(coord_format)) def speed_string(self, unit=KPH): - sform = '{:3.2f} {:s}' + sform = "{:3.2f} {:s}" speed = self.speed(unit) if unit == MPH: - return sform.format(speed, 'mph') + return sform.format(speed, "mph") elif unit == KNOT: - return sform.format(speed, 'knots') - return sform.format(speed, 'km/h') + return sform.format(speed, "knots") + return sform.format(speed, "km/h") # Return local time (hrs: int, mins: int, secs:float) @property @@ -608,8 +618,9 @@ def utc(self): def time_string(self, local=True): hrs, mins, secs = self.local_time if local else self.utc - return '{:02d}:{:02d}:{:02d}'.format(hrs, mins, secs) + return "{:02d}:{:02d}:{:02d}".format(hrs, mins, secs) def date_string(self, formatting=MDY): from .as_GPS_utils import date_string + return date_string(self, formatting) diff --git a/v3/as_drivers/as_GPS/as_GPS_time.py b/v3/as_drivers/as_GPS/as_GPS_time.py index 943cfc0..5742c33 100644 --- a/v3/as_drivers/as_GPS/as_GPS_time.py +++ b/v3/as_drivers/as_GPS/as_GPS_time.py @@ -6,7 +6,7 @@ # Copyright (c) 2018-2020 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -import uasyncio as asyncio +import asyncio import pyb import utime import math @@ -17,12 +17,12 @@ PPS_PIN = pyb.Pin.board.X3 UART_ID = 4 -print('Available tests:') -print('calibrate(minutes=5) Set and calibrate the RTC.') -print('drift(minutes=5) Repeatedly print the difference between RTC and GPS time.') -print('time(minutes=1) Print get_ms() and get_t_split values.') -print('usec(minutes=1) Measure accuracy of usec timer.') -print('Press ctrl-d to reboot after each test.') +print("Available tests:") +print("calibrate(minutes=5) Set and calibrate the RTC.") +print("drift(minutes=5) Repeatedly print the difference between RTC and GPS time.") +print("time(minutes=1) Print get_ms() and get_t_split values.") +print("usec(minutes=1) Measure accuracy of usec timer.") +print("Press ctrl-d to reboot after each test.") # Setup for tests. Red LED toggles on fix, green on PPS interrupt. async def setup(): @@ -31,63 +31,74 @@ async def setup(): uart = pyb.UART(UART_ID, 9600, read_buf_len=200) sreader = asyncio.StreamReader(uart) pps_pin = pyb.Pin(PPS_PIN, pyb.Pin.IN) - return GPS_Timer(sreader, pps_pin, local_offset=1, - fix_cb=lambda *_: red.toggle(), - pps_cb=lambda *_: green.toggle()) + return GPS_Timer( + sreader, + pps_pin, + local_offset=1, + fix_cb=lambda *_: red.toggle(), + pps_cb=lambda *_: green.toggle(), + ) + # Test terminator: task sets the passed event after the passed time. async def killer(end_event, minutes): - print('Will run for {} minutes.'.format(minutes)) + print("Will run for {} minutes.".format(minutes)) await asyncio.sleep(minutes * 60) end_event.set() + # ******** Calibrate and set the Pyboard RTC ******** async def do_cal(minutes): gps = await setup() await gps.calibrate(minutes) gps.close() + def calibrate(minutes=5): asyncio.run(do_cal(minutes)) + # ******** Drift test ******** # Every 10s print the difference between GPS time and RTC time async def drift_test(terminate, gps): dstart = await gps.delta() while not terminate.is_set(): dt = await gps.delta() - print('{} Delta {}μs'.format(gps.time_string(), dt)) + print("{} Delta {}μs".format(gps.time_string(), dt)) await asyncio.sleep(10) return dt - dstart + async def do_drift(minutes): - print('Setting up GPS.') + print("Setting up GPS.") gps = await setup() - print('Waiting for time data.') + print("Waiting for time data.") await gps.ready() terminate = asyncio.Event() asyncio.create_task(killer(terminate, minutes)) - print('Setting RTC.') + print("Setting RTC.") await gps.set_rtc() - print('Measuring drift.') + print("Measuring drift.") change = await drift_test(terminate, gps) - ush = int(60 * change/minutes) + ush = int(60 * change / minutes) spa = int(ush * 365 * 24 / 1000000) - print('Rate of change {}μs/hr {}secs/year'.format(ush, spa)) + print("Rate of change {}μs/hr {}secs/year".format(ush, spa)) gps.close() + def drift(minutes=5): asyncio.run(do_drift(minutes)) + # ******** Time printing demo ******** # Every 10s print the difference between GPS time and RTC time async def do_time(minutes): - fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}' - print('Setting up GPS.') + fstr = "{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}" + print("Setting up GPS.") gps = await setup() - print('Waiting for time data.') + print("Waiting for time data.") await gps.ready() - print('Setting RTC.') + print("Setting RTC.") await gps.set_rtc() terminate = asyncio.Event() asyncio.create_task(killer(terminate, minutes)) @@ -98,9 +109,11 @@ async def do_time(minutes): print(fstr.format(gps.get_ms(), t[0], t[1], t[2], t[3])) gps.close() + def time(minutes=1): asyncio.run(do_time(minutes)) + # ******** Measure accracy of μs clock ******** # At 9600 baud see occasional lag of up to 3ms followed by similar lead. # This implies that the ISR is being disabled for that period (~3 chars). @@ -111,6 +124,8 @@ def time(minutes=1): # Callback occurs in interrupt context us_acquired = None + + def us_cb(my_gps, tick, led): global us_acquired # Time of previous PPS edge in ticks_us() if us_acquired is not None: @@ -119,6 +134,7 @@ def us_cb(my_gps, tick, led): us_acquired = my_gps.acquired led.toggle() + # Setup initialises with above callback async def us_setup(tick): red = pyb.LED(1) @@ -126,15 +142,21 @@ async def us_setup(tick): uart = pyb.UART(UART_ID, 9600, read_buf_len=200) sreader = asyncio.StreamReader(uart) pps_pin = pyb.Pin(PPS_PIN, pyb.Pin.IN) - return GPS_Timer(sreader, pps_pin, local_offset=1, - fix_cb=lambda *_: red.toggle(), - pps_cb=us_cb, pps_cb_args=(tick, yellow)) + return GPS_Timer( + sreader, + pps_pin, + local_offset=1, + fix_cb=lambda *_: red.toggle(), + pps_cb=us_cb, + pps_cb_args=(tick, yellow), + ) + async def do_usec(minutes): tick = Message() - print('Setting up GPS.') + print("Setting up GPS.") gps = await us_setup(tick) - print('Waiting for time data.') + print("Waiting for time data.") await gps.ready() max_us = 0 min_us = 0 @@ -149,7 +171,7 @@ async def do_usec(minutes): tick.clear() err = 1000000 - usecs count += 1 - print('Timing discrepancy is {:4d}μs {}'.format(err, '(skipped)' if count < 3 else '')) + print("Timing discrepancy is {:4d}μs {}".format(err, "(skipped)" if count < 3 else "")) if count < 3: # Discard 1st two samples from statistics continue # as these can be unrepresentative max_us = max(max_us, err) @@ -157,9 +179,14 @@ async def do_usec(minutes): sd += err * err nsamples += 1 # SD: apply Bessel's correction for infinite population - sd = int(math.sqrt(sd/(nsamples - 1))) - print('Timing discrepancy is: {:5d}μs max {:5d}μs min. Standard deviation {:4d}μs'.format(max_us, min_us, sd)) + sd = int(math.sqrt(sd / (nsamples - 1))) + print( + "Timing discrepancy is: {:5d}μs max {:5d}μs min. Standard deviation {:4d}μs".format( + max_us, min_us, sd + ) + ) gps.close() + def usec(minutes=1): asyncio.run(do_usec(minutes)) diff --git a/v3/as_drivers/as_GPS/as_rwGPS_time.py b/v3/as_drivers/as_GPS/as_rwGPS_time.py index cff1844..5cf94cd 100644 --- a/v3/as_drivers/as_GPS/as_rwGPS_time.py +++ b/v3/as_drivers/as_GPS/as_rwGPS_time.py @@ -12,7 +12,7 @@ # Data has (for 38400): $PMTK251,38400*27 # Sending: $PMTK251,38400*27\r\n' -import uasyncio as asyncio +import asyncio from uasyncio import Event from threadsafe.message import Message import pyb @@ -29,16 +29,17 @@ UPDATE_INTERVAL = 100 READ_BUF_LEN = 200 -print('Available tests:') -print('calibrate(minutes=5) Set and calibrate the RTC.') -print('drift(minutes=5) Repeatedly print the difference between RTC and GPS time.') -print('time(minutes=1) Print get_ms() and get_t_split values.') -print('usec(minutes=1) Measure accuracy of usec timer.') -print('Press ctrl-d to reboot after each test.') +print("Available tests:") +print("calibrate(minutes=5) Set and calibrate the RTC.") +print("drift(minutes=5) Repeatedly print the difference between RTC and GPS time.") +print("time(minutes=1) Print get_ms() and get_t_split values.") +print("usec(minutes=1) Measure accuracy of usec timer.") +print("Press ctrl-d to reboot after each test.") # Initially use factory baudrate uart = pyb.UART(UART_ID, 9600, read_buf_len=READ_BUF_LEN) + async def shutdown(): global gps # Normally UART is already at BAUDRATE. But if last session didn't restore @@ -47,17 +48,18 @@ async def shutdown(): uart.init(BAUDRATE) await asyncio.sleep(0.5) await gps.command(FULL_COLD_START) - print('Factory reset') + print("Factory reset") gps.close() # Stop ISR - #print('Restoring default baudrate (9600).') - #await gps.baudrate(9600) - #uart.init(9600) - #gps.close() # Stop ISR - #print('Restoring default 1s update rate.') - #await asyncio.sleep(0.5) - #await gps.update_interval(1000) # 1s update rate - #print('Restoring satellite data.') - #await gps.command(as_rwGPS.DEFAULT_SENTENCES) # Restore satellite data + # print('Restoring default baudrate (9600).') + # await gps.baudrate(9600) + # uart.init(9600) + # gps.close() # Stop ISR + # print('Restoring default 1s update rate.') + # await asyncio.sleep(0.5) + # await gps.update_interval(1000) # 1s update rate + # print('Restoring satellite data.') + # await gps.command(as_rwGPS.DEFAULT_SENTENCES) # Restore satellite data + # Setup for tests. Red LED toggles on fix, blue on PPS interrupt. async def setup(): @@ -67,9 +69,14 @@ async def setup(): sreader = asyncio.StreamReader(uart) swriter = asyncio.StreamWriter(uart, {}) pps_pin = pyb.Pin(PPS_PIN, pyb.Pin.IN) - gps = GPS_RWTimer(sreader, swriter, pps_pin, local_offset=1, - fix_cb=lambda *_: red.toggle(), - pps_cb=lambda *_: blue.toggle()) + gps = GPS_RWTimer( + sreader, + swriter, + pps_pin, + local_offset=1, + fix_cb=lambda *_: red.toggle(), + pps_cb=lambda *_: blue.toggle(), + ) gps.FULL_CHECK = False await asyncio.sleep(2) await gps.baudrate(BAUDRATE) @@ -77,52 +84,58 @@ async def setup(): await asyncio.sleep(1) await gps.enable(gsa=0, gsv=0) # Disable satellite data await gps.update_interval(UPDATE_INTERVAL) - pstr = 'Baudrate {} update interval {}ms satellite messages disabled.' + pstr = "Baudrate {} update interval {}ms satellite messages disabled." print(pstr.format(BAUDRATE, UPDATE_INTERVAL)) return gps + # Test terminator: task sets the passed event after the passed time. async def killer(end_event, minutes): - print('Will run for {} minutes.'.format(minutes)) + print("Will run for {} minutes.".format(minutes)) await asyncio.sleep(minutes * 60) end_event.set() + # ******** Calibrate and set the Pyboard RTC ******** async def do_cal(minutes): gps = await setup() await gps.calibrate(minutes) + def calibrate(minutes=5): try: asyncio.run(do_cal(minutes)) finally: asyncio.run(shutdown()) + # ******** Drift test ******** # Every 10s print the difference between GPS time and RTC time async def drift_test(terminate, gps): dstart = await gps.delta() while not terminate.is_set(): dt = await gps.delta() - print('{} Delta {}μs'.format(gps.time_string(), dt)) + print("{} Delta {}μs".format(gps.time_string(), dt)) await asyncio.sleep(10) return dt - dstart + async def do_drift(minutes): global gps - print('Setting up GPS.') + print("Setting up GPS.") gps = await setup() - print('Waiting for time data.') + print("Waiting for time data.") await gps.ready() - print('Setting RTC.') + print("Setting RTC.") await gps.set_rtc() - print('Measuring drift.') + print("Measuring drift.") terminate = Event() asyncio.create_task(killer(terminate, minutes)) change = await drift_test(terminate, gps) - ush = int(60 * change/minutes) + ush = int(60 * change / minutes) spa = int(ush * 365 * 24 / 1000000) - print('Rate of change {}μs/hr {}secs/year'.format(ush, spa)) + print("Rate of change {}μs/hr {}secs/year".format(ush, spa)) + def drift(minutes=5): try: @@ -130,18 +143,19 @@ def drift(minutes=5): finally: asyncio.run(shutdown()) + # ******** Time printing demo ******** # Every 10s print the difference between GPS time and RTC time async def do_time(minutes): global gps - fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}' - print('Setting up GPS.') + fstr = "{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}" + print("Setting up GPS.") gps = await setup() - print('Waiting for time data.') + print("Waiting for time data.") await gps.ready() - print('Setting RTC.') + print("Setting RTC.") await gps.set_rtc() - print('RTC is set.') + print("RTC is set.") terminate = Event() asyncio.create_task(killer(terminate, minutes)) while not terminate.is_set(): @@ -150,12 +164,14 @@ async def do_time(minutes): t = gps.get_t_split() print(fstr.format(gps.get_ms(), t[0], t[1], t[2], t[3])) + def time(minutes=1): try: asyncio.run(do_time(minutes)) finally: asyncio.run(shutdown()) + # ******** Measure accracy of μs clock ******** # Test produces better numbers at 57600 baud (SD 112μs) # and better still at 10Hz update rate (SD 34μs). @@ -163,6 +179,8 @@ def time(minutes=1): # Callback occurs in interrupt context us_acquired = None # Time of previous PPS edge in ticks_us() + + def us_cb(my_gps, tick, led): global us_acquired if us_acquired is not None: @@ -171,6 +189,7 @@ def us_cb(my_gps, tick, led): us_acquired = my_gps.acquired led.toggle() + # Setup initialises with above callback async def us_setup(tick): global uart, gps # For shutdown @@ -179,9 +198,15 @@ async def us_setup(tick): sreader = asyncio.StreamReader(uart) swriter = asyncio.StreamWriter(uart, {}) pps_pin = pyb.Pin(PPS_PIN, pyb.Pin.IN) - gps = GPS_RWTimer(sreader, swriter, pps_pin, local_offset=1, - fix_cb=lambda *_: red.toggle(), - pps_cb=us_cb, pps_cb_args=(tick, blue)) + gps = GPS_RWTimer( + sreader, + swriter, + pps_pin, + local_offset=1, + fix_cb=lambda *_: red.toggle(), + pps_cb=us_cb, + pps_cb_args=(tick, blue), + ) gps.FULL_CHECK = False await asyncio.sleep(2) await gps.baudrate(BAUDRATE) @@ -189,15 +214,16 @@ async def us_setup(tick): await asyncio.sleep(1) await gps.enable(gsa=0, gsv=0) # Disable satellite data await gps.update_interval(UPDATE_INTERVAL) - pstr = 'Baudrate {} update interval {}ms satellite messages disabled.' + pstr = "Baudrate {} update interval {}ms satellite messages disabled." print(pstr.format(BAUDRATE, UPDATE_INTERVAL)) + async def do_usec(minutes): global gps tick = Message() - print('Setting up GPS.') + print("Setting up GPS.") await us_setup(tick) - print('Waiting for time data.') + print("Waiting for time data.") await gps.ready() max_us = 0 min_us = 0 @@ -212,7 +238,7 @@ async def do_usec(minutes): tick.clear() err = 1000000 - usecs count += 1 - print('Timing discrepancy is {:4d}μs {}'.format(err, '(skipped)' if count < 3 else '')) + print("Timing discrepancy is {:4d}μs {}".format(err, "(skipped)" if count < 3 else "")) if count < 3: # Discard 1st two samples from statistics continue # as these can be unrepresentative max_us = max(max_us, err) @@ -220,8 +246,13 @@ async def do_usec(minutes): sd += err * err nsamples += 1 # SD: apply Bessel's correction for infinite population - sd = int(math.sqrt(sd/(nsamples - 1))) - print('Timing discrepancy is: {:5d}μs max {:5d}μs min. Standard deviation {:4d}μs'.format(max_us, min_us, sd)) + sd = int(math.sqrt(sd / (nsamples - 1))) + print( + "Timing discrepancy is: {:5d}μs max {:5d}μs min. Standard deviation {:4d}μs".format( + max_us, min_us, sd + ) + ) + def usec(minutes=1): try: diff --git a/v3/as_drivers/as_GPS/as_tGPS.py b/v3/as_drivers/as_GPS/as_tGPS.py index 92cce67..78bebf3 100644 --- a/v3/as_drivers/as_GPS/as_tGPS.py +++ b/v3/as_drivers/as_GPS/as_tGPS.py @@ -4,10 +4,12 @@ # Released under the MIT License (MIT) - see LICENSE file # TODO Test machine version. Replace LED with callback. Update tests and doc. -import uasyncio as asyncio +import asyncio import machine + try: import pyb + on_pyboard = True rtc = pyb.RTC() except ImportError: @@ -23,27 +25,49 @@ # Convenience function. Return RTC seconds since midnight as float def rtc_secs(): if not on_pyboard: - raise OSError('Only available on STM targets.') + raise OSError("Only available on STM targets.") dt = rtc.datetime() - return 3600*dt[4] + 60*dt[5] + dt[6] + (255 - dt[7])/256 + return 3600 * dt[4] + 60 * dt[5] + dt[6] + (255 - dt[7]) / 256 + # Constructor for GPS_Timer class -def gps_ro_t_init(self, sreader, pps_pin, local_offset=0, - fix_cb=lambda *_ : None, cb_mask=RMC, fix_cb_args=(), - pps_cb=lambda *_ : None, pps_cb_args=()): +def gps_ro_t_init( + self, + sreader, + pps_pin, + local_offset=0, + fix_cb=lambda *_: None, + cb_mask=RMC, + fix_cb_args=(), + pps_cb=lambda *_: None, + pps_cb_args=(), +): AS_GPS.__init__(self, sreader, local_offset, fix_cb, cb_mask, fix_cb_args) self.setup(pps_pin, pps_cb, pps_cb_args) + # Constructor for GPS_RWTimer class -def gps_rw_t_init(self, sreader, swriter, pps_pin, local_offset=0, - fix_cb=lambda *_ : None, cb_mask=RMC, fix_cb_args=(), - msg_cb=lambda *_ : None, msg_cb_args=(), - pps_cb=lambda *_ : None, pps_cb_args=()): - GPS.__init__(self, sreader, swriter, local_offset, fix_cb, cb_mask, fix_cb_args, - msg_cb, msg_cb_args) +def gps_rw_t_init( + self, + sreader, + swriter, + pps_pin, + local_offset=0, + fix_cb=lambda *_: None, + cb_mask=RMC, + fix_cb_args=(), + msg_cb=lambda *_: None, + msg_cb_args=(), + pps_cb=lambda *_: None, + pps_cb_args=(), +): + GPS.__init__( + self, sreader, swriter, local_offset, fix_cb, cb_mask, fix_cb_args, msg_cb, msg_cb_args + ) self.setup(pps_pin, pps_cb, pps_cb_args) -class GPS_Tbase(): + +class GPS_Tbase: def setup(self, pps_pin, pps_cb, pps_cb_args): self._pps_pin = pps_pin self._pps_cb = pps_cb @@ -52,13 +76,13 @@ def setup(self, pps_pin, pps_cb, pps_cb_args): self.t_ms = 0 # ms since midnight self.acquired = None # Value of ticks_us at edge of PPS self._rtc_set = False # Set RTC flag - self._rtcbuf = [0]*8 # Buffer for RTC setting - self._time = [0]*4 # get_t_split() time buffer. + self._rtcbuf = [0] * 8 # Buffer for RTC setting + self._time = [0] * 4 # get_t_split() time buffer. asyncio.create_task(self._start()) async def _start(self): await self.data_received(date=True) - self._pps_pin.irq(self._isr, trigger = machine.Pin.IRQ_RISING) + self._pps_pin.irq(self._isr, trigger=machine.Pin.IRQ_RISING) def close(self): self._pps_pin.irq(None) @@ -69,7 +93,7 @@ def _isr(self, _): acquired = utime.ticks_us() # Save time of PPS # Time in last NMEA sentence was time of last PPS. # Reduce to integer secs since midnight local time. - isecs = (self.epoch_time + int(3600*self.local_offset)) % 86400 + isecs = (self.epoch_time + int(3600 * self.local_offset)) % 86400 # ms since midnight (28 bits). Add in any ms in RMC data msecs = isecs * 1000 + self.msecs # This PPS is presumed to be one update later @@ -108,7 +132,7 @@ def _dtset(self, wday): # Set flag and let ISR set the RTC. Pause until done. async def set_rtc(self): if not on_pyboard: - raise OSError('Only available on STM targets.') + raise OSError("Only available on STM targets.") self._rtc_set = True while self._rtc_set: await asyncio.sleep_ms(250) @@ -125,7 +149,7 @@ def _get_rtc_usecs(self): # PPS leading edge. async def delta(self): if not on_pyboard: - raise OSError('Only available on STM targets.') + raise OSError("Only available on STM targets.") rtc_time, gps_time = await self._await_pps() # μs since Y2K at time of latest PPS return rtc_time - gps_time @@ -141,25 +165,25 @@ async def _await_pps(self): while rtc.datetime()[7] == st: # Wait for RTC to change (4ms max) pass dt = utime.ticks_diff(utime.ticks_us(), self.acquired) - trtc = self._get_rtc_usecs() - dt # Read RTC now and adjust for PPS edge - tgps = 1000000 * (self.epoch_time + 3600*self.local_offset + 1) + trtc = self._get_rtc_usecs() - dt # Read RTC now and adjust for PPS edge + tgps = 1000000 * (self.epoch_time + 3600 * self.local_offset + 1) return trtc, tgps # Non-realtime calculation of calibration factor. times are in μs def _calculate(self, gps_start, gps_end, rtc_start, rtc_end): # Duration (μs) between PPS edges - pps_delta = (gps_end - gps_start) + pps_delta = gps_end - gps_start # Duration (μs) between PPS edges as measured by RTC and corrected - rtc_delta = (rtc_end - rtc_start) + rtc_delta = rtc_end - rtc_start ppm = (1000000 * (rtc_delta - pps_delta)) / pps_delta # parts per million - return int(-ppm/0.954) + return int(-ppm / 0.954) # Measure difference between RTC and GPS rate and return calibration factor # If 3 successive identical results are within 1 digit the outcome is considered # valid and the coro quits. async def _getcal(self, minutes=5): if minutes < 1: - raise ValueError('minutes must be >= 1') + raise ValueError("minutes must be >= 1") results = [0, 0, 0] # Last 3 cal results idx = 0 # Index into above circular buffer nresults = 0 # Count of results @@ -175,13 +199,13 @@ async def _getcal(self, minutes=5): # Get RTC time at instant of PPS rtc_end, gps_end = await self._await_pps() cal = self._calculate(gps_start, gps_end, rtc_start, rtc_end) - print('Mins {:d} cal factor {:d}'.format(n + 1, cal)) + print("Mins {:d} cal factor {:d}".format(n + 1, cal)) results[idx] = cal idx += 1 idx %= len(results) nresults += 1 if nresults >= 4 and (abs(max(results) - min(results)) <= 1): - return round(sum(results)/len(results)) + return round(sum(results) / len(results)) return cal # Pause until time/date message received and 1st PPS interrupt has occurred. @@ -191,16 +215,16 @@ async def ready(self): async def calibrate(self, minutes=5): if not on_pyboard: - raise OSError('Only available on STM targets.') - print('Waiting for GPS startup.') + raise OSError("Only available on STM targets.") + print("Waiting for GPS startup.") await self.ready() - print('Waiting up to {} minutes to acquire calibration factor...'.format(minutes)) + print("Waiting up to {} minutes to acquire calibration factor...".format(minutes)) cal = await self._getcal(minutes) if cal <= 512 and cal >= -511: rtc.calibration(cal) - print('Pyboard RTC is calibrated. Factor is {:d}.'.format(cal)) + print("Pyboard RTC is calibrated. Factor is {:d}.".format(cal)) else: - print('Calibration factor {:d} is out of range.'.format(cal)) + print("Calibration factor {:d} is out of range.".format(cal)) # User interface functions: accurate GPS time. # Return GPS time in ms since midnight (small int on 32 bit h/w). @@ -231,8 +255,9 @@ def get_t_split(self): self._time[0] = hrs self._time[1] = mins self._time[2] = secs + ds - self._time[3] = us + ims*1000 + self._time[3] = us + ims * 1000 return self._time -GPS_Timer = type('GPS_Timer', (GPS_Tbase, AS_GPS), {'__init__': gps_ro_t_init}) -GPS_RWTimer = type('GPS_RWTimer', (GPS_Tbase, GPS), {'__init__': gps_rw_t_init}) + +GPS_Timer = type("GPS_Timer", (GPS_Tbase, AS_GPS), {"__init__": gps_ro_t_init}) +GPS_RWTimer = type("GPS_RWTimer", (GPS_Tbase, GPS), {"__init__": gps_rw_t_init}) diff --git a/v3/as_drivers/as_GPS/ast_pb.py b/v3/as_drivers/as_GPS/ast_pb.py index a4a2a6f..de93fed 100644 --- a/v3/as_drivers/as_GPS/ast_pb.py +++ b/v3/as_drivers/as_GPS/ast_pb.py @@ -6,7 +6,7 @@ # Test asynchronous GPS device driver as_pyGPS import pyb -import uasyncio as asyncio +import asyncio from primitives.delay_ms import Delay_ms from .as_GPS import DD, MPH, LONG, AS_GPS @@ -14,72 +14,80 @@ green = pyb.LED(2) ntimeouts = 0 + def callback(gps, _, timer): red.toggle() green.on() timer.trigger(10000) + def timeout(): global ntimeouts green.off() ntimeouts += 1 + # Print satellite data every 10s async def sat_test(gps): while True: d = await gps.get_satellite_data() - print('***** SATELLITE DATA *****') + print("***** SATELLITE DATA *****") for i in d: print(i, d[i]) print() await asyncio.sleep(10) + # Print statistics every 30s async def stats(gps): while True: await asyncio.sleep(30) - print('***** STATISTICS *****') - print('Outages:', ntimeouts) - print('Sentences Found:', gps.clean_sentences) - print('Sentences Parsed:', gps.parsed_sentences) - print('CRC_Fails:', gps.crc_fails) + print("***** STATISTICS *****") + print("Outages:", ntimeouts) + print("Sentences Found:", gps.clean_sentences) + print("Sentences Parsed:", gps.parsed_sentences) + print("CRC_Fails:", gps.crc_fails) print() + # Print navigation data every 4s async def navigation(gps): while True: await asyncio.sleep(4) await gps.data_received(position=True) - print('***** NAVIGATION DATA *****') - print('Data is Valid:', gps._valid) - print('Longitude:', gps.longitude(DD)) - print('Latitude', gps.latitude(DD)) + print("***** NAVIGATION DATA *****") + print("Data is Valid:", gps._valid) + print("Longitude:", gps.longitude(DD)) + print("Latitude", gps.latitude(DD)) print() + async def course(gps): while True: await asyncio.sleep(4) await gps.data_received(course=True) - print('***** COURSE DATA *****') - print('Data is Valid:', gps._valid) - print('Speed:', gps.speed_string(MPH)) - print('Course', gps.course) - print('Compass Direction:', gps.compass_direction()) + print("***** COURSE DATA *****") + print("Data is Valid:", gps._valid) + print("Speed:", gps.speed_string(MPH)) + print("Course", gps.course) + print("Compass Direction:", gps.compass_direction()) print() + async def date(gps): while True: await asyncio.sleep(4) await gps.data_received(date=True) - print('***** DATE AND TIME *****') - print('Data is Valid:', gps._valid) - print('UTC time:', gps.utc) - print('Local time:', gps.local_time) - print('Date:', gps.date_string(LONG)) + print("***** DATE AND TIME *****") + print("Data is Valid:", gps._valid) + print("UTC time:", gps.utc) + print("Local time:", gps.local_time) + print("Date:", gps.date_string(LONG)) print() + async def gps_test(): - print('Initialising') + print("Initialising") # Adapt for other MicroPython hardware uart = pyb.UART(4, 9600, read_buf_len=200) # read_buf_len is precautionary: code runs reliably without it.) @@ -87,7 +95,7 @@ async def gps_test(): timer = Delay_ms(timeout) sentence_count = 0 gps = AS_GPS(sreader, local_offset=1, fix_cb=callback, fix_cb_args=(timer,)) - print('awaiting first fix') + print("awaiting first fix") asyncio.create_task(sat_test(gps)) asyncio.create_task(stats(gps)) asyncio.create_task(navigation(gps)) diff --git a/v3/as_drivers/as_GPS/ast_pbrw.py b/v3/as_drivers/as_GPS/ast_pbrw.py index 6c994b8..3e177ff 100644 --- a/v3/as_drivers/as_GPS/ast_pbrw.py +++ b/v3/as_drivers/as_GPS/ast_pbrw.py @@ -12,7 +12,7 @@ # Yellow toggles on position reading. import pyb -import uasyncio as asyncio +import asyncio from primitives.delay_ms import Delay_ms from .as_GPS import DD, LONG, MPH from .as_rwGPS import * @@ -22,101 +22,111 @@ red, green, yellow = pyb.LED(1), pyb.LED(2), pyb.LED(3) ntimeouts = 0 + def callback(gps, _, timer): red.toggle() green.on() timer.trigger(10000) # Outage is declared after 10s + def cb_timeout(): global ntimeouts green.off() ntimeouts += 1 + def message_cb(gps, segs): - print('Message received:', segs) + print("Message received:", segs) + # Print satellite data every 10s async def sat_test(gps): while True: d = await gps.get_satellite_data() - print('***** SATELLITE DATA *****') - print('Data is Valid:', hex(gps._valid)) + print("***** SATELLITE DATA *****") + print("Data is Valid:", hex(gps._valid)) for i in d: print(i, d[i]) print() await asyncio.sleep(10) + # Print statistics every 30s async def stats(gps): while True: await gps.data_received(position=True) # Wait for a valid fix await asyncio.sleep(30) - print('***** STATISTICS *****') - print('Outages:', ntimeouts) - print('Sentences Found:', gps.clean_sentences) - print('Sentences Parsed:', gps.parsed_sentences) - print('CRC_Fails:', gps.crc_fails) - print('Antenna status:', gps.antenna) - print('Firmware version:', gps.version) - print('Enabled sentences:', gps.enabled) + print("***** STATISTICS *****") + print("Outages:", ntimeouts) + print("Sentences Found:", gps.clean_sentences) + print("Sentences Parsed:", gps.parsed_sentences) + print("CRC_Fails:", gps.crc_fails) + print("Antenna status:", gps.antenna) + print("Firmware version:", gps.version) + print("Enabled sentences:", gps.enabled) print() + # Print navigation data every 4s async def navigation(gps): while True: await asyncio.sleep(4) await gps.data_received(position=True) yellow.toggle() - print('***** NAVIGATION DATA *****') - print('Data is Valid:', hex(gps._valid)) - print('Longitude:', gps.longitude(DD)) - print('Latitude', gps.latitude(DD)) + print("***** NAVIGATION DATA *****") + print("Data is Valid:", hex(gps._valid)) + print("Longitude:", gps.longitude(DD)) + print("Latitude", gps.latitude(DD)) print() + async def course(gps): while True: await asyncio.sleep(4) await gps.data_received(course=True) - print('***** COURSE DATA *****') - print('Data is Valid:', hex(gps._valid)) - print('Speed:', gps.speed_string(MPH)) - print('Course', gps.course) - print('Compass Direction:', gps.compass_direction()) + print("***** COURSE DATA *****") + print("Data is Valid:", hex(gps._valid)) + print("Speed:", gps.speed_string(MPH)) + print("Course", gps.course) + print("Compass Direction:", gps.compass_direction()) print() + async def date(gps): while True: await asyncio.sleep(4) await gps.data_received(date=True) - print('***** DATE AND TIME *****') - print('Data is Valid:', hex(gps._valid)) - print('UTC Time:', gps.utc) - print('Local time:', gps.local_time) - print('Date:', gps.date_string(LONG)) + print("***** DATE AND TIME *****") + print("Data is Valid:", hex(gps._valid)) + print("UTC Time:", gps.utc) + print("Local time:", gps.local_time) + print("Date:", gps.date_string(LONG)) print() + async def change_status(gps, uart): await asyncio.sleep(10) - print('***** Changing status. *****') + print("***** Changing status. *****") await gps.baudrate(BAUDRATE) uart.init(BAUDRATE) - print('***** baudrate 19200 *****') + print("***** baudrate 19200 *****") await asyncio.sleep(5) # Ensure baudrate is sorted - print('***** Query VERSION *****') + print("***** Query VERSION *****") await gps.command(VERSION) await asyncio.sleep(10) - print('***** Query ENABLE *****') + print("***** Query ENABLE *****") await gps.command(ENABLE) await asyncio.sleep(10) # Allow time for 1st report await gps.update_interval(2000) - print('***** Update interval 2s *****') + print("***** Update interval 2s *****") await asyncio.sleep(10) - await gps.enable(gsv = False, chan = False) - print('***** Disable satellite in view and channel messages *****') + await gps.enable(gsv=False, chan=False) + print("***** Disable satellite in view and channel messages *****") await asyncio.sleep(10) - print('***** Query ENABLE *****') + print("***** Query ENABLE *****") await gps.command(ENABLE) + # See README.md re antenna commands # await asyncio.sleep(10) # await gps.command(ANTENNA) @@ -126,9 +136,10 @@ async def change_status(gps, uart): # print('***** Antenna reports turned off *****') # await asyncio.sleep(10) + async def gps_test(): global gps, uart # For shutdown - print('Initialising') + print("Initialising") # Adapt UART instantiation for other MicroPython hardware uart = pyb.UART(4, 9600, read_buf_len=200) # read_buf_len is precautionary: code runs reliably without it. @@ -136,14 +147,15 @@ async def gps_test(): swriter = asyncio.StreamWriter(uart, {}) timer = Delay_ms(cb_timeout) sentence_count = 0 - gps = GPS(sreader, swriter, local_offset=1, fix_cb=callback, - fix_cb_args=(timer,), msg_cb = message_cb) + gps = GPS( + sreader, swriter, local_offset=1, fix_cb=callback, fix_cb_args=(timer,), msg_cb=message_cb + ) await asyncio.sleep(2) await gps.command(DEFAULT_SENTENCES) - print('Set sentence frequencies to default') - #await gps.command(FULL_COLD_START) - #print('Performed FULL_COLD_START') - print('awaiting first fix') + print("Set sentence frequencies to default") + # await gps.command(FULL_COLD_START) + # print('Performed FULL_COLD_START') + print("awaiting first fix") asyncio.create_task(sat_test(gps)) asyncio.create_task(stats(gps)) asyncio.create_task(navigation(gps)) @@ -152,6 +164,7 @@ async def gps_test(): await gps.data_received(True, True, True, True) # all messages await change_status(gps, uart) + async def shutdown(): # Normally UART is already at BAUDRATE. But if last session didn't restore # factory baudrate we can restore connectivity in the subsequent stuck @@ -159,13 +172,14 @@ async def shutdown(): uart.init(BAUDRATE) await asyncio.sleep(1) await gps.command(FULL_COLD_START) - print('Factory reset') - #print('Restoring default baudrate.') - #await gps.baudrate(9600) + print("Factory reset") + # print('Restoring default baudrate.') + # await gps.baudrate(9600) + try: asyncio.run(gps_test()) except KeyboardInterrupt: - print('Interrupted') + print("Interrupted") finally: asyncio.run(shutdown()) diff --git a/v3/as_drivers/as_GPS/astests.py b/v3/as_drivers/as_GPS/astests.py index 59dba46..c71d70a 100755 --- a/v3/as_drivers/as_GPS/astests.py +++ b/v3/as_drivers/as_GPS/astests.py @@ -11,167 +11,183 @@ # Run under CPython 3.5+ or MicroPython from .as_GPS import * -try: - import uasyncio as asyncio -except ImportError: - import asyncio +import asyncio + async def run(): sentence_count = 0 - test_RMC = ['$GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62\n', - '$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\n', - '$GPRMC,225446,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E*68\n', - '$GPRMC,180041.896,A,3749.1851,N,08338.7891,W,001.9,154.9,240911,,,A*7A\n', - '$GPRMC,180049.896,A,3749.1808,N,08338.7869,W,001.8,156.3,240911,,,A*70\n', - '$GPRMC,092751.000,A,5321.6802,N,00630.3371,W,0.06,31.66,280511,,,A*45\n'] - - test_VTG = ['$GPVTG,232.9,T,,M,002.3,N,004.3,K,A*01\n'] - test_GGA = ['$GPGGA,180050.896,3749.1802,N,08338.7865,W,1,07,1.1,397.4,M,-32.5,M,,0000*6C\n'] - test_GSA = ['$GPGSA,A,3,07,11,28,24,26,08,17,,,,,,2.0,1.1,1.7*37\n', - '$GPGSA,A,3,07,02,26,27,09,04,15,,,,,,1.8,1.0,1.5*33\n'] - test_GSV = ['$GPGSV,3,1,12,28,72,355,39,01,52,063,33,17,51,272,44,08,46,184,38*74\n', - '$GPGSV,3,2,12,24,42,058,33,11,34,053,33,07,20,171,40,20,15,116,*71\n', - '$GPGSV,3,3,12,04,12,204,34,27,11,324,35,32,11,089,,26,10,264,40*7B\n', - '$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74\n', - '$GPGSV,3,2,11,14,25,170,00,16,57,208,39,18,67,296,40,19,40,246,00*74\n', - '$GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D\n', - '$GPGSV,4,1,14,22,81,349,25,14,64,296,22,18,54,114,21,51,40,212,*7D\n', - '$GPGSV,4,2,14,24,30,047,22,04,22,312,26,31,22,204,,12,19,088,23*72\n', - '$GPGSV,4,3,14,25,17,127,18,21,16,175,,11,09,315,16,19,05,273,*72\n', - '$GPGSV,4,4,14,32,05,303,,15,02,073,*7A\n'] - test_GLL = ['$GPGLL,3711.0942,N,08671.4472,W,000812.000,A,A*46\n', - '$GPGLL,4916.45,N,12311.12,W,225444,A,*1D\n', - '$GPGLL,4250.5589,S,14718.5084,E,092204.999,A*2D\n', - '$GPGLL,0000.0000,N,00000.0000,E,235947.000,V*2D\n'] + test_RMC = [ + "$GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62\n", + "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\n", + "$GPRMC,225446,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E*68\n", + "$GPRMC,180041.896,A,3749.1851,N,08338.7891,W,001.9,154.9,240911,,,A*7A\n", + "$GPRMC,180049.896,A,3749.1808,N,08338.7869,W,001.8,156.3,240911,,,A*70\n", + "$GPRMC,092751.000,A,5321.6802,N,00630.3371,W,0.06,31.66,280511,,,A*45\n", + ] + + test_VTG = ["$GPVTG,232.9,T,,M,002.3,N,004.3,K,A*01\n"] + test_GGA = ["$GPGGA,180050.896,3749.1802,N,08338.7865,W,1,07,1.1,397.4,M,-32.5,M,,0000*6C\n"] + test_GSA = [ + "$GPGSA,A,3,07,11,28,24,26,08,17,,,,,,2.0,1.1,1.7*37\n", + "$GPGSA,A,3,07,02,26,27,09,04,15,,,,,,1.8,1.0,1.5*33\n", + ] + test_GSV = [ + "$GPGSV,3,1,12,28,72,355,39,01,52,063,33,17,51,272,44,08,46,184,38*74\n", + "$GPGSV,3,2,12,24,42,058,33,11,34,053,33,07,20,171,40,20,15,116,*71\n", + "$GPGSV,3,3,12,04,12,204,34,27,11,324,35,32,11,089,,26,10,264,40*7B\n", + "$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74\n", + "$GPGSV,3,2,11,14,25,170,00,16,57,208,39,18,67,296,40,19,40,246,00*74\n", + "$GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D\n", + "$GPGSV,4,1,14,22,81,349,25,14,64,296,22,18,54,114,21,51,40,212,*7D\n", + "$GPGSV,4,2,14,24,30,047,22,04,22,312,26,31,22,204,,12,19,088,23*72\n", + "$GPGSV,4,3,14,25,17,127,18,21,16,175,,11,09,315,16,19,05,273,*72\n", + "$GPGSV,4,4,14,32,05,303,,15,02,073,*7A\n", + ] + test_GLL = [ + "$GPGLL,3711.0942,N,08671.4472,W,000812.000,A,A*46\n", + "$GPGLL,4916.45,N,12311.12,W,225444,A,*1D\n", + "$GPGLL,4250.5589,S,14718.5084,E,092204.999,A*2D\n", + "$GPGLL,0000.0000,N,00000.0000,E,235947.000,V*2D\n", + ] my_gps = AS_GPS(None) - sentence = '' + sentence = "" for sentence in test_RMC: my_gps._valid = 0 sentence_count += 1 sentence = await my_gps._update(sentence) if sentence is None: - print('RMC sentence is invalid.') + print("RMC sentence is invalid.") else: - print('Parsed a', sentence, 'Sentence') - print('Longitude:', my_gps.longitude()) - print('Latitude', my_gps.latitude()) - print('UTC Timestamp:', my_gps.utc) - print('Speed:', my_gps.speed()) - print('Date Stamp:', my_gps.date) - print('Course', my_gps.course) - print('Data is Valid:', bool(my_gps._valid & 1)) - print('Compass Direction:', my_gps.compass_direction()) - print('') + print("Parsed a", sentence, "Sentence") + print("Longitude:", my_gps.longitude()) + print("Latitude", my_gps.latitude()) + print("UTC Timestamp:", my_gps.utc) + print("Speed:", my_gps.speed()) + print("Date Stamp:", my_gps.date) + print("Course", my_gps.course) + print("Data is Valid:", bool(my_gps._valid & 1)) + print("Compass Direction:", my_gps.compass_direction()) + print("") for sentence in test_GLL: my_gps._valid = 0 sentence_count += 1 sentence = await my_gps._update(sentence) if sentence is None: - print('GLL sentence is invalid.') + print("GLL sentence is invalid.") else: - print('Parsed a', sentence, 'Sentence') - print('Longitude:', my_gps.longitude()) - print('Latitude', my_gps.latitude()) - print('UTC Timestamp:', my_gps.utc) - print('Data is Valid:', bool(my_gps._valid & 2)) - print('') + print("Parsed a", sentence, "Sentence") + print("Longitude:", my_gps.longitude()) + print("Latitude", my_gps.latitude()) + print("UTC Timestamp:", my_gps.utc) + print("Data is Valid:", bool(my_gps._valid & 2)) + print("") for sentence in test_VTG: my_gps._valid = 0 sentence_count += 1 sentence = await my_gps._update(sentence) if sentence is None: - print('VTG sentence is invalid.') + print("VTG sentence is invalid.") else: - print('Parsed a', sentence, 'Sentence') - print('Speed:', my_gps.speed()) - print('Course', my_gps.course) - print('Compass Direction:', my_gps.compass_direction()) - print('Data is Valid:', bool(my_gps._valid & 4)) - print('') + print("Parsed a", sentence, "Sentence") + print("Speed:", my_gps.speed()) + print("Course", my_gps.course) + print("Compass Direction:", my_gps.compass_direction()) + print("Data is Valid:", bool(my_gps._valid & 4)) + print("") for sentence in test_GGA: my_gps._valid = 0 sentence_count += 1 sentence = await my_gps._update(sentence) if sentence is None: - print('GGA sentence is invalid.') + print("GGA sentence is invalid.") else: - print('Parsed a', sentence, 'Sentence') - print('Longitude', my_gps.longitude()) - print('Latitude', my_gps.latitude()) - print('UTC Timestamp:', my_gps.utc) - print('Altitude:', my_gps.altitude) - print('Height Above Geoid:', my_gps.geoid_height) - print('Horizontal Dilution of Precision:', my_gps.hdop) - print('Satellites in Use by Receiver:', my_gps.satellites_in_use) - print('Data is Valid:', bool(my_gps._valid & 8)) - print('') + print("Parsed a", sentence, "Sentence") + print("Longitude", my_gps.longitude()) + print("Latitude", my_gps.latitude()) + print("UTC Timestamp:", my_gps.utc) + print("Altitude:", my_gps.altitude) + print("Height Above Geoid:", my_gps.geoid_height) + print("Horizontal Dilution of Precision:", my_gps.hdop) + print("Satellites in Use by Receiver:", my_gps.satellites_in_use) + print("Data is Valid:", bool(my_gps._valid & 8)) + print("") for sentence in test_GSA: my_gps._valid = 0 sentence_count += 1 sentence = await my_gps._update(sentence) if sentence is None: - print('GSA sentence is invalid.') + print("GSA sentence is invalid.") else: - print('Parsed a', sentence, 'Sentence') - print('Satellites Used', my_gps.satellites_used) - print('Horizontal Dilution of Precision:', my_gps.hdop) - print('Vertical Dilution of Precision:', my_gps.vdop) - print('Position Dilution of Precision:', my_gps.pdop) - print('Data is Valid:', bool(my_gps._valid & 16)) - print('') + print("Parsed a", sentence, "Sentence") + print("Satellites Used", my_gps.satellites_used) + print("Horizontal Dilution of Precision:", my_gps.hdop) + print("Vertical Dilution of Precision:", my_gps.vdop) + print("Position Dilution of Precision:", my_gps.pdop) + print("Data is Valid:", bool(my_gps._valid & 16)) + print("") for sentence in test_GSV: my_gps._valid = 0 sentence_count += 1 sentence = await my_gps._update(sentence) if sentence is None: - print('GSV sentence is invalid.') + print("GSV sentence is invalid.") else: - print('Parsed a', sentence, 'Sentence') - print('SV Sentences Parsed', my_gps._last_sv_sentence) - print('SV Sentences in Total', my_gps._total_sv_sentences) - print('# of Satellites in View:', my_gps.satellites_in_view) - print('Data is Valid:', bool(my_gps._valid & 32)) - data_valid = my_gps._total_sv_sentences > 0 and my_gps._total_sv_sentences == my_gps._last_sv_sentence - print('Is Satellite Data Valid?:', data_valid) + print("Parsed a", sentence, "Sentence") + print("SV Sentences Parsed", my_gps._last_sv_sentence) + print("SV Sentences in Total", my_gps._total_sv_sentences) + print("# of Satellites in View:", my_gps.satellites_in_view) + print("Data is Valid:", bool(my_gps._valid & 32)) + data_valid = ( + my_gps._total_sv_sentences > 0 + and my_gps._total_sv_sentences == my_gps._last_sv_sentence + ) + print("Is Satellite Data Valid?:", data_valid) if data_valid: - print('Satellite Data:', my_gps._satellite_data) - print('Satellites Visible:', list(my_gps._satellite_data.keys())) - print('') + print("Satellite Data:", my_gps._satellite_data) + print("Satellites Visible:", list(my_gps._satellite_data.keys())) + print("") print("Pretty Print Examples:") - print('Latitude (degs):', my_gps.latitude_string(DD)) - print('Longitude (degs):', my_gps.longitude_string(DD)) - print('Latitude (dms):', my_gps.latitude_string(DMS)) - print('Longitude (dms):', my_gps.longitude_string(DMS)) - print('Latitude (kml):', my_gps.latitude_string(KML)) - print('Longitude (kml):', my_gps.longitude_string(KML)) - print('Latitude (degs, mins):', my_gps.latitude_string()) - print('Longitude (degs, mins):', my_gps.longitude_string()) - print('Speed:', my_gps.speed_string(KPH), 'or', - my_gps.speed_string(MPH), 'or', - my_gps.speed_string(KNOT)) - print('Date (Long Format):', my_gps.date_string(LONG)) - print('Date (Short D/M/Y Format):', my_gps.date_string(DMY)) - print('Date (Short M/D/Y Format):', my_gps.date_string(MDY)) - print('Time:', my_gps.time_string()) + print("Latitude (degs):", my_gps.latitude_string(DD)) + print("Longitude (degs):", my_gps.longitude_string(DD)) + print("Latitude (dms):", my_gps.latitude_string(DMS)) + print("Longitude (dms):", my_gps.longitude_string(DMS)) + print("Latitude (kml):", my_gps.latitude_string(KML)) + print("Longitude (kml):", my_gps.longitude_string(KML)) + print("Latitude (degs, mins):", my_gps.latitude_string()) + print("Longitude (degs, mins):", my_gps.longitude_string()) + print( + "Speed:", + my_gps.speed_string(KPH), + "or", + my_gps.speed_string(MPH), + "or", + my_gps.speed_string(KNOT), + ) + print("Date (Long Format):", my_gps.date_string(LONG)) + print("Date (Short D/M/Y Format):", my_gps.date_string(DMY)) + print("Date (Short M/D/Y Format):", my_gps.date_string(MDY)) + print("Time:", my_gps.time_string()) print() - print('### Final Results ###') - print('Sentences Attempted:', sentence_count) - print('Sentences Found:', my_gps.clean_sentences) - print('Sentences Parsed:', my_gps.parsed_sentences) - print('Unsupported sentences:', my_gps.unsupported_sentences) - print('CRC_Fails:', my_gps.crc_fails) + print("### Final Results ###") + print("Sentences Attempted:", sentence_count) + print("Sentences Found:", my_gps.clean_sentences) + print("Sentences Parsed:", my_gps.parsed_sentences) + print("Unsupported sentences:", my_gps.unsupported_sentences) + print("CRC_Fails:", my_gps.crc_fails) + def run_tests(): asyncio.run(run()) + if __name__ == "__main__": run_tests() diff --git a/v3/as_drivers/as_GPS/astests_pyb.py b/v3/as_drivers/as_GPS/astests_pyb.py index b2e4b12..171714f 100755 --- a/v3/as_drivers/as_GPS/astests_pyb.py +++ b/v3/as_drivers/as_GPS/astests_pyb.py @@ -13,10 +13,12 @@ from .as_GPS import * from machine import UART -import uasyncio as asyncio +import asyncio + def callback(gps, _, arg): - print('Fix callback. Time:', gps.utc, arg) + print("Fix callback. Time:", gps.utc, arg) + async def run_tests(): uart = UART(4, 9600, read_buf_len=200) @@ -24,128 +26,145 @@ async def run_tests(): sreader = asyncio.StreamReader(uart) sentence_count = 0 - test_RMC = ['$GPRMC,180041.896,A,3749.1851,N,08338.7891,W,001.9,154.9,240911,,,A*7A\n', - '$GPRMC,180049.896,A,3749.1808,N,08338.7869,W,001.8,156.3,240911,,,A*70\n', - '$GPRMC,092751.000,A,5321.6802,N,00630.3371,W,0.06,31.66,280511,,,A*45\n'] - - test_VTG = ['$GPVTG,232.9,T,,M,002.3,N,004.3,K,A*01\n'] - test_GGA = ['$GPGGA,180050.896,3749.1802,N,08338.7865,W,1,07,1.1,397.4,M,-32.5,M,,0000*6C\n'] - test_GSA = ['$GPGSA,A,3,07,11,28,24,26,08,17,,,,,,2.0,1.1,1.7*37\n', - '$GPGSA,A,3,07,02,26,27,09,04,15,,,,,,1.8,1.0,1.5*33\n'] - test_GSV = ['$GPGSV,3,1,12,28,72,355,39,01,52,063,33,17,51,272,44,08,46,184,38*74\n', - '$GPGSV,3,2,12,24,42,058,33,11,34,053,33,07,20,171,40,20,15,116,*71\n', - '$GPGSV,3,3,12,04,12,204,34,27,11,324,35,32,11,089,,26,10,264,40*7B\n', - '$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74\n', - '$GPGSV,3,2,11,14,25,170,00,16,57,208,39,18,67,296,40,19,40,246,00*74\n', - '$GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D\n', - '$GPGSV,4,1,14,22,81,349,25,14,64,296,22,18,54,114,21,51,40,212,*7D\n', - '$GPGSV,4,2,14,24,30,047,22,04,22,312,26,31,22,204,,12,19,088,23*72\n', - '$GPGSV,4,3,14,25,17,127,18,21,16,175,,11,09,315,16,19,05,273,*72\n', - '$GPGSV,4,4,14,32,05,303,,15,02,073,*7A\n'] - test_GLL = ['$GPGLL,3711.0942,N,08671.4472,W,000812.000,A,A*46\n', - '$GPGLL,4916.45,N,12311.12,W,225444,A,*1D\n', - '$GPGLL,4250.5589,S,14718.5084,E,092204.999,A*2D\n', - '$GPGLL,4250.5589,S,14718.5084,E,092204.999,A*2D\n',] - -# '$GPGLL,0000.0000,N,00000.0000,E,235947.000,V*2D\n', # Will ignore this one + test_RMC = [ + "$GPRMC,180041.896,A,3749.1851,N,08338.7891,W,001.9,154.9,240911,,,A*7A\n", + "$GPRMC,180049.896,A,3749.1808,N,08338.7869,W,001.8,156.3,240911,,,A*70\n", + "$GPRMC,092751.000,A,5321.6802,N,00630.3371,W,0.06,31.66,280511,,,A*45\n", + ] + + test_VTG = ["$GPVTG,232.9,T,,M,002.3,N,004.3,K,A*01\n"] + test_GGA = ["$GPGGA,180050.896,3749.1802,N,08338.7865,W,1,07,1.1,397.4,M,-32.5,M,,0000*6C\n"] + test_GSA = [ + "$GPGSA,A,3,07,11,28,24,26,08,17,,,,,,2.0,1.1,1.7*37\n", + "$GPGSA,A,3,07,02,26,27,09,04,15,,,,,,1.8,1.0,1.5*33\n", + ] + test_GSV = [ + "$GPGSV,3,1,12,28,72,355,39,01,52,063,33,17,51,272,44,08,46,184,38*74\n", + "$GPGSV,3,2,12,24,42,058,33,11,34,053,33,07,20,171,40,20,15,116,*71\n", + "$GPGSV,3,3,12,04,12,204,34,27,11,324,35,32,11,089,,26,10,264,40*7B\n", + "$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74\n", + "$GPGSV,3,2,11,14,25,170,00,16,57,208,39,18,67,296,40,19,40,246,00*74\n", + "$GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D\n", + "$GPGSV,4,1,14,22,81,349,25,14,64,296,22,18,54,114,21,51,40,212,*7D\n", + "$GPGSV,4,2,14,24,30,047,22,04,22,312,26,31,22,204,,12,19,088,23*72\n", + "$GPGSV,4,3,14,25,17,127,18,21,16,175,,11,09,315,16,19,05,273,*72\n", + "$GPGSV,4,4,14,32,05,303,,15,02,073,*7A\n", + ] + test_GLL = [ + "$GPGLL,3711.0942,N,08671.4472,W,000812.000,A,A*46\n", + "$GPGLL,4916.45,N,12311.12,W,225444,A,*1D\n", + "$GPGLL,4250.5589,S,14718.5084,E,092204.999,A*2D\n", + "$GPGLL,4250.5589,S,14718.5084,E,092204.999,A*2D\n", + ] + + # '$GPGLL,0000.0000,N,00000.0000,E,235947.000,V*2D\n', # Will ignore this one my_gps = AS_GPS(sreader, fix_cb=callback, fix_cb_args=(42,)) - sentence = '' + sentence = "" for sentence in test_RMC: sentence_count += 1 await swriter.awrite(sentence) await my_gps.data_received(date=True) - print('Longitude:', my_gps.longitude()) - print('Latitude', my_gps.latitude()) - print('UTC Time:', my_gps.utc) - print('Speed:', my_gps.speed()) - print('Date Stamp:', my_gps.date) - print('Course', my_gps.course) - print('Data is Valid:', my_gps._valid) - print('Compass Direction:', my_gps.compass_direction()) - print('') + print("Longitude:", my_gps.longitude()) + print("Latitude", my_gps.latitude()) + print("UTC Time:", my_gps.utc) + print("Speed:", my_gps.speed()) + print("Date Stamp:", my_gps.date) + print("Course", my_gps.course) + print("Data is Valid:", my_gps._valid) + print("Compass Direction:", my_gps.compass_direction()) + print("") for sentence in test_GLL: sentence_count += 1 await swriter.awrite(sentence) await my_gps.data_received(position=True) - print('Longitude:', my_gps.longitude()) - print('Latitude', my_gps.latitude()) - print('UTC Time:', my_gps.utc) - print('Data is Valid:', my_gps._valid) - print('') + print("Longitude:", my_gps.longitude()) + print("Latitude", my_gps.latitude()) + print("UTC Time:", my_gps.utc) + print("Data is Valid:", my_gps._valid) + print("") for sentence in test_VTG: - print('Test VTG', sentence) + print("Test VTG", sentence) sentence_count += 1 await swriter.awrite(sentence) await asyncio.sleep_ms(200) # Can't wait for course because of position check - print('Speed:', my_gps.speed()) - print('Course', my_gps.course) - print('Compass Direction:', my_gps.compass_direction()) - print('') + print("Speed:", my_gps.speed()) + print("Course", my_gps.course) + print("Compass Direction:", my_gps.compass_direction()) + print("") for sentence in test_GGA: sentence_count += 1 await swriter.awrite(sentence) await my_gps.data_received(position=True) - print('Longitude', my_gps.longitude()) - print('Latitude', my_gps.latitude()) - print('UTC Time:', my_gps.utc) -# print('Fix Status:', my_gps.fix_stat) - print('Altitude:', my_gps.altitude) - print('Height Above Geoid:', my_gps.geoid_height) - print('Horizontal Dilution of Precision:', my_gps.hdop) - print('Satellites in Use by Receiver:', my_gps.satellites_in_use) - print('') + print("Longitude", my_gps.longitude()) + print("Latitude", my_gps.latitude()) + print("UTC Time:", my_gps.utc) + # print('Fix Status:', my_gps.fix_stat) + print("Altitude:", my_gps.altitude) + print("Height Above Geoid:", my_gps.geoid_height) + print("Horizontal Dilution of Precision:", my_gps.hdop) + print("Satellites in Use by Receiver:", my_gps.satellites_in_use) + print("") for sentence in test_GSA: sentence_count += 1 await swriter.awrite(sentence) await asyncio.sleep_ms(200) - print('Satellites Used', my_gps.satellites_used) - print('Horizontal Dilution of Precision:', my_gps.hdop) - print('Vertical Dilution of Precision:', my_gps.vdop) - print('Position Dilution of Precision:', my_gps.pdop) - print('') + print("Satellites Used", my_gps.satellites_used) + print("Horizontal Dilution of Precision:", my_gps.hdop) + print("Vertical Dilution of Precision:", my_gps.vdop) + print("Position Dilution of Precision:", my_gps.pdop) + print("") for sentence in test_GSV: sentence_count += 1 await swriter.awrite(sentence) await asyncio.sleep_ms(200) - print('SV Sentences Parsed', my_gps._last_sv_sentence) - print('SV Sentences in Total', my_gps._total_sv_sentences) - print('# of Satellites in View:', my_gps.satellites_in_view) - data_valid = my_gps._total_sv_sentences > 0 and my_gps._total_sv_sentences == my_gps._last_sv_sentence - print('Is Satellite Data Valid?:', data_valid) + print("SV Sentences Parsed", my_gps._last_sv_sentence) + print("SV Sentences in Total", my_gps._total_sv_sentences) + print("# of Satellites in View:", my_gps.satellites_in_view) + data_valid = ( + my_gps._total_sv_sentences > 0 + and my_gps._total_sv_sentences == my_gps._last_sv_sentence + ) + print("Is Satellite Data Valid?:", data_valid) if data_valid: - print('Satellite Data:', my_gps._satellite_data) - print('Satellites Visible:', list(my_gps._satellite_data.keys())) - print('') + print("Satellite Data:", my_gps._satellite_data) + print("Satellites Visible:", list(my_gps._satellite_data.keys())) + print("") print("Pretty Print Examples:") - print('Latitude (degs):', my_gps.latitude_string(DD)) - print('Longitude (degs):', my_gps.longitude_string(DD)) - print('Latitude (dms):', my_gps.latitude_string(DMS)) - print('Longitude (dms):', my_gps.longitude_string(DMS)) - print('Latitude (kml):', my_gps.latitude_string(KML)) - print('Longitude (kml):', my_gps.longitude_string(KML)) - print('Latitude (degs, mins):', my_gps.latitude_string()) - print('Longitude (degs, mins):', my_gps.longitude_string()) - print('Speed:', my_gps.speed_string(KPH), 'or', - my_gps.speed_string(MPH), 'or', - my_gps.speed_string(KNOT)) - print('Date (Long Format):', my_gps.date_string(LONG)) - print('Date (Short D/M/Y Format):', my_gps.date_string(DMY)) - print('Date (Short M/D/Y Format):', my_gps.date_string(MDY)) - print('Time:', my_gps.time_string()) + print("Latitude (degs):", my_gps.latitude_string(DD)) + print("Longitude (degs):", my_gps.longitude_string(DD)) + print("Latitude (dms):", my_gps.latitude_string(DMS)) + print("Longitude (dms):", my_gps.longitude_string(DMS)) + print("Latitude (kml):", my_gps.latitude_string(KML)) + print("Longitude (kml):", my_gps.longitude_string(KML)) + print("Latitude (degs, mins):", my_gps.latitude_string()) + print("Longitude (degs, mins):", my_gps.longitude_string()) + print( + "Speed:", + my_gps.speed_string(KPH), + "or", + my_gps.speed_string(MPH), + "or", + my_gps.speed_string(KNOT), + ) + print("Date (Long Format):", my_gps.date_string(LONG)) + print("Date (Short D/M/Y Format):", my_gps.date_string(DMY)) + print("Date (Short M/D/Y Format):", my_gps.date_string(MDY)) + print("Time:", my_gps.time_string()) print() - print('### Final Results ###') - print('Sentences Attempted:', sentence_count) - print('Sentences Found:', my_gps.clean_sentences) - print('Sentences Parsed:', my_gps.parsed_sentences) - print('Unsupported sentences:', my_gps.unsupported_sentences) - print('CRC_Fails:', my_gps.crc_fails) + print("### Final Results ###") + print("Sentences Attempted:", sentence_count) + print("Sentences Found:", my_gps.clean_sentences) + print("Sentences Parsed:", my_gps.parsed_sentences) + print("Unsupported sentences:", my_gps.unsupported_sentences) + print("CRC_Fails:", my_gps.crc_fails) + asyncio.run(run_tests()) diff --git a/v3/as_drivers/as_GPS/baud.py b/v3/as_drivers/as_GPS/baud.py index 6832f6d..29852c3 100644 --- a/v3/as_drivers/as_GPS/baud.py +++ b/v3/as_drivers/as_GPS/baud.py @@ -1,8 +1,9 @@ # baud.py Test uasyncio at high baudrate import pyb -import uasyncio as asyncio +import asyncio import utime import as_drivers.as_rwGPS as as_rwGPS + # Outcome # Sleep Buffer # 0 None OK, length limit 74 @@ -21,8 +22,9 @@ BAUDRATE = 115200 red, green, yellow, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) + async def setup(): - print('Initialising') + print("Initialising") uart = pyb.UART(4, 9600) sreader = asyncio.StreamReader(uart) swriter = asyncio.StreamWriter(uart, {}) @@ -31,12 +33,14 @@ async def setup(): await gps.baudrate(BAUDRATE) uart.init(BAUDRATE) + def setbaud(): asyncio.run(setup()) - print('Baudrate set to 115200.') + print("Baudrate set to 115200.") + async def gps_test(): - print('Initialising') + print("Initialising") uart = pyb.UART(4, BAUDRATE, read_buf_len=400) sreader = asyncio.StreamReader(uart) swriter = asyncio.StreamWriter(uart, {}) @@ -51,5 +55,6 @@ async def gps_test(): red.toggle() utime.sleep_ms(10) + def test(): asyncio.run(gps_test()) diff --git a/v3/as_drivers/as_GPS/log_kml.py b/v3/as_drivers/as_GPS/log_kml.py index 22279bf..e16b1b8 100644 --- a/v3/as_drivers/as_GPS/log_kml.py +++ b/v3/as_drivers/as_GPS/log_kml.py @@ -9,10 +9,10 @@ # Logging stops and the file is closed when the user switch is pressed. from .as_GPS import KML, AS_GPS -import uasyncio as asyncio +import asyncio import pyb -str_start = ''' +str_start = """