From 61c24c34fde960f09d4d266de2c5ff85288d3207 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 5 Apr 2018 18:39:42 +0100 Subject: [PATCH 001/472] auart_hd.py added. Tutorial amended to suit. --- README.md | 3 +- TUTORIAL.md | 27 +++++++++++++ auart_hd.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 auart_hd.py diff --git a/README.md b/README.md index 337798a..755641f 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ This GitHub repository consists of the following parts: * [A tutorial](./TUTORIAL.md) An introductory tutorial on asynchronous - programming and the use of the uasyncio library is offered. This is a work in - progress, not least because uasyncio is not yet complete. + programming and the use of the uasyncio library is offered. * [Asynchronous device drivers](./DRIVERS.md). A module providing drivers for devices such as switches and pushbuttons. * [Synchronisation primitives](./PRIMITIVES.md). Provides commonly used diff --git a/TUTORIAL.md b/TUTORIAL.md index 7b6416f..0677bf8 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -123,6 +123,8 @@ and rebuilding. 5.1 [The IORead mechnaism](./TUTORIAL.md#51-the-ioread-mechanism) + 5.1.1 [A UART driver example](./TUTORIAL.md#511-a-uart-driver-example) + 5.2 [Using a coro to poll hardware](./TUTORIAL.md#52-using-a-coro-to-poll-hardware) 5.3 [Using IORead to poll hardware](./TUTORIAL.md#53-using-ioread-to-poll-hardware) @@ -211,6 +213,9 @@ results by accessing Pyboard hardware. 8. `aqtest.py` Demo of uasyncio `Queue` class. 9. `aremote.py` Example device driver for NEC protocol IR remote control. 10. `auart.py` Demo of streaming I/O via a Pyboard UART. + 11. `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. **Test Programs** @@ -1033,6 +1038,28 @@ The mechanism works because the device driver (written in C) implements the following methods: `ioctl`, `read`, `write`, `readline` and `close`. See section 5.3 for further discussion. +### 5.1.1 A UART driver example + +The program `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) ## 5.2 Using a coro to poll hardware diff --git a/auart_hd.py b/auart_hd.py new file mode 100644 index 0000000..da2b33e --- /dev/null +++ b/auart_hd.py @@ -0,0 +1,106 @@ +# 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. +# >>> From c2aba5b5938039e97050a1c24d01bf1a98b87f82 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 6 Apr 2018 10:00:21 +0100 Subject: [PATCH 002/472] asyntest.py condition_test workround for Loboris port bug. --- asyntest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/asyntest.py b/asyntest.py index 070c5d8..3d98bcb 100644 --- a/asyntest.py +++ b/asyntest.py @@ -303,7 +303,7 @@ async def cond03(): # Maintain a count of seconds tim += 1 def predicate(): - return tim >= 12 + return tim >= 8 # 12 async def cond04(n, barrier): with await cond: @@ -344,9 +344,9 @@ def condition_test(): cond02 2 triggered. tim = 5 cond02 0 triggered. tim = 7 cond04 99 Awaiting notification and predicate. -cond04 99 triggered. tim = 13 +cond04 99 triggered. tim = 9 Done. -''', 16) +''', 13) loop = asyncio.get_event_loop() loop.run_until_complete(cond_go(loop)) From 89c120e5f68cd311e530903df34298960b166513 Mon Sep 17 00:00:00 2001 From: peterhinch Date: Sat, 5 May 2018 11:50:36 +0100 Subject: [PATCH 003/472] Add async GPS driver. --- TUTORIAL.md | 40 +++ gps/LICENSE | 21 ++ gps/README.md | 482 ++++++++++++++++++++++++++++++ gps/as_GPS.py | 621 ++++++++++++++++++++++++++++++++++++++ gps/as_pyGPS.py | 731 +++++++++++++++++++++++++++++++++++++++++++++ gps/as_rwGPS.py | 117 ++++++++ gps/ast_pb.py | 100 +++++++ gps/ast_pbrw.py | 169 +++++++++++ gps/astests.py | 180 +++++++++++ gps/astests_pyb.py | 153 ++++++++++ gps/log.kml | 128 ++++++++ gps/log_kml.py | 76 +++++ 12 files changed, 2818 insertions(+) create mode 100644 gps/LICENSE create mode 100644 gps/README.md create mode 100644 gps/as_GPS.py create mode 100644 gps/as_pyGPS.py create mode 100644 gps/as_rwGPS.py create mode 100644 gps/ast_pb.py create mode 100644 gps/ast_pbrw.py create mode 100755 gps/astests.py create mode 100755 gps/astests_pyb.py create mode 100644 gps/log.kml create mode 100644 gps/log_kml.py diff --git a/TUTORIAL.md b/TUTORIAL.md index 0677bf8..b40e29a 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -963,6 +963,46 @@ of `uasyncio` by forcing the coro to run, and possibly terminate, when it is still queued for execution. I haven't entirely thought through the implications of this, but it's a thoroughly bad idea. +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 Device driver examples diff --git a/gps/LICENSE b/gps/LICENSE new file mode 100644 index 0000000..798b35f --- /dev/null +++ b/gps/LICENSE @@ -0,0 +1,21 @@ +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/gps/README.md b/gps/README.md new file mode 100644 index 0000000..ae53e54 --- /dev/null +++ b/gps/README.md @@ -0,0 +1,482 @@ +# 1. as_GPS + +This is an asynchronous device driver for GPS devices which communicate with +the driver via a UART. GPS NMEA sentence parsing is based on this excellent +library [micropyGPS]. + +The driver is designed to be extended by subclassing, for example to support +additional sentence types. It is compatible with Python 3.5 or later and also +with [MicroPython]. 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 +sentences on startup. An optional read-write driver is provided for +MTK3329/MTK3339 chips as used on the above board. This enables the device +configuration to be altered. + +## 1.1 Overview + +The `AS_GPS` object runs a coroutine which receives GPS NMEA 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.2 Basic Usage + +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 and therefore assumes that power has been applied +long enough for the GPS to have started to acquire data. + +```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 +my_gps = as_GPS.AS_GPS(sreader, fix_cb=callback) # Instantiate GPS + +async def test(): + await asyncio.sleep(60) # Run for one minute +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. + * `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. + * `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`. + +Additional files relevant to the read/write driver are listed +[here](./README.md#31-files). + +## 1.4 Installation + +### 1.4.1 Micropython + +To install on "bare metal" hardware such as the Pyboard copy the file +`as_GPS.py` onto the device's filesystem and ensure that `uasyncio` is +installed. The code has been tested on the Pyboard with `uasyncio` V2 and the +Adafruit [Ultimate GPS Breakout] module. If memory errors are encountered on +resource constrained devices install 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. + +### 1.4.2 Python 3.5 or later + +On platforms with an underlying OS such as the Raspberry Pi ensure that +`as_GPS.py` (and optionally `as_rwGPS.py`) is 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. Whether this matters and any action to take 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 3.2](./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). + * `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` does not affect the date value. +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=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' + +### 2.2.3 Time and date + + * `time_since_fix` No args. Returns time in milliseconds since last valid fix. + + * `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'. + + * `time` No args. Returns the current time in form 'hh:mm:ss.sss'. + +## 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 valid messages of the + specified types have been received. For example: + +```python +while True: + await my_gps.data_received(position=True, altitude=True) + # can now access these data values with confidence +``` + +No check is provided for satellite data as this is checked 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 + +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 and 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 with 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 5). + +### 2.4.3 Date and time + +As received from most recent GPS message. + + * `timestamp` [hrs, mins, secs] e.g. [12, 15, 3.23] + * `date` [day, month, year] e.g. [23, 3, 18] + * `local_offset` Local time offset in hrs as specified to constructor. + +### 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','','']` + +# 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 limited 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 + +## 3.1 Files + + * `as_rwGPS.py` Supports the `GPS` class. This subclass of `AS_GPS` enables + writing a limited subset of the MTK commands used on many popular devices. + +## 3.2 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, except that element 0 has +the '$' symbol removed. The last element is the last informational string - the +checksum has been verified and is not in the list. + +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. + * `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 + +The 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. + +## 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. + * `antenna` Initially 0. Values: + 0 No report received. + 1 Antenna fault. + 2 Internal antenna. + 3 External antenna. + +## 3.5 The parse method + +The default `parse` method is redefined. 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 + +Other `PMTK` messages are passed to the optional message callback as described +[in section 3.2](./README.md#32-constructor). + +# 4. Supported Sentences + + * GPRMC GP indicates NMEA sentence + * 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 + +# 5. 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`. + +An example of this may be found in the `as_rwGPS.py` module. + +A subclass may redefine this to attempt to parse such sentences. The method +receives an arg `segs` being a list of strings. These are the parts of the +sentence which were delimited by commas. `segs[0]` is the sentence type with +the leading '$' character removed. + +It should return `True` if the sentence was successfully parsed, otherwise +`False`. + +[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 \ No newline at end of file diff --git a/gps/as_GPS.py b/gps/as_GPS.py new file mode 100644 index 0000000..384e9f4 --- /dev/null +++ b/gps/as_GPS.py @@ -0,0 +1,621 @@ +# 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 +# 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 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +try: + from micropython import const +except ImportError: + const = lambda x : x + +# 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): + _SENTENCE_LIMIT = 76 # Max sentence length (based on GGA sentence) + _NO_FIX = 1 + _DIRECTIONS = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', + 'WNW', 'NW', 'NNW') + _MONTHS = ('January', 'February', 'March', 'April', 'May', + 'June', 'July', 'August', 'September', 'October', + 'November', 'December') + + # 8-bit xor of characters between "$" and "*" + @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 + + # Import utime or time for fix time handling + try: + import utime + self._get_time = utime.ticks_ms + self._time_diff = utime.ticks_diff + 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) + + # Key: currently supported NMEA sentences. Value: parse method. + self.supported_sentences = {'GPRMC': self._gprmc, 'GLRMC': self._gprmc, + 'GPGGA': self._gpgga, 'GLGGA': self._gpgga, + 'GPVTG': self._gpvtg, 'GLVTG': self._gpvtg, + 'GPGSA': self._gpgsa, 'GLGSA': self._gpgsa, + 'GPGSV': self._gpgsv, 'GLGSV': self._gpgsv, + 'GPGLL': self._gpgll, 'GLGLL': self._gpgll, + 'GNGGA': self._gpgga, 'GNRMC': self._gprmc, + 'GNVTG': self._gpvtg, + } + + ##################### + # 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 + self.timestamp = [0, 0, 0] # [h, m, s] + self.date = [0, 0, 0] # [d, m, y] + self.local_offset = local_offset # hrs + + # 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() + + # 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()) + + ########################################## + # Data Stream Handler Functions + ########################################## + + async def _run(self): + while True: + res = await self._sreader.readline() + try: + res = res.decode('utf8') + except UnicodeError: # Garbage: can happen e.g. on baudrate change + continue + self._update(res) + + # Update takes a line of text + def _update(self, line): + line = line.rstrip() + try: + next(c for c in line if ord(c) < 10 or ord(c) > 126) + return None # Bad character received + except StopIteration: + pass # All good + + if len(line) > self._SENTENCE_LIMIT or not '*' in line: + return None # Too long or malformed + + a = line.split(',') + segs = a[:-1] + a[-1].split('*') + if not self._crc_check(line, segs[-1]): + self.crc_fails += 1 # Update statistics + return None + + self.clean_sentences += 1 # Sentence is good but unparsed. + segs[0] = segs[0][1:] # discard $ + segs = segs[:-1] # and checksum + if segs[0] in self.supported_sentences: + s_type = self.supported_sentences[segs[0]](segs) + 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 segs[0] # For test programs + else: + if self.parse(segs): # Subclass hook + self.parsed_sentences += 1 + self.unsupported_sentences += 1 + return segs[0] # 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 + ######################################## + + def _fix(self, gps_segments, idx_lat, idx_long): + try: + # 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] + except ValueError: + return False + + if lat_hemi not in 'NS'or lon_hemi not in 'EW': + return False + 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() + return True + + # Set timestamp. If time/date not present retain last reading (if any). + def _set_timestamp(self, utc_string): + if utc_string: # Possible timestamp found + self.timestamp[0] = int(utc_string[0:2]) + self.local_offset # h + self.timestamp[1] = int(utc_string[2:4]) # mins + self.timestamp[2] = float(utc_string[4:]) # secs + + ######################################## + # Sentence Parsers + ######################################## + +# For all parsers: +# Return value: True if sentence was correctly parsed. This includes cases where +# data from receiver is correctly formed but reports an invalid state. +# The ._received dict entry is initially set False and is set only if the data +# was successfully updated. Valid because parsers are synchronous methods. + + def _gprmc(self, gps_segments): # Parse RMC sentence + self._valid &= ~RMC + # UTC Timestamp. + try: + self._set_timestamp(gps_segments[1]) + except ValueError: # Bad Timestamp value present + return False + + # Date stamp + try: + date_string = gps_segments[9] + if date_string: # Possible date stamp found + self.date[0] = int(date_string[0:2]) # day + self.date[1] = int(date_string[2:4]) # month + self.date[2] = int(date_string[4:6]) # year 18 == 2018 + + except ValueError: # Bad Date stamp value present + return False + + # Check Receiver Data Valid Flag + if gps_segments[2] != 'A': + return True # Correctly parsed + + # Data from Receiver is Valid/Has Fix. Longitude / Latitude + if not self._fix(gps_segments, 3, 5): + return False + + # Speed + try: + spd_knt = float(gps_segments[7]) + except ValueError: + return False + + # Course + try: + course = float(gps_segments[8]) + except ValueError: + return False + + # Add Magnetic Variation if firmware supplies it + if gps_segments[10]: + try: + mv = float(gps_segments[10]) + except ValueError: + return False + if gps_segments[11] not in ('EW'): + return False + 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 + # UTC Timestamp. + try: + self._set_timestamp(gps_segments[5]) + except ValueError: # Bad Timestamp value present + return False + + # Check Receiver Data Valid Flag + if gps_segments[6] != 'A': # Invalid. Don't update data + return True # Correctly parsed + + # Data from Receiver is Valid/Has Fix. Longitude / Latitude + if not self._fix(gps_segments, 1, 3): + return False + + # 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 + try: + course = float(gps_segments[1]) + spd_knt = float(gps_segments[5]) + except ValueError: + return False + + self._speed = spd_knt + self.course = course + self._valid |= VTG + return VTG + + def _gpgga(self, gps_segments): # Parse GGA sentence + self._valid &= ~GGA + try: + # UTC Timestamp + self._set_timestamp(gps_segments[1]) + + # 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]) + + except ValueError: + return False + + # Process Location and Altitude if Fix is GOOD + if fix_stat: + # Longitude / Latitude + if not self._fix(gps_segments, 2, 4): + return False + + # Altitude / Height Above Geoid + try: + altitude = float(gps_segments[9]) + geoid_height = float(gps_segments[11]) + except ValueError: + return False + + # 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) + try: + fix_type = int(gps_segments[2]) + except ValueError: + return False + # 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: + try: + sat_number = int(sat_number_str) + sats_used.append(sat_number) + except ValueError: + return False + else: + break + # PDOP,HDOP,VDOP + try: + pdop = float(gps_segments[15]) + hdop = float(gps_segments[16]) + vdop = float(gps_segments[17]) + except ValueError: + return False + + # If Fix is GOOD, update fix timestamp + if fix_type <= self._NO_FIX: # Deviation from Michael McCoy's logic. Is this right? + return False + 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 + try: + num_sv_sentences = int(gps_segments[1]) + current_sv_sentence = int(gps_segments[2]) + sats_in_view = int(gps_segments[3]) + except ValueError: + return False + + # 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 (ValueError,IndexError): + return False + + 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_ms(200) + while self._total_sv_sentences > self._last_sv_sentence: + await asyncio.sleep_ms(100) + 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. + # Calculate the offset for a rotated compass + if self.course >= 348.75: + offset_course = 360 - self.course + else: + offset_course = self.course + 11.25 + # Each compass point is separated by 22.5°, divide to find lookup value + return self._DIRECTIONS[int(offset_course // 22.5)] + + 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') + + def time(self): + return '{:02d}:{:02d}:{:02.3f}'.format(*self.timestamp) + + def date_string(self, formatting=MDY): + day, month, year = self.date + # Long Format January 1st, 2014 + if formatting == LONG: + dform = '{:s} {:2d}{:s}, 20{:2d}' + # Retrieve Month string from private set + month = self._MONTHS[month - 1] + # Determine Date Suffix + if day in (1, 21, 31): + suffix = 'st' + elif day in (2, 22): + suffix = 'nd' + elif day == 3: + 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/gps/as_pyGPS.py b/gps/as_pyGPS.py new file mode 100644 index 0000000..8c57dab --- /dev/null +++ b/gps/as_pyGPS.py @@ -0,0 +1,731 @@ +""" +# MicropyGPS - a GPS NMEA sentence parser for Micropython/Python 3.X +# Copyright (c) 2017 Michael Calvin McCoy (calvin.mccoy@gmail.com) +# The MIT License (MIT) - see LICENSE file +""" +# Modified for uasyncio operation Peter Hinch April 2018 +# Portability: +# Replaced pyb with machine +# If machine not available assumed to be running under CPython (Raspberry Pi) +# time module assumed to return a float + +# TODO: +# Time Since First Fix +# Distance/Time to Target +# More Helper Functions +# Dynamically limit sentences types to parse + +from math import floor, modf +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +# Import utime or time for fix time handling +try: + # Assume running on MicroPython + import utime +except ImportError: + # Otherwise default to time module for non-embedded implementations + # Should still support millisecond resolution. + import time + + +class MicropyGPS(object): + """GPS NMEA Sentence Parser. Creates object that stores all relevant GPS data and statistics. + Parses sentences by complete line using update(). """ + + _SENTENCE_LIMIT = 76 # Max sentence length (based on GGA sentence) + _HEMISPHERES = ('N', 'S', 'E', 'W') + _NO_FIX = 1 + _FIX_2D = 2 + _FIX_3D = 3 + _DIRECTIONS = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', + 'WNW', 'NW', 'NNW') + _MONTHS = ('January', 'February', 'March', 'April', 'May', + 'June', 'July', 'August', 'September', 'October', + 'November', 'December') + + def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, fix_cb_args=()): + """ + Setup GPS Object Status Flags, Internal Data Registers, etc + local_offset (int): Timzone Difference to UTC + location_formatting (str): Style For Presenting Longitude/Latitude: + Decimal Degree Minute (ddm) - 40° 26.767′ N + Degrees Minutes Seconds (dms) - 40° 26′ 46″ N + Decimal Degrees (dd) - 40.446° N + """ + + self.sreader = sreader # None: in testing update is called with simulated data + self.fix_cb = fix_cb + self.fix_cb_args = fix_cb_args + # All the currently supported NMEA sentences + self.supported_sentences = {'GPRMC': self.gprmc, 'GLRMC': self.gprmc, + 'GPGGA': self.gpgga, 'GLGGA': self.gpgga, + 'GPVTG': self.gpvtg, 'GLVTG': self.gpvtg, + 'GPGSA': self.gpgsa, 'GLGSA': self.gpgsa, + 'GPGSV': self.gpgsv, 'GLGSV': self.gpgsv, + 'GPGLL': self.gpgll, 'GLGLL': self.gpgll, + 'GNGGA': self.gpgga, 'GNRMC': self.gprmc, + 'GNVTG': self.gpvtg, + } + + ##################### + # Object Status Flags + self.fix_time = None + + ##################### + # Sentence Statistics + self.crc_fails = 0 + self.clean_sentences = 0 + self.parsed_sentences = 0 + + ##################### + # Data From Sentences + # Time + self.timestamp = (0, 0, 0) + self.date = (0, 0, 0) + self.local_offset = local_offset + + # Position/Motion + self._latitude = (0, 0.0, 'N') + self._longitude = (0, 0.0, 'W') + self.speed = (0.0, 0.0, 0.0) + self.course = 0.0 + self.altitude = 0.0 + self.geoid_height = 0.0 + + # GPS Info + self.satellites_in_view = 0 + self.satellites_in_use = 0 + self.satellites_used = [] + self.last_sv_sentence = 0 + self.total_sv_sentences = 0 + self.satellite_data = dict() + self.hdop = 0.0 + self.pdop = 0.0 + self.vdop = 0.0 + self.valid = False + self.fix_stat = 0 + self.fix_type = 1 + if sreader is not None: # Running with UART data + loop = asyncio.get_event_loop() + loop.create_task(self.run()) + + ########################################## + # Data Stream Handler Functions + ########################################## + + + async def run(self): + while True: + res = await self.sreader.readline() +# print(res) + self.update(res.decode('utf8')) + + # 8-bit xor of characters between "$" and "*" + def crc_check(self, 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 + + # Update takes a line of text + def update(self, line): + line = line.rstrip() + try: + next(c for c in line if ord(c) < 10 or ord(c) > 126) + return None # Bad character received + except StopIteration: + pass + + if len(line) > self._SENTENCE_LIMIT: + return None # Too long + + if not '*' in line: + return None + + a = line.split(',') + segs = a[:-1] + a[-1].split('*') + if not self.crc_check(line, segs[-1]): + self.crc_fails += 1 + return None + + self.clean_sentences += 1 + segs[0] = segs[0][1:] # discard $ + if segs[0] in self.supported_sentences: + if self.supported_sentences[segs[0]](segs): + self.parsed_sentences += 1 + return segs[0] + + def new_fix_time(self): + """Updates a high resolution counter with current time when fix is updated. Currently only triggered from + GGA, GSA and RMC sentences""" + try: + self.fix_time = utime.ticks_ms() + except NameError: + self.fix_time = time.time() + self.fix_cb(self, *self.fix_cb_args) # Run the callback + + ######################################## + # Coordinates Translation Functions + ######################################## + + def latitude(self, coord_format=None): + """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': + minute_parts = modf(self._latitude[1]) + seconds = round(minute_parts[0] * 60) + return [self._latitude[0], int(minute_parts[1]), seconds, self._latitude[2]] + else: + return self._latitude + + def longitude(self, coord_format=None): + """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': + minute_parts = modf(self._longitude[1]) + seconds = round(minute_parts[0] * 60) + return [self._longitude[0], int(minute_parts[1]), seconds, self._longitude[2]] + else: + return self._longitude + + + ######################################## + # Sentence Parsers + ######################################## + def gprmc(self, gps_segments): + """Parse Recommended Minimum Specific GPS/Transit data (RMC)Sentence. + Updates UTC timestamp, latitude, longitude, Course, Speed, Date, and fix status + """ + + # UTC Timestamp. If time/date not present retain last reading (if any). + try: + utc_string = gps_segments[1] + + if utc_string: # Possible timestamp found + hours = int(utc_string[0:2]) + self.local_offset + minutes = int(utc_string[2:4]) + seconds = float(utc_string[4:]) + self.timestamp = (hours, minutes, seconds) + + except ValueError: # Bad Timestamp value present + return False + + # Date stamp + try: + date_string = gps_segments[9] + + # Date string printer function assumes to be year >=2000, + # date_string() must be supplied with the correct century argument to display correctly + if date_string: # Possible date stamp found + day = int(date_string[0:2]) + month = int(date_string[2:4]) + year = int(date_string[4:6]) + self.date = (day, month, year) + + except ValueError: # Bad Date stamp value present + return False + + # Check Receiver Data Valid Flag + if gps_segments[2] == 'A': # Data from Receiver is Valid/Has Fix + + # Longitude / Latitude + try: + # Latitude + l_string = gps_segments[3] + lat_degs = int(l_string[0:2]) + lat_mins = float(l_string[2:]) + lat_hemi = gps_segments[4] + + # Longitude + l_string = gps_segments[5] + lon_degs = int(l_string[0:3]) + lon_mins = float(l_string[3:]) + lon_hemi = gps_segments[6] + except ValueError: + return False + + if lat_hemi not in self._HEMISPHERES: + return False + + if lon_hemi not in self._HEMISPHERES: + return False + + # Speed + try: + spd_knt = float(gps_segments[7]) + except ValueError: + return False + + # Course + try: + course = float(gps_segments[8]) + except ValueError: + return False + + # TODO - Add Magnetic Variation + + # Update Object Data + self._latitude = (lat_degs, lat_mins, lat_hemi) + self._longitude = (lon_degs, lon_mins, lon_hemi) + # Include mph and hm/h + self.speed = (spd_knt, spd_knt * 1.151, spd_knt * 1.852) + self.course = course + self.valid = True + + # Update Last Fix Time + self.new_fix_time() + + else: # Leave data unchanged if Sentence is 'Invalid' + self.valid = False + + return True + + def gpgll(self, gps_segments): + """Parse Geographic Latitude and Longitude (GLL)Sentence. Updates UTC timestamp, latitude, + longitude, and fix status""" + + # UTC Timestamp. If time/date not present retain last reading (if any). + try: + utc_string = gps_segments[5] + + if utc_string: # Possible timestamp found + hours = int(utc_string[0:2]) + self.local_offset + minutes = int(utc_string[2:4]) + seconds = float(utc_string[4:]) + self.timestamp = (hours, minutes, seconds) + + except ValueError: # Bad Timestamp value present + return False + + # Check Receiver Data Valid Flag + if gps_segments[6] == 'A': # Data from Receiver is Valid/Has Fix + + # Longitude / Latitude + try: + # Latitude + l_string = gps_segments[1] + lat_degs = int(l_string[0:2]) + lat_mins = float(l_string[2:]) + lat_hemi = gps_segments[2] + + # Longitude + l_string = gps_segments[3] + lon_degs = int(l_string[0:3]) + lon_mins = float(l_string[3:]) + lon_hemi = gps_segments[4] + except ValueError: + return False + + if lat_hemi not in self._HEMISPHERES: + return False + + if lon_hemi not in self._HEMISPHERES: + return False + + # Update Object Data + self._latitude = (lat_degs, lat_mins, lat_hemi) + self._longitude = (lon_degs, lon_mins, lon_hemi) + self.valid = True + + # Update Last Fix Time + self.new_fix_time() + + else: # Leave data unchanged if Sentence is 'Invalid' + self.valid = False + + return True + + def gpvtg(self, gps_segments): + """Parse Track Made Good and Ground Speed (VTG) Sentence. Updates speed and course""" + try: + course = float(gps_segments[1]) + spd_knt = float(gps_segments[5]) + except ValueError: + return False + + # Include mph and km/h + self.speed = (spd_knt, spd_knt * 1.151, spd_knt * 1.852) + self.course = course + return True + + def gpgga(self, gps_segments): + """Parse Global Positioning System Fix Data (GGA) Sentence. Updates UTC timestamp, latitude, longitude, + fix status, satellites in use, Horizontal Dilution of Precision (HDOP), altitude, geoid height and fix status""" + + try: + # UTC Timestamp + utc_string = gps_segments[1] + + # Skip timestamp if receiver doesn't have one yet + if utc_string: + hms = (int(utc_string[0:2]) + self.local_offset, + int(utc_string[2:4]), + float(utc_string[4:])) + else: + hms = None + + # 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]) + + except ValueError: + return False + + # Process Location and Speed Data if Fix is GOOD + if fix_stat: + + # Longitude / Latitude + try: + # Latitude + l_string = gps_segments[2] + lat_degs = int(l_string[0:2]) + lat_mins = float(l_string[2:]) + lat_hemi = gps_segments[3] + + # Longitude + l_string = gps_segments[4] + lon_degs = int(l_string[0:3]) + lon_mins = float(l_string[3:]) + lon_hemi = gps_segments[5] + except ValueError: + return False + + if lat_hemi not in self._HEMISPHERES: + return False + + if lon_hemi not in self._HEMISPHERES: + return False + + # Altitude / Height Above Geoid + try: + altitude = float(gps_segments[9]) + geoid_height = float(gps_segments[11]) + except ValueError: + return False + + # Update Object Data + self._latitude = (lat_degs, lat_mins, lat_hemi) + self._longitude = (lon_degs, lon_mins, lon_hemi) + self.altitude = altitude + self.geoid_height = geoid_height + + # Update Object Data + if hms is not None: + self.timestamp = hms + self.satellites_in_use = satellites_in_use + self.hdop = hdop + self.fix_stat = fix_stat + + # If Fix is GOOD, update fix timestamp + if fix_stat: + self.new_fix_time() + + return True + + def gpgsa(self, gps_segments): + """Parse GNSS DOP and Active Satellites (GSA) sentence. Updates GPS fix type, list of satellites used in + fix calculation, Position Dilution of Precision (PDOP), Horizontal Dilution of Precision (HDOP), Vertical + Dilution of Precision, and fix status""" + + # Fix Type (None,2D or 3D) + try: + fix_type = int(gps_segments[2]) + except ValueError: + return False + + # 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: + try: + sat_number = int(sat_number_str) + sats_used.append(sat_number) + except ValueError: + return False + else: + break + + # PDOP,HDOP,VDOP + try: + pdop = float(gps_segments[15]) + hdop = float(gps_segments[16]) + vdop = float(gps_segments[17]) + except ValueError: + return False + + # Update Object Data + self.fix_type = fix_type + + # If Fix is GOOD, update fix timestamp + if fix_type > self._NO_FIX: + self.new_fix_time() + + self.satellites_used = sats_used + self.hdop = hdop + self.vdop = vdop + self.pdop = pdop + + return True + + def gpgsv(self, gps_segments): + """Parse Satellites in View (GSV) sentence. Updates number of SV Sentences,the number of the last SV sentence + parsed, and data on each satellite present in the sentence""" + try: + num_sv_sentences = int(gps_segments[1]) + current_sv_sentence = int(gps_segments[2]) + sats_in_view = int(gps_segments[3]) + except ValueError: + return False + + # 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 (ValueError,IndexError): + return False + + 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) + + return True + + ######################################### + # User Helper Functions + # These functions make working with the GPS object data easier + ######################################### + + def satellite_data_updated(self): + """ + Checks if the all the GSV sentences in a group have been read, making satellite data complete + :return: boolean + """ + if self.total_sv_sentences > 0 and self.total_sv_sentences == self.last_sv_sentence: + return True + else: + return False + + def satellites_visible(self): + """ + Returns a list of of the satellite PRNs currently visible to the receiver + :return: list + """ + return list(self.satellite_data.keys()) + + def time_since_fix(self): + """Returns number of millisecond since the last sentence with a valid fix was parsed. + Returns -1 if no fix has been found""" + + # Test if a Fix has been found + if self.fix_time is None: + return -1 + + # Try calculating fix time using utime; default to seconds if not running MicroPython + try: + current = utime.ticks_diff(utime.ticks_ms(), self.fix_time) + except NameError: + current = (time.time() - self.fix_time) * 1000 # ms + + return current + + def compass_direction(self): + """ + Determine a cardinal or inter-cardinal direction based on current course. + :return: string + """ + # Calculate the offset for a rotated compass + if self.course >= 348.75: + offset_course = 360 - self.course + else: + offset_course = self.course + 11.25 + + # Each compass point is separated by 22.5 degrees, divide to find lookup value + dir_index = floor(offset_course / 22.5) + + final_dir = self._DIRECTIONS[dir_index] + + return final_dir + + def latitude_string(self, coord_format=None): + """ + Create a readable string of the current latitude data + :return: string + """ + if coord_format == 'dd': + form_lat = self.latitude(coord_format) + lat_string = str(form_lat[0]) + '° ' + str(self._latitude[2]) + elif coord_format == 'dms': + form_lat = self.latitude(coord_format) + lat_string = str(form_lat[0]) + '° ' + str(form_lat[1]) + "' " + str(form_lat[2]) + '" ' + str(form_lat[3]) + elif coord_format == 'kml': + form_lat = self.latitude('dd') + lat_string = str(form_lat[0] if self._latitude[2] == 'N' else -form_lat[0]) + else: + lat_string = str(self._latitude[0]) + '° ' + str(self._latitude[1]) + "' " + str(self._latitude[2]) + return lat_string + + def longitude_string(self, coord_format=None): + """ + Create a readable string of the current longitude data + :return: string + """ + if coord_format == 'dd': + form_long = self.longitude(coord_format) + lon_string = str(form_long[0]) + '° ' + str(self._longitude[2]) + elif coord_format == 'dms': + form_long = self.longitude(coord_format) + lon_string = str(form_long[0]) + '° ' + str(form_long[1]) + "' " + str(form_long[2]) + '" ' + str(form_long[3]) + elif coord_format == 'kml': + form_long = self.longitude('dd') + lon = form_long[0] if self._longitude[2] == 'E' else -form_long[0] + lon_string = str(lon) + else: + lon_string = str(self._longitude[0]) + '° ' + str(self._longitude[1]) + "' " + str(self._longitude[2]) + return lon_string + + def speed_string(self, unit='kph'): + """ + Creates a readable string of the current speed data in one of three units + :param unit: string of 'kph','mph, or 'knot' + :return: + """ + if unit == 'mph': + speed_string = str(self.speed[1]) + ' mph' + + elif unit == 'knot': + if self.speed[0] == 1: + unit_str = ' knot' + else: + unit_str = ' knots' + speed_string = str(self.speed[0]) + unit_str + + else: + speed_string = str(self.speed[2]) + ' km/h' + + return speed_string + + def date_string(self, formatting='s_mdy', century='20'): + """ + Creates a readable string of the current date. + Can select between long format: Januray 1st, 2014 + or two short formats: + 11/01/2014 (MM/DD/YYYY) + 01/11/2014 (DD/MM/YYYY) + :param formatting: string 's_mdy', 's_dmy', or 'long' + :param century: int delineating the century the GPS data is from (19 for 19XX, 20 for 20XX) + :return: date_string string with long or short format date + """ + + # Long Format Januray 1st, 2014 + if formatting == 'long': + # Retrieve Month string from private set + month = self._MONTHS[self.date[1] - 1] + + # Determine Date Suffix + if self.date[0] in (1, 21, 31): + suffix = 'st' + elif self.date[0] in (2, 22): + suffix = 'nd' + elif self.date[0] == 3: + suffix = 'rd' + else: + suffix = 'th' + + day = str(self.date[0]) + suffix # Create Day String + + year = century + str(self.date[2]) # Create Year String + + date_string = month + ' ' + day + ', ' + year # Put it all together + + else: + # Add leading zeros to day string if necessary + if self.date[0] < 10: + day = '0' + str(self.date[0]) + else: + day = str(self.date[0]) + + # Add leading zeros to month string if necessary + if self.date[1] < 10: + month = '0' + str(self.date[1]) + else: + month = str(self.date[1]) + + # Add leading zeros to year string if necessary + if self.date[2] < 10: + year = '0' + str(self.date[2]) + else: + year = str(self.date[2]) + + # Build final string based on desired formatting + if formatting == 's_dmy': + date_string = day + '/' + month + '/' + year + + else: # Default date format + date_string = month + '/' + day + '/' + year + + return date_string + + +if __name__ == "__main__": + pass diff --git a/gps/as_rwGPS.py b/gps/as_rwGPS.py new file mode 100644 index 0000000..70cf86d --- /dev/null +++ b/gps/as_rwGPS.py @@ -0,0 +1,117 @@ +# 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) + + 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/gps/ast_pb.py b/gps/ast_pb.py new file mode 100644 index 0000000..1de4e7c --- /dev/null +++ b/gps/ast_pb.py @@ -0,0 +1,100 @@ +# 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_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 Timestamp:', gps.timestamp) + 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, 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/gps/ast_pbrw.py b/gps/ast_pbrw.py new file mode 100644 index 0000000..873dca9 --- /dev/null +++ b/gps/ast_pbrw.py @@ -0,0 +1,169 @@ +# 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_pyGPS + +# 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 + +BAUDRATE = 19200 +red, green, yellow, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) +ntimeouts = 0 + +def callback(gps, _, timer): + red.toggle() + green.on() + timer.trigger(10000) # Outage is declared after 10s + +def 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) + blue.toggle() + print('***** DATE AND TIME *****') + print('Data is Valid:', hex(gps._valid)) + print('UTC Timestamp:', gps.timestamp) + 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(timeout) + sentence_count = 0 + gps = as_rwGPS.GPS(sreader, swriter, 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(): + # If power was lost in last session can retrieve connectivity in the subsequent + # stuck session with ctrl-c + uart.init(BAUDRATE) + await asyncio.sleep(1) + 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/gps/astests.py b/gps/astests.py new file mode 100755 index 0000000..6606810 --- /dev/null +++ b/gps/astests.py @@ -0,0 +1,180 @@ +#!/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.x or MicroPython + +import as_GPS + +def run_tests(): + 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 = 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.timestamp) + 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 = 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.timestamp) + print('Data is Valid:', bool(my_gps._valid & 2)) + print('') + + for sentence in test_VTG: + my_gps._valid = 0 + sentence_count += 1 + sentence = 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 = 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.timestamp) + 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 = 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 = 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()) + 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) + +import unittest + +class TestMicroPyGPS(unittest.TestCase): + + def test_smoke(self): + try: + run_tests() + except: + self.fail("smoke test raised exception") + +if __name__ == "__main__": + run_tests() diff --git a/gps/astests_pyb.py b/gps/astests_pyb.py new file mode 100755 index 0000000..8891935 --- /dev/null +++ b/gps/astests_pyb.py @@ -0,0 +1,153 @@ +# 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.timestamp, 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,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,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(position=True) + print('Longitude:', my_gps.longitude()) + print('Latitude', my_gps.latitude()) + print('UTC Timestamp:', my_gps.timestamp) + 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 Timestamp:', my_gps.timestamp) + 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 Timestamp:', my_gps.timestamp) +# 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()) + 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/gps/log.kml b/gps/log.kml new file mode 100644 index 0000000..31d1076 --- /dev/null +++ b/gps/log.kml @@ -0,0 +1,128 @@ + + + + +#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/gps/log_kml.py b/gps/log_kml.py new file mode 100644 index 0000000..8fed4c6 --- /dev/null +++ b/gps/log_kml.py @@ -0,0 +1,76 @@ +# log_kml.py Log GPS data to a kml file for display on Google Earth + +# Copyright (c) Peter Hinch 2018 +# 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/ + +# 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, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) +sw = pyb.Switch() + +# Toggle the red LED +def toggle_led(gps): + 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') + blue.toggle() + 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()) From a1d7a7a597351f11c74950da5f594a1a42db8084 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 9 May 2018 13:12:47 +0100 Subject: [PATCH 004/472] Improve docs, add reference to GPS. --- HD44780/README.md | 38 ++++++++++++++++++++++++++++++++++++++ README.md | 5 +++++ 2 files changed, 43 insertions(+) create mode 100644 HD44780/README.md diff --git a/HD44780/README.md b/HD44780/README.md new file mode 100644 index 0000000..7419005 --- /dev/null +++ b/HD44780/README.md @@ -0,0 +1,38 @@ +# 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. + +# Files + + * `alcd.py` Driver, includes connection details. + * `alcdtest.py` Test/demo script. + +Currently most of the documentation, including wiring details, is in the code. + +# 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 exampls 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). This ensures previous contents are overwritten. + +```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 +>>> +``` + +Similar use of the `format` method can be used to achieve more complex +tabulated data layouts. diff --git a/README.md b/README.md index 755641f..377c37b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ This GitHub repository consists of the following parts: * [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. * [A modified uasyncio](./FASTPOLL.md) This incorporates a simple priority mechanism. With suitable application design this improves the rate at which devices can be polled and improves the accuracy of time delays. Also provides From d7cbefbaec3bd66e32a5ef948e68c9fa4635572b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 9 May 2018 13:24:38 +0100 Subject: [PATCH 005/472] GPS: timestamp seconds value is int. --- gps/README.md | 4 ++-- gps/as_GPS.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gps/README.md b/gps/README.md index ae53e54..dd5e120 100644 --- a/gps/README.md +++ b/gps/README.md @@ -189,7 +189,7 @@ gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG) `as_GPS.DMY` returns 'DD/MM/YY'. `as_GPS.LONG` returns a string of form 'January 1st, 2014'. - * `time` No args. Returns the current time in form 'hh:mm:ss.sss'. + * `time` No args. Returns the current time in form 'hh:mm:ss'. ## 2.3 Public coroutines @@ -266,7 +266,7 @@ The following are counts since instantiation. As received from most recent GPS message. - * `timestamp` [hrs, mins, secs] e.g. [12, 15, 3.23] + * `timestamp` [hrs, mins, secs] e.g. [12, 15, 3]. Values are integers. * `date` [day, month, year] e.g. [23, 3, 18] * `local_offset` Local time offset in hrs as specified to constructor. diff --git a/gps/as_GPS.py b/gps/as_GPS.py index 384e9f4..4a707e8 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -233,7 +233,7 @@ def _set_timestamp(self, utc_string): if utc_string: # Possible timestamp found self.timestamp[0] = int(utc_string[0:2]) + self.local_offset # h self.timestamp[1] = int(utc_string[2:4]) # mins - self.timestamp[2] = float(utc_string[4:]) # secs + self.timestamp[2] = int(utc_string[4:]) # secs ######################################## # Sentence Parsers @@ -593,7 +593,7 @@ def speed_string(self, unit=KPH): return sform.format(speed, 'km/h') def time(self): - return '{:02d}:{:02d}:{:02.3f}'.format(*self.timestamp) + return '{:02d}:{:02d}:{:02d}'.format(*self.timestamp) def date_string(self, formatting=MDY): day, month, year = self.date From 1e7954ea7cffe561140ed630e4351bfaaac32619 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 13 May 2018 08:59:54 +0100 Subject: [PATCH 006/472] HD44780 improve docs, move code comments to README.md. --- HD44780/README.md | 63 ++++++++++++++++++++++++++++++++++++++++++----- HD44780/alcd.py | 23 +++-------------- 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/HD44780/README.md b/HD44780/README.md index 7419005..43baa1e 100644 --- a/HD44780/README.md +++ b/HD44780/README.md @@ -1,23 +1,74 @@ -# Driver for character-based LCD displays +# 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. -# Files +# 2. Files * `alcd.py` Driver, includes connection details. * `alcdtest.py` Test/demo script. -Currently most of the documentation, including wiring details, is in the code. +# 3. Typical wiring -# Display Formatting +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. +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 exampls this function formats a string such that it is left-padded with +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). This ensures previous contents are overwritten. @@ -34,5 +85,5 @@ def print_at(st, col, width=16): >>> ``` -Similar use of the `format` method can be used to achieve more complex +This use of the `format` method may be extended to achieve more complex tabulated data layouts. diff --git a/HD44780/alcd.py b/HD44780/alcd.py index 74b4368..bce80e7 100644 --- a/HD44780/alcd.py +++ b/HD44780/alcd.py @@ -13,31 +13,16 @@ # # Author : Matt Hawkins # Site : http://www.raspberrypi-spy.co.uk -# -# Date : 26/07/2012 from machine import Pin import utime as time import uasyncio as asyncio -# **************************************************** LCD DRIVER *************************************************** +# ********************************** GLOBAL CONSTANTS: TARGET BOARD PIN NUMBERS ************************************* -""" -Pin correspondence of default pinlist. This is supplied as an example -Name LCD connector Board -Rs 4 1 red Y1 -E 6 2 Y2 -D7 14 3 Y3 -D6 13 4 Y4 -D5 12 5 Y5 -D4 11 6 Y6 -""" +# Supply board pin numbers as a tuple in order Rs, E, D4, D5, D6, D7 -# *********************************** GLOBAL CONSTANTS: MICROPYTHON PIN NUMBERS ************************************* - -# Supply as board pin numbers as a tuple Rs, E, D4, D5, D6, D7 - -PINLIST = ('Y1','Y2','Y6','Y5','Y4','Y3') +PINLIST = ('Y1','Y2','Y6','Y5','Y4','Y3') # As used in testing. # **************************************************** LCD CLASS **************************************************** # Initstring: @@ -121,5 +106,3 @@ async def runlcd(self): # Periodically check for changed tex await asyncio.sleep_ms(0) # Reshedule ASAP self.dirty[row] = False await asyncio.sleep_ms(20) # Give other coros a look-in - - From bf9caacba8b8ec45cd1d2e76f0d915dbf024d860 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 13 May 2018 09:11:02 +0100 Subject: [PATCH 007/472] HD44780 improve docs, move code comments to README.md. --- HD44780/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/HD44780/README.md b/HD44780/README.md index 43baa1e..97d3ff9 100644 --- a/HD44780/README.md +++ b/HD44780/README.md @@ -35,8 +35,9 @@ This takes the following positional args: ## 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. -This is illustrated by the test program: +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 @@ -70,7 +71,8 @@ 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). This ensures previous contents are overwritten. +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): From 8b6727063113bb470d659adf32792d8cd35a813e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 13 May 2018 12:42:32 +0100 Subject: [PATCH 008/472] gps/README.md Add timing notes and wiring. --- HD44780/README.md | 2 ++ gps/README.md | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/HD44780/README.md b/HD44780/README.md index 97d3ff9..70ad222 100644 --- a/HD44780/README.md +++ b/HD44780/README.md @@ -3,6 +3,8 @@ 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. diff --git a/gps/README.md b/gps/README.md index dd5e120..077a797 100644 --- a/gps/README.md +++ b/gps/README.md @@ -14,6 +14,8 @@ sentences on startup. An optional read-write driver is provided for MTK3329/MTK3339 chips as used on the above board. This enables the device configuration to be altered. +###### [Main README](../README.md) + ## 1.1 Overview The `AS_GPS` object runs a coroutine which receives GPS NMEA sentences from the @@ -21,6 +23,22 @@ 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 it may be wired as follows: + +| GPS | Pyboard | +|:---:|:----------:| +| Vin | V+ or 3V3 | +| Gnd | Gnd | +| Tx | X2 (U4 rx) | +| Rx | X1 (U4 tx) | + +This is based on UART 4 as used in the test programs; any UART may be used. The +X1-Rx connection is only necessary if using the read/write driver to alter the +GPS device operation. + ## 1.2 Basic Usage In the example below a UART is instantiated and an `AS_GPS` instance created. @@ -315,6 +333,15 @@ packets to GPS modules based on the MTK3329/MTK3339 chip. These include: * `as_rwGPS.py` Supports the `GPS` class. This subclass of `AS_GPS` enables writing a limited subset of the MTK commands used on many popular devices. + * `ast_pbrw.py` Test script which changes various attributes. This will pause + until a fix has been achieved. After that changes are made for about 1 minute, + then 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.2 Constructor @@ -352,7 +379,8 @@ The args presented to the fix callback are as described in * `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. + 10000. If the rate is to be increased see + [notes on timing](./README.md#6-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. @@ -410,6 +438,8 @@ 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#6-notes-on-timing). + ## 3.4 Public bound variables These are updated when a response to a command is received. The time taken for @@ -472,6 +502,19 @@ the leading '$' character removed. It should return `True` if the sentence was successfully parsed, otherwise `False`. +# 6. Notes on timing + +At the default baudrate of 9600 I measured a time of 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 must +be increased or the satellite information messages should be disabled. + +The PPS signal (not used by this driver) on the MTK3339 occurs only when a fix +has been achieved. The leading edge always occurs before a set of messages are +output. So, if the leading edge is to be used for precise timing, 1s should be +added to the `timestamp` value (beware of possible rollover into minutes and +hours). + [MicroPython]:https://micropython.org/ [frozen module]:https://learn.adafruit.com/micropython-basics-loading-modules/frozen-modules [NMEA-0183]:http://aprs.gids.nl/nmea/ From db919004171a355b08c1e708349661900ed77958 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 15 May 2018 10:18:27 +0100 Subject: [PATCH 009/472] Fix _set_timestamp return value bug. --- gps/as_GPS.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/gps/as_GPS.py b/gps/as_GPS.py index 4a707e8..3cdf363 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -231,9 +231,14 @@ def _fix(self, gps_segments, idx_lat, idx_long): # Set timestamp. If time/date not present retain last reading (if any). def _set_timestamp(self, utc_string): if utc_string: # Possible timestamp found - self.timestamp[0] = int(utc_string[0:2]) + self.local_offset # h - self.timestamp[1] = int(utc_string[2:4]) # mins - self.timestamp[2] = int(utc_string[4:]) # secs + try: + self.timestamp[0] = int(utc_string[0:2]) + self.local_offset # h + self.timestamp[1] = int(utc_string[2:4]) # mins + self.timestamp[2] = int(utc_string[4:]) # secs + return True + except ValueError: + pass + return False ######################################## # Sentence Parsers From 43db85519a4f4c5e9ad36b2ebc589e524a236847 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 15 May 2018 11:14:20 +0100 Subject: [PATCH 010/472] Interim fix for _set_timestamp seconds bug. --- gps/as_GPS.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gps/as_GPS.py b/gps/as_GPS.py index 3cdf363..a0eb665 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -234,7 +234,9 @@ def _set_timestamp(self, utc_string): try: self.timestamp[0] = int(utc_string[0:2]) + self.local_offset # h self.timestamp[1] = int(utc_string[2:4]) # mins - self.timestamp[2] = int(utc_string[4:]) # secs + # secs TODO spec states 2 chars but getting decimal: perhaps this + # should be float after all. + self.timestamp[2] = int(utc_string[4:6]) # secs return True except ValueError: pass From 786aab528fca3ca0e96fa2902ee148da632b80cc Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 15 May 2018 11:21:04 +0100 Subject: [PATCH 011/472] Add as_GPS_time.py. Not fully tested yet. --- gps/as_GPS_time.py | 179 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 gps/as_GPS_time.py diff --git a/gps/as_GPS_time.py b/gps/as_GPS_time.py new file mode 100644 index 0000000..03f1b49 --- /dev/null +++ b/gps/as_GPS_time.py @@ -0,0 +1,179 @@ +# as_GPS_time.py Using GPS for precision timing and for calibrating Pyboard RTC +# This is STM-specific: requires pyb module. + +# Current state: getcal seems to work but needs further testing (odd values ocasionally) +# Other API functions need testing + +# Copyright (c) 2018 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +import math +import pyb +import utime +import micropython +import as_GPS + +micropython.alloc_emergency_exception_buf(100) + +red, green, yellow, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) +rtc = pyb.RTC() + +# Convenience function. Return RTC seconds since midnight as float +def rtc_secs(): + dt = rtc.datetime() + return 3600*dt[4] + 60*dt[5] + dt[6] + (255 - dt[7])/256 + +# 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. +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 + +class GPS_Timer(): + def __init__(self, gps, pps_pin): + self.gps = gps + self.secs = None # Integer time since midnight at last PPS + self.acquired = None # Value of ticks_us at edge of PPS + loop = asyncio.get_event_loop() + loop.create_task(self._start(pps_pin)) + + async def _start(self, pps_pin): + await self.gps.data_received(date=True) + pyb.ExtInt(pps_pin, pyb.ExtInt.IRQ_RISING, pyb.Pin.PULL_NONE, self._isr) + print('ISR set up', self.gps.date, self.gps.timestamp) + + def _isr(self, _): + t = self.gps.timestamp + secs = 3600*t[0] + 60*t[1] + t[2] # Time in last NMEA sentence + # Could be an outage here, so PPS arrives many secs after last sentence + # Is this right? Does PPS continue during outage? + self.secs = secs + 1 # PPS preceeds NMEA so add 1 sec + self.acquired = utime.ticks_us() + blue.toggle() # TEST + + # Return accurate GPS time in seconds (float) since midnight + def get_secs(self): + print(self.gps.timestamp) + t = self.secs + if t != self.secs: # An interrupt has occurred + t = self.secs # IRQ's are at 1Hz so this must be OK + return t + utime.ticks_diff(utime.ticks_us(), self.acquired) / 1000000 + + # Return accurate GPS time of day (hrs , mins , secs) + def get_t_split(self): + t = math.modf(self.get_secs()) + m, s = divmod(int(t[1]), 60) + h = int(m // 60) + return h, m, s + t[0] + + # Return a time/date tuple suitable for setting RTC + def _get_td_split(self): + d, m, y = self.gps.date + t = math.modf(self.get_secs()) + m, s = divmod(int(t[1]), 60) + h = int(m // 60) + ss = int(255*(1 - t[0])) + return y, m, d, week_day(y, m, d), h, m, s, ss + + def set_rtc(self): + rtc.datetime(self._get_td_split()) + + # Time from GPS: integer μs since Y2K. Call after awaiting PPS: result is + # time when PPS leading edge occurred + def _get_gps_usecs(self): + d, m, y = self.gps.date + t = math.modf(self.get_secs()) + mins, secs = divmod(int(t[1]), 60) + hrs = int(mins // 60) + print(y, m, d, t, hrs, mins, secs) + tim = utime.mktime((2000 + y, m, d, hrs, mins, secs, week_day(y, m, d) - 1, 0)) + return tim * 1000000 + + # Return no. of μs RTC leads GPS. Done by comparing times at the instant of + # PPS leading edge. + def delta(self): + rtc_time = self._await_pps() # μs since Y2K at time of PPS + gps_time = self._get_gps_usecs() # μs since Y2K at 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. + def _await_pps(self): + t0 = self.acquired + while self.acquired == t0: # Busy-wait on PPS interrupt + pass + st = rtc.datetime()[7] + while rtc.datetime()[7] == st: # Wait for RTC to change + pass + dt = utime.ticks_diff(utime.ticks_us(), self.acquired) + return 1000000 * utime.time() + ((1000000 * (255 - rtc.datetime()[7])) >> 8) - dt + + # 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 + print('Calculate', gps_start, gps_end, rtc_start, rtc_end) + 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 + # Note this blocks for upto 1 sec at intervals + async def getcal(self, minutes=5): + if minutes < 1: + raise ValueError('Minutes must be >= 1') + rtc.calibration(0) # Clear existing cal + # Wait for PPS, then RTC 1/256 second change + # return RTC time in μs since Y2K at instant of PPS + rtc_start = self._await_pps() + # GPS start time in μs since Y2K: co at time of PPS edge + gps_start = self._get_gps_usecs() + for n in range(minutes): + for _ in range(6): + await asyncio.sleep(10) # TEST 60 + # Get RTC time at instant of PPS + rtc_end = self._await_pps() + gps_end = self._get_gps_usecs() + cal = self._calculate(gps_start, gps_end, rtc_start, rtc_end) + print('Mins {:d} cal factor {:d}'.format(n + 1, cal)) + return cal + + async def calibrate(self, minutes=5): + print('Waiting for startup') + while self.acquired is None: + await asyncio.sleep(1) # Wait for startup + print('Waiting {} 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)) + +# Test script. Red LED toggles on fix, Blue on PPS interrupt. +async def run_test(minutes): + uart = pyb.UART(4, 9600, read_buf_len=200) + sreader = asyncio.StreamReader(uart) + gps = as_GPS.AS_GPS(sreader, fix_cb=lambda *_: red.toggle()) + pps_pin = pyb.Pin('X3', pyb.Pin.IN) + gps_tim = GPS_Timer(gps, pps_pin) + await gps_tim.calibrate(minutes) + +def test(minutes=5): + loop = asyncio.get_event_loop() + loop.run_until_complete(run_test(minutes)) From 80c1860591136ce74616e27372736ccfe6338eee Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 16 May 2018 17:05:04 +0100 Subject: [PATCH 012/472] as_GPS_time.py getcal has RT issue to be fixed. --- gps/README.md | 79 +++++++++++++++++++++++++------ gps/as_GPS.py | 12 ++--- gps/as_GPS_time.py | 115 +++++++++++++++++++++++++++++++-------------- 3 files changed, 151 insertions(+), 55 deletions(-) diff --git a/gps/README.md b/gps/README.md index 077a797..327bef3 100644 --- a/gps/README.md +++ b/gps/README.md @@ -28,16 +28,18 @@ 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 it may be wired as follows: -| GPS | Pyboard | -|:---:|:----------:| -| Vin | V+ or 3V3 | -| Gnd | Gnd | -| Tx | X2 (U4 rx) | -| Rx | X1 (U4 tx) | +| GPS | Pyboard | Optional | +|:---:|:----------:|:--------:| +| Vin | V+ or 3V3 | | +| Gnd | Gnd | | +| PPS | X3 | Y | +| Tx | X2 (U4 rx) | Y | +| Rx | X1 (U4 tx) | | This is based on UART 4 as used in the test programs; any UART may be used. The X1-Rx connection is only necessary if using the read/write driver to alter the -GPS device operation. +GPS device operation. The PPS connection is required only if using the device +for precise timing (`as_GPS_time.py`). Any pin may be used. ## 1.2 Basic Usage @@ -284,7 +286,8 @@ The following are counts since instantiation. As received from most recent GPS message. - * `timestamp` [hrs, mins, secs] e.g. [12, 15, 3]. Values are integers. + * `timestamp` [hrs, mins, secs] e.g. [12, 15, 3.0]. Values are integers except + for secs which is a float (perhaps dependent on GPS hardware). * `date` [day, month, year] e.g. [23, 3, 18] * `local_offset` Local time offset in hrs as specified to constructor. @@ -343,7 +346,7 @@ packets to GPS modules based on the MTK3329/MTK3339 chip. These include: * Yellow: Toggles each 4s if navigation updates are being received. * Blue: Toggles each 4s if time updates are being received. -## 3.2 Constructor +## 3.2 GPS class Constructor This takes two mandatory positional args: * `sreader` This is a `StreamReader` instance associated with the UART. @@ -380,7 +383,7 @@ The args presented to the fix callback are as described in 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#6-notes-on-timing). + [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. @@ -438,7 +441,7 @@ 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#6-notes-on-timing). +See also [notes on timing](./README.md#7-notes-on-timing). ## 3.4 Public bound variables @@ -467,7 +470,55 @@ with Other `PMTK` messages are passed to the optional message callback as described [in section 3.2](./README.md#32-constructor). -# 4. Supported Sentences +# 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 time reference. It may +be used to set and to calibrate the Pyboard realtime clock (RTC). + +## 4.1 Files + + * `as_GPS_time.py` Supports the `GPS_Timer` class. + +## 4.2 GPS_Timer class Constructor + +This takes the following arguments: + * `gps` An instance of the `AS_GPS` (read-only) or `GPS` (read/write) classes. + * `pps_pin` An initialised input `Pin` instance for the PPS signal. + +## 4.3 Public methods + +With the exception of `delta` these return immediately. Times are derived from +the GPS PPS signal. These functions should not be called until a valid +time/date message and PPS signal have occurred: await the `ready` coroutine +prior to first use. These functions do not check this for themselves to ensure +fast return. + + * `get_secs` No args. Returns a float: the period past midnight in seconds. + * `get_t_split` No args. Returns time of day tuple of form + (hrs: int, mins: int, secs: float). + * `set_rtc` No args. Sets the Pyboard RTC to GPS time. + * `delta` No args. Returns no. of μs RTC leads GPS. This method blocks for up + to a second. + +## 4.4 Public coroutines + + * `ready` No args. Pauses until a valid time/date message and PPS signal have + occurred. + * `calibrate` Arg: integer, no. of minutes to run default 5. Calibrates the + Pyboard RTC and returns the calibration factor for it. + +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 and re-apply it on startup. + +The coroutine calculates the calibration factor at 10 second intervals and will +return early if three consecutive identical calibration factors are calculated. +Note that, because of the need for precise timing, this coroutine blocks at +intervals for periods of up to one second. + +# 5. Supported Sentences * GPRMC GP indicates NMEA sentence * GLRMC GL indicates GLONASS (Russian system) @@ -485,7 +536,7 @@ Other `PMTK` messages are passed to the optional message callback as described * GPGSV * GLGSV -# 5. Subclassing +# 6. 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, @@ -502,7 +553,7 @@ the leading '$' character removed. It should return `True` if the sentence was successfully parsed, otherwise `False`. -# 6. Notes on timing +# 7. Notes on timing At the default baudrate of 9600 I measured a time of 400ms when a set of GPSV messages came in. This time could be longer depending on data. So if an update diff --git a/gps/as_GPS.py b/gps/as_GPS.py index a0eb665..e6cabc4 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -1,6 +1,7 @@ # 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 @@ -109,8 +110,9 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC ##################### # Data From Sentences - # Time - self.timestamp = [0, 0, 0] # [h, m, s] + # Time. Ignore http://www.gpsinformation.org/dale/nmea.htm, hardware + # returns a float. + self.timestamp = [0, 0, 0.0] # [h, m, s] self.date = [0, 0, 0] # [d, m, y] self.local_offset = local_offset # hrs @@ -234,9 +236,7 @@ def _set_timestamp(self, utc_string): try: self.timestamp[0] = int(utc_string[0:2]) + self.local_offset # h self.timestamp[1] = int(utc_string[2:4]) # mins - # secs TODO spec states 2 chars but getting decimal: perhaps this - # should be float after all. - self.timestamp[2] = int(utc_string[4:6]) # secs + self.timestamp[2] = float(utc_string[4:]) # secs from chip is a float return True except ValueError: pass @@ -600,7 +600,7 @@ def speed_string(self, unit=KPH): return sform.format(speed, 'km/h') def time(self): - return '{:02d}:{:02d}:{:02d}'.format(*self.timestamp) + return '{:02d}:{:02d}:{:2.3f}'.format(*self.timestamp) def date_string(self, formatting=MDY): day, month, year = self.date diff --git a/gps/as_GPS_time.py b/gps/as_GPS_time.py index 03f1b49..bf78467 100644 --- a/gps/as_GPS_time.py +++ b/gps/as_GPS_time.py @@ -1,7 +1,7 @@ # as_GPS_time.py Using GPS for precision timing and for calibrating Pyboard RTC # This is STM-specific: requires pyb module. -# Current state: getcal seems to work but needs further testing (odd values ocasionally) +# Current state: getcal works but has a HACK due to a realtime issue I haven't yet diagnosed. # Other API functions need testing # Copyright (c) 2018 Peter Hinch @@ -53,11 +53,12 @@ def __init__(self, gps, pps_pin): async def _start(self, pps_pin): await self.gps.data_received(date=True) pyb.ExtInt(pps_pin, pyb.ExtInt.IRQ_RISING, pyb.Pin.PULL_NONE, self._isr) - print('ISR set up', self.gps.date, self.gps.timestamp) def _isr(self, _): + # Time in last NMEA sentence t = self.gps.timestamp - secs = 3600*t[0] + 60*t[1] + t[2] # Time in last NMEA sentence + # secs is rounded down to an int: exact time of last PPS + secs = 3600*t[0] + 60*t[1] + int(t[2]) # Could be an outage here, so PPS arrives many secs after last sentence # Is this right? Does PPS continue during outage? self.secs = secs + 1 # PPS preceeds NMEA so add 1 sec @@ -66,42 +67,50 @@ def _isr(self, _): # Return accurate GPS time in seconds (float) since midnight def get_secs(self): - print(self.gps.timestamp) t = self.secs if t != self.secs: # An interrupt has occurred - t = self.secs # IRQ's are at 1Hz so this must be OK + # Re-read to ensure .acquired is correct. No need to get clever + t = self.secs # here as IRQ's are at only 1Hz return t + utime.ticks_diff(utime.ticks_us(), self.acquired) / 1000000 + # Return GPS time as hrs: int, mins: int, secs: int, fractional_secs: float + def _get_hms(self): + t = math.modf(self.get_secs()) + x, secs = divmod(int(t[1]), 60) + hrs, mins = divmod(x, 60) + return hrs, mins, secs, t[0] + # Return accurate GPS time of day (hrs , mins , secs) def get_t_split(self): - t = math.modf(self.get_secs()) - m, s = divmod(int(t[1]), 60) - h = int(m // 60) - return h, m, s + t[0] + hrs, mins, secs, frac_secs = self._get_hms() + return hrs, mins, secs + frac_secs # Return a time/date tuple suitable for setting RTC def _get_td_split(self): d, m, y = self.gps.date - t = math.modf(self.get_secs()) - m, s = divmod(int(t[1]), 60) - h = int(m // 60) - ss = int(255*(1 - t[0])) - return y, m, d, week_day(y, m, d), h, m, s, ss + y += 2000 + hrs, mins, secs, frac_secs = self._get_hms() + ss = int(255*(1 - frac_secs)) + return y, m, d, week_day(y, m, d), hrs, mins, secs, ss def set_rtc(self): rtc.datetime(self._get_td_split()) # Time from GPS: integer μs since Y2K. Call after awaiting PPS: result is - # time when PPS leading edge occurred + # time when PPS leading edge occurred so fractional secs discarded. def _get_gps_usecs(self): d, m, y = self.gps.date - t = math.modf(self.get_secs()) - mins, secs = divmod(int(t[1]), 60) - hrs = int(mins // 60) - print(y, m, d, t, hrs, mins, secs) - tim = utime.mktime((2000 + y, m, d, hrs, mins, secs, week_day(y, m, d) - 1, 0)) + y += 2000 + hrs, mins, secs, _ = self._get_hms() + tim = utime.mktime((y, m, d, hrs, mins, secs, week_day(y, m, d) - 1, 0)) return tim * 1000000 + # Value of RTC time at current instant. Units μs since Y2K. Tests OK. + 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. def delta(self): @@ -120,12 +129,13 @@ def _await_pps(self): while rtc.datetime()[7] == st: # Wait for RTC to change pass dt = utime.ticks_diff(utime.ticks_us(), self.acquired) - return 1000000 * utime.time() + ((1000000 * (255 - rtc.datetime()[7])) >> 8) - dt + t = self._get_rtc_usecs() # Read RTC now + assert abs(dt) < 10000 + return t - dt # 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 - print('Calculate', gps_start, gps_end, rtc_start, rtc_end) pps_delta = (gps_end - gps_start) # Duration (μs) between PPS edges as measured by RTC and corrected rtc_delta = (rtc_end - rtc_start) @@ -133,31 +143,66 @@ def _calculate(self, gps_start, gps_end, rtc_start, rtc_end): return int(-ppm/0.954) # Measure difference between RTC and GPS rate and return calibration factor - # Note this blocks for upto 1 sec at intervals + # Note this blocks for upto 1 sec at intervals. If 3 successive identical + # results are measured the outcome is considered valid and the coro quits. async def getcal(self, minutes=5): if minutes < 1: - raise ValueError('Minutes must be >= 1') - rtc.calibration(0) # Clear existing cal - # Wait for PPS, then RTC 1/256 second change - # return RTC time in μs since Y2K at instant of PPS + 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 + self.set_rtc() + # Wait for PPS, then RTC 1/256 second change. Return the time the RTC + # would have measured at instant of PPS (μs since Y2K). rtc_start = self._await_pps() - # GPS start time in μs since Y2K: co at time of PPS edge + # GPS start time in μs since Y2K: correct at the time of PPS edge. gps_start = self._get_gps_usecs() + # ******** HACK ******** + # This synchronisation phase is necessary because of occasional anomalous + # readings which result in incorrect start times. If start times are wrong + # it would take a very long time to converge. + synchronised = False + while not synchronised: + await asyncio.sleep(10) + rtc_end = self._await_pps() + gps_end = self._get_gps_usecs() + cal = self._calculate(gps_start, gps_end, rtc_start, rtc_end) + if abs(cal) < 2000: + synchronised = True + else: + print('Resync', (gps_end - gps_start) / 1000000, (rtc_end - rtc_start) / 1000000) + # On 1st pass GPS delta is sometimes exactly 10s and RTC delta is 11s. + # Subsequently both deltas increase by 11s each pass (which figures). + rtc_start = rtc_end # Still getting instances where RTC delta > GPS delta by 1.0015 second + gps_start = gps_end + for n in range(minutes): - for _ in range(6): - await asyncio.sleep(10) # TEST 60 + for _ in range(6): # Try every 10s + await asyncio.sleep(10) # Get RTC time at instant of PPS rtc_end = self._await_pps() gps_end = self._get_gps_usecs() cal = self._calculate(gps_start, gps_end, rtc_start, rtc_end) + print('Run', (gps_end - gps_start) / 1000000, (rtc_end - rtc_start) / 1000000) print('Mins {:d} cal factor {:d}'.format(n + 1, cal)) + results[idx] = cal + idx += 1 + idx %= len(results) + nresults += 1 + if nresults > 5 and len(set(results)) == 1: + return cal # 3 successive identical results received return cal - async def calibrate(self, minutes=5): - print('Waiting for startup') + # 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) # Wait for startup - print('Waiting {} minutes to acquire calibration factor...'.format(minutes)) + await asyncio.sleep(1) + + async def calibrate(self, minutes=5): + 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) @@ -169,7 +214,7 @@ async def calibrate(self, minutes=5): async def run_test(minutes): uart = pyb.UART(4, 9600, read_buf_len=200) sreader = asyncio.StreamReader(uart) - gps = as_GPS.AS_GPS(sreader, fix_cb=lambda *_: red.toggle()) + gps = as_GPS.AS_GPS(sreader, local_offset=1, fix_cb=lambda *_: red.toggle()) pps_pin = pyb.Pin('X3', pyb.Pin.IN) gps_tim = GPS_Timer(gps, pps_pin) await gps_tim.calibrate(minutes) From ab243d0d179ad5f422973d2b40b372e8e3ec7086 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 19 May 2018 13:43:11 +0100 Subject: [PATCH 013/472] Re-organise files. Add as_tGPS.py. --- gps/README.md | 73 +++-- gps/as_GPS.py | 105 +++++-- gps/as_GPS_time.py | 239 ++------------- gps/as_pyGPS.py | 731 --------------------------------------------- gps/as_tGPS.py | 183 ++++++++++++ gps/ast_pb.py | 5 +- gps/ast_pbrw.py | 7 +- gps/astests.py | 16 +- gps/astests_pyb.py | 8 +- 9 files changed, 349 insertions(+), 1018 deletions(-) delete mode 100644 gps/as_pyGPS.py create mode 100644 gps/as_tGPS.py diff --git a/gps/README.md b/gps/README.md index 327bef3..484ce83 100644 --- a/gps/README.md +++ b/gps/README.md @@ -14,6 +14,10 @@ sentences on startup. An optional read-write driver is provided for MTK3329/MTK3339 chips as used on the above board. This enables the device configuration to be altered. +A further driver, for the Pyboard and other boards based on STM processors, +provides for using the GPS device for precision timing. The chip's RTC may be +precisely set and calibrated using the PPS signal from the GPS chip. + ###### [Main README](../README.md) ## 1.1 Overview @@ -26,7 +30,8 @@ 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 it may be wired as follows: +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 | |:---:|:----------:|:--------:| @@ -37,9 +42,9 @@ or 5V. If running the Pyboard from USB it may be wired as follows: | Rx | X1 (U4 tx) | | This is based on UART 4 as used in the test programs; any UART may be used. The -X1-Rx connection is only necessary if using the read/write driver to alter the -GPS device operation. The PPS connection is required only if using the device -for precise timing (`as_GPS_time.py`). Any pin may be used. +UART Tx-GPS Rx connection is only necessary if using the read/write driver. The +PPS connection is required only if using the device for precise timing +(`as_tGPS.py`). Any pin may be used. ## 1.2 Basic Usage @@ -81,7 +86,8 @@ The following are relevant to the default read-only driver. `uasyncio`. Additional files relevant to the read/write driver are listed -[here](./README.md#31-files). +[here](./README.md#31-files). Files for the timing driver are listed +[here](./README.md#41-files). ## 1.4 Installation @@ -94,7 +100,9 @@ Adafruit [Ultimate GPS Breakout] module. If memory errors are encountered on resource constrained devices install 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. +file `as_rwGPS.py` must also be installed. For the +[timing driver](./README.md#4-using-gps-for-accurate-timing) `as_tGPS.py` +should also be copied across. ### 1.4.2 Python 3.5 or later @@ -128,8 +136,7 @@ Optional positional args: 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` does not affect the date value. +Note: If `sreader` is `None` a special test mode is engaged (see `astests.py`). ### 2.1.1 The fix callback @@ -284,13 +291,19 @@ The following are counts since instantiation. ### 2.4.3 Date and time -As received from most recent GPS message. - - * `timestamp` [hrs, mins, secs] e.g. [12, 15, 3.0]. Values are integers except - for secs which is a float (perhaps dependent on GPS hardware). - * `date` [day, month, year] e.g. [23, 3, 18] + * `utc` [hrs: int, mins: int, secs: float] UTC time e.g. [23, 3, 58.0]. Note + that some GPS hardware may only provide integer seconds. The MTK3339 chip + provides a float. + * `local_time` [hrs: int, mins: int, secs: float] Local time. + * `date` [day: int, month: int, year: int] e.g. [23, 3, 18] * `local_offset` Local time offset in hrs as specified to constructor. +The `utc` bound variable updates on receipt of RMC, GLL or GGA messages. + +The `date` and `local_time` variables are updated when an RMC message is +received. A local time offset will result in date changes where the time +offset causes the local time to pass midnight. + ### 2.4.4 Satellite data * `satellites_in_view` No. of satellites in view. (GSV). @@ -478,45 +491,47 @@ be used to set and to calibrate the Pyboard realtime clock (RTC). ## 4.1 Files - * `as_GPS_time.py` Supports the `GPS_Timer` class. + * `as_tGPS.py` The library. Supports the `GPS_Timer` class. + * `as_GPS_time.py` Test scripts for above. ## 4.2 GPS_Timer class Constructor This takes the following arguments: * `gps` An instance of the `AS_GPS` (read-only) or `GPS` (read/write) classes. * `pps_pin` An initialised input `Pin` instance for the PPS signal. + * `led` Default `None`. If an `LED` instance is passed, this will toggle each + time a PPS interrupt is handled. ## 4.3 Public methods -With the exception of `delta` these return immediately. Times are derived from -the GPS PPS signal. These functions should not be called until a valid -time/date message and PPS signal have occurred: await the `ready` coroutine -prior to first use. These functions do not check this for themselves to ensure -fast return. +These return immediately. Times are derived from the GPS PPS signal. These +functions should not be called until a valid time/date message and PPS signal +have occurred: await the `ready` coroutine prior to first use. * `get_secs` No args. Returns a float: the period past midnight in seconds. * `get_t_split` No args. Returns time of day tuple of form (hrs: int, mins: int, secs: float). - * `set_rtc` No args. Sets the Pyboard RTC to GPS time. - * `delta` No args. Returns no. of μs RTC leads GPS. This method blocks for up - to a second. ## 4.4 Public coroutines * `ready` No args. Pauses until a valid time/date message and PPS signal have occurred. + * `set_rtc` No args. Sets the Pyboard RTC to GPS time. Coro 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 - Pyboard RTC and returns the calibration factor for it. + Pyboard RTC and returns the calibration factor for it. This 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 1 + 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 and re-apply it on startup. - -The coroutine calculates the calibration factor at 10 second intervals and will -return early if three consecutive identical calibration factors are calculated. -Note that, because of the need for precise timing, this coroutine blocks at -intervals for periods of up to one second. +calibration factor in a file (or in code) and re-apply it on startup. # 5. Supported Sentences diff --git a/gps/as_GPS.py b/gps/as_GPS.py index e6cabc4..5d4e17e 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -8,6 +8,9 @@ # Copyright (c) 2018 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: @@ -45,6 +48,7 @@ DATE = const(RMC) COURSE = const(RMC | VTG) + class AS_GPS(object): _SENTENCE_LIMIT = 76 # Max sentence length (based on GGA sentence) _NO_FIX = 1 @@ -54,6 +58,25 @@ class AS_GPS(object): 'June', 'July', 'August', 'September', 'October', 'November', 'December') + # 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 "*" @staticmethod def _crc_check(res, ascii_crc): @@ -74,17 +97,21 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC self.cb_mask = cb_mask self._fix_cb_args = fix_cb_args - # Import utime or time for fix time handling + # 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 = {'GPRMC': self._gprmc, 'GLRMC': self._gprmc, @@ -112,8 +139,9 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC # Data From Sentences # Time. Ignore http://www.gpsinformation.org/dale/nmea.htm, hardware # returns a float. - self.timestamp = [0, 0, 0.0] # [h, m, s] - self.date = [0, 0, 0] # [d, m, y] + self.utc = [0, 0, 0.0] # [h: int, m: int, s: float] + self.local_time = [0, 0, 0.0] # [h: int, m: int, s: float] + self.date = [0, 0, 0] # [dd: int, mm: int, yy: int] self.local_offset = local_offset # hrs # Position/Motion @@ -232,15 +260,46 @@ def _fix(self, gps_segments, idx_lat, idx_long): # Set timestamp. If time/date not present retain last reading (if any). def _set_timestamp(self, utc_string): - if utc_string: # Possible timestamp found - try: - self.timestamp[0] = int(utc_string[0:2]) + self.local_offset # h - self.timestamp[1] = int(utc_string[2:4]) # mins - self.timestamp[2] = float(utc_string[4:]) # secs from chip is a float - return True - except ValueError: - pass - return False + if not utc_string: + return False + # Possible timestamp found + try: + self.utc[0] = int(utc_string[0:2]) # h + self.utc[1] = int(utc_string[2:4]) # mins + self.utc[2] = float(utc_string[4:]) # secs from chip is a float + return True + except ValueError: + return False + for idx in range(3): + self.local_time[idx] = self.utc[idx] + return True + + # A local offset may exist so check for date rollover. Local offsets can + # include fractions of an hour but not seconds (AFAIK). + def _set_date(self, date_string): + if not date_string: + return False + try: + d = int(date_string[0:2]) # day + m = int(date_string[2:4]) # month + y = int(date_string[4:6]) + 2000 # year + except ValueError: # Bad Date stamp value present + return False + hrs = self.utc[0] + mins = self.utc[1] + secs = self.utc[2] + wday = self._week_day(y, m, d) - 1 + t = self._mktime((y, m, d, hrs, mins, int(secs), wday, 0, 0)) + t += int(3600 * self.local_offset) + y, m, d, hrs, mins, *_ = self._localtime(t) # Preserve float seconds + y -= 2000 + self.local_time[0] = hrs + self.local_time[1] = mins + self.local_time[2] = secs + self.date[0] = d + self.date[1] = m + self.date[2] = y + return True ######################################## # Sentence Parsers @@ -255,20 +314,9 @@ def _set_timestamp(self, utc_string): def _gprmc(self, gps_segments): # Parse RMC sentence self._valid &= ~RMC # UTC Timestamp. - try: - self._set_timestamp(gps_segments[1]) - except ValueError: # Bad Timestamp value present - return False - - # Date stamp - try: - date_string = gps_segments[9] - if date_string: # Possible date stamp found - self.date[0] = int(date_string[0:2]) # day - self.date[1] = int(date_string[2:4]) # month - self.date[2] = int(date_string[4:6]) # year 18 == 2018 - - except ValueError: # Bad Date stamp value present + if not self._set_timestamp(gps_segments[1]): + return False # Bad Timestamp value present + if not self._set_date(gps_segments[9]): return False # Check Receiver Data Valid Flag @@ -599,8 +647,9 @@ def speed_string(self, unit=KPH): return sform.format(speed, 'knots') return sform.format(speed, 'km/h') - def time(self): - return '{:02d}:{:02d}:{:2.3f}'.format(*self.timestamp) + def time(self, local=True): + t = self.local_time if local else self.utc + return '{:02d}:{:02d}:{:2.3f}'.format(*t) def date_string(self, formatting=MDY): day, month, year = self.date diff --git a/gps/as_GPS_time.py b/gps/as_GPS_time.py index bf78467..f4f5653 100644 --- a/gps/as_GPS_time.py +++ b/gps/as_GPS_time.py @@ -1,224 +1,47 @@ -# as_GPS_time.py Using GPS for precision timing and for calibrating Pyboard RTC +# as_GPS_time.py Test scripts for as_tGPS +# Using GPS for precision timing and for calibrating Pyboard RTC # This is STM-specific: requires pyb module. -# Current state: getcal works but has a HACK due to a realtime issue I haven't yet diagnosed. -# Other API functions need testing - # Copyright (c) 2018 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file import uasyncio as asyncio -import math import pyb -import utime -import micropython import as_GPS +import as_tGPS -micropython.alloc_emergency_exception_buf(100) - -red, green, yellow, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) -rtc = pyb.RTC() - -# Convenience function. Return RTC seconds since midnight as float -def rtc_secs(): - dt = rtc.datetime() - return 3600*dt[4] + 60*dt[5] + dt[6] + (255 - dt[7])/256 - -# 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. -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 - -class GPS_Timer(): - def __init__(self, gps, pps_pin): - self.gps = gps - self.secs = None # Integer time since midnight at last PPS - self.acquired = None # Value of ticks_us at edge of PPS - loop = asyncio.get_event_loop() - loop.create_task(self._start(pps_pin)) - - async def _start(self, pps_pin): - await self.gps.data_received(date=True) - pyb.ExtInt(pps_pin, pyb.ExtInt.IRQ_RISING, pyb.Pin.PULL_NONE, self._isr) - - def _isr(self, _): - # Time in last NMEA sentence - t = self.gps.timestamp - # secs is rounded down to an int: exact time of last PPS - secs = 3600*t[0] + 60*t[1] + int(t[2]) - # Could be an outage here, so PPS arrives many secs after last sentence - # Is this right? Does PPS continue during outage? - self.secs = secs + 1 # PPS preceeds NMEA so add 1 sec - self.acquired = utime.ticks_us() - blue.toggle() # TEST - - # Return accurate GPS time in seconds (float) since midnight - def get_secs(self): - t = self.secs - if t != self.secs: # An interrupt has occurred - # Re-read to ensure .acquired is correct. No need to get clever - t = self.secs # here as IRQ's are at only 1Hz - return t + utime.ticks_diff(utime.ticks_us(), self.acquired) / 1000000 - - # Return GPS time as hrs: int, mins: int, secs: int, fractional_secs: float - def _get_hms(self): - t = math.modf(self.get_secs()) - x, secs = divmod(int(t[1]), 60) - hrs, mins = divmod(x, 60) - return hrs, mins, secs, t[0] - - # Return accurate GPS time of day (hrs , mins , secs) - def get_t_split(self): - hrs, mins, secs, frac_secs = self._get_hms() - return hrs, mins, secs + frac_secs - - # Return a time/date tuple suitable for setting RTC - def _get_td_split(self): - d, m, y = self.gps.date - y += 2000 - hrs, mins, secs, frac_secs = self._get_hms() - ss = int(255*(1 - frac_secs)) - return y, m, d, week_day(y, m, d), hrs, mins, secs, ss - - def set_rtc(self): - rtc.datetime(self._get_td_split()) - - # Time from GPS: integer μs since Y2K. Call after awaiting PPS: result is - # time when PPS leading edge occurred so fractional secs discarded. - def _get_gps_usecs(self): - d, m, y = self.gps.date - y += 2000 - hrs, mins, secs, _ = self._get_hms() - tim = utime.mktime((y, m, d, hrs, mins, secs, week_day(y, m, d) - 1, 0)) - return tim * 1000000 - - # Value of RTC time at current instant. Units μs since Y2K. Tests OK. - 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. - def delta(self): - rtc_time = self._await_pps() # μs since Y2K at time of PPS - gps_time = self._get_gps_usecs() # μs since Y2K at 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. - def _await_pps(self): - t0 = self.acquired - while self.acquired == t0: # Busy-wait on PPS interrupt - pass - st = rtc.datetime()[7] - while rtc.datetime()[7] == st: # Wait for RTC to change - pass - dt = utime.ticks_diff(utime.ticks_us(), self.acquired) - t = self._get_rtc_usecs() # Read RTC now - assert abs(dt) < 10000 - return t - dt - - # 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 - # Note this blocks for upto 1 sec at intervals. If 3 successive identical - # results are measured 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 - self.set_rtc() - # Wait for PPS, then RTC 1/256 second change. Return the time the RTC - # would have measured at instant of PPS (μs since Y2K). - rtc_start = self._await_pps() - # GPS start time in μs since Y2K: correct at the time of PPS edge. - gps_start = self._get_gps_usecs() - # ******** HACK ******** - # This synchronisation phase is necessary because of occasional anomalous - # readings which result in incorrect start times. If start times are wrong - # it would take a very long time to converge. - synchronised = False - while not synchronised: - await asyncio.sleep(10) - rtc_end = self._await_pps() - gps_end = self._get_gps_usecs() - cal = self._calculate(gps_start, gps_end, rtc_start, rtc_end) - if abs(cal) < 2000: - synchronised = True - else: - print('Resync', (gps_end - gps_start) / 1000000, (rtc_end - rtc_start) / 1000000) - # On 1st pass GPS delta is sometimes exactly 10s and RTC delta is 11s. - # Subsequently both deltas increase by 11s each pass (which figures). - rtc_start = rtc_end # Still getting instances where RTC delta > GPS delta by 1.0015 second - gps_start = gps_end - - 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 = self._await_pps() - gps_end = self._get_gps_usecs() - cal = self._calculate(gps_start, gps_end, rtc_start, rtc_end) - print('Run', (gps_end - gps_start) / 1000000, (rtc_end - rtc_start) / 1000000) - print('Mins {:d} cal factor {:d}'.format(n + 1, cal)) - results[idx] = cal - idx += 1 - idx %= len(results) - nresults += 1 - if nresults > 5 and len(set(results)) == 1: - return cal # 3 successive identical results received - 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): - 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)) - -# Test script. Red LED toggles on fix, Blue on PPS interrupt. -async def run_test(minutes): +# 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(4, 9600, read_buf_len=200) sreader = asyncio.StreamReader(uart) gps = as_GPS.AS_GPS(sreader, local_offset=1, fix_cb=lambda *_: red.toggle()) pps_pin = pyb.Pin('X3', pyb.Pin.IN) - gps_tim = GPS_Timer(gps, pps_pin) + return as_tGPS.GPS_Timer(gps, pps_pin, blue) + +async def drift_test(gps_tim, minutes): + for _ in range(minutes): + for _ in range(6): + dt = await gps_tim.delta() + print(gps_tim.get_t_split(), end='') + print('Delta {}'.format(dt)) + await asyncio.sleep(10) + +# Calibrate and set the Pyboard RTC +async def do_cal(minutes): + gps_tim = await setup() await gps_tim.calibrate(minutes) -def test(minutes=5): +def calibrate(minutes=5): + loop = asyncio.get_event_loop() + loop.run_until_complete(do_cal(minutes)) + +# Every 10s print the difference between GPS time and RTC time +async def do_drift(minutes): + gps_tim = await setup() + await drift_test(gps_tim, minutes) + +def drift(minutes=5): loop = asyncio.get_event_loop() - loop.run_until_complete(run_test(minutes)) + loop.run_until_complete(do_drift(minutes)) diff --git a/gps/as_pyGPS.py b/gps/as_pyGPS.py deleted file mode 100644 index 8c57dab..0000000 --- a/gps/as_pyGPS.py +++ /dev/null @@ -1,731 +0,0 @@ -""" -# MicropyGPS - a GPS NMEA sentence parser for Micropython/Python 3.X -# Copyright (c) 2017 Michael Calvin McCoy (calvin.mccoy@gmail.com) -# The MIT License (MIT) - see LICENSE file -""" -# Modified for uasyncio operation Peter Hinch April 2018 -# Portability: -# Replaced pyb with machine -# If machine not available assumed to be running under CPython (Raspberry Pi) -# time module assumed to return a float - -# TODO: -# Time Since First Fix -# Distance/Time to Target -# More Helper Functions -# Dynamically limit sentences types to parse - -from math import floor, modf -try: - import uasyncio as asyncio -except ImportError: - import asyncio - -# Import utime or time for fix time handling -try: - # Assume running on MicroPython - import utime -except ImportError: - # Otherwise default to time module for non-embedded implementations - # Should still support millisecond resolution. - import time - - -class MicropyGPS(object): - """GPS NMEA Sentence Parser. Creates object that stores all relevant GPS data and statistics. - Parses sentences by complete line using update(). """ - - _SENTENCE_LIMIT = 76 # Max sentence length (based on GGA sentence) - _HEMISPHERES = ('N', 'S', 'E', 'W') - _NO_FIX = 1 - _FIX_2D = 2 - _FIX_3D = 3 - _DIRECTIONS = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', - 'WNW', 'NW', 'NNW') - _MONTHS = ('January', 'February', 'March', 'April', 'May', - 'June', 'July', 'August', 'September', 'October', - 'November', 'December') - - def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, fix_cb_args=()): - """ - Setup GPS Object Status Flags, Internal Data Registers, etc - local_offset (int): Timzone Difference to UTC - location_formatting (str): Style For Presenting Longitude/Latitude: - Decimal Degree Minute (ddm) - 40° 26.767′ N - Degrees Minutes Seconds (dms) - 40° 26′ 46″ N - Decimal Degrees (dd) - 40.446° N - """ - - self.sreader = sreader # None: in testing update is called with simulated data - self.fix_cb = fix_cb - self.fix_cb_args = fix_cb_args - # All the currently supported NMEA sentences - self.supported_sentences = {'GPRMC': self.gprmc, 'GLRMC': self.gprmc, - 'GPGGA': self.gpgga, 'GLGGA': self.gpgga, - 'GPVTG': self.gpvtg, 'GLVTG': self.gpvtg, - 'GPGSA': self.gpgsa, 'GLGSA': self.gpgsa, - 'GPGSV': self.gpgsv, 'GLGSV': self.gpgsv, - 'GPGLL': self.gpgll, 'GLGLL': self.gpgll, - 'GNGGA': self.gpgga, 'GNRMC': self.gprmc, - 'GNVTG': self.gpvtg, - } - - ##################### - # Object Status Flags - self.fix_time = None - - ##################### - # Sentence Statistics - self.crc_fails = 0 - self.clean_sentences = 0 - self.parsed_sentences = 0 - - ##################### - # Data From Sentences - # Time - self.timestamp = (0, 0, 0) - self.date = (0, 0, 0) - self.local_offset = local_offset - - # Position/Motion - self._latitude = (0, 0.0, 'N') - self._longitude = (0, 0.0, 'W') - self.speed = (0.0, 0.0, 0.0) - self.course = 0.0 - self.altitude = 0.0 - self.geoid_height = 0.0 - - # GPS Info - self.satellites_in_view = 0 - self.satellites_in_use = 0 - self.satellites_used = [] - self.last_sv_sentence = 0 - self.total_sv_sentences = 0 - self.satellite_data = dict() - self.hdop = 0.0 - self.pdop = 0.0 - self.vdop = 0.0 - self.valid = False - self.fix_stat = 0 - self.fix_type = 1 - if sreader is not None: # Running with UART data - loop = asyncio.get_event_loop() - loop.create_task(self.run()) - - ########################################## - # Data Stream Handler Functions - ########################################## - - - async def run(self): - while True: - res = await self.sreader.readline() -# print(res) - self.update(res.decode('utf8')) - - # 8-bit xor of characters between "$" and "*" - def crc_check(self, 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 - - # Update takes a line of text - def update(self, line): - line = line.rstrip() - try: - next(c for c in line if ord(c) < 10 or ord(c) > 126) - return None # Bad character received - except StopIteration: - pass - - if len(line) > self._SENTENCE_LIMIT: - return None # Too long - - if not '*' in line: - return None - - a = line.split(',') - segs = a[:-1] + a[-1].split('*') - if not self.crc_check(line, segs[-1]): - self.crc_fails += 1 - return None - - self.clean_sentences += 1 - segs[0] = segs[0][1:] # discard $ - if segs[0] in self.supported_sentences: - if self.supported_sentences[segs[0]](segs): - self.parsed_sentences += 1 - return segs[0] - - def new_fix_time(self): - """Updates a high resolution counter with current time when fix is updated. Currently only triggered from - GGA, GSA and RMC sentences""" - try: - self.fix_time = utime.ticks_ms() - except NameError: - self.fix_time = time.time() - self.fix_cb(self, *self.fix_cb_args) # Run the callback - - ######################################## - # Coordinates Translation Functions - ######################################## - - def latitude(self, coord_format=None): - """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': - minute_parts = modf(self._latitude[1]) - seconds = round(minute_parts[0] * 60) - return [self._latitude[0], int(minute_parts[1]), seconds, self._latitude[2]] - else: - return self._latitude - - def longitude(self, coord_format=None): - """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': - minute_parts = modf(self._longitude[1]) - seconds = round(minute_parts[0] * 60) - return [self._longitude[0], int(minute_parts[1]), seconds, self._longitude[2]] - else: - return self._longitude - - - ######################################## - # Sentence Parsers - ######################################## - def gprmc(self, gps_segments): - """Parse Recommended Minimum Specific GPS/Transit data (RMC)Sentence. - Updates UTC timestamp, latitude, longitude, Course, Speed, Date, and fix status - """ - - # UTC Timestamp. If time/date not present retain last reading (if any). - try: - utc_string = gps_segments[1] - - if utc_string: # Possible timestamp found - hours = int(utc_string[0:2]) + self.local_offset - minutes = int(utc_string[2:4]) - seconds = float(utc_string[4:]) - self.timestamp = (hours, minutes, seconds) - - except ValueError: # Bad Timestamp value present - return False - - # Date stamp - try: - date_string = gps_segments[9] - - # Date string printer function assumes to be year >=2000, - # date_string() must be supplied with the correct century argument to display correctly - if date_string: # Possible date stamp found - day = int(date_string[0:2]) - month = int(date_string[2:4]) - year = int(date_string[4:6]) - self.date = (day, month, year) - - except ValueError: # Bad Date stamp value present - return False - - # Check Receiver Data Valid Flag - if gps_segments[2] == 'A': # Data from Receiver is Valid/Has Fix - - # Longitude / Latitude - try: - # Latitude - l_string = gps_segments[3] - lat_degs = int(l_string[0:2]) - lat_mins = float(l_string[2:]) - lat_hemi = gps_segments[4] - - # Longitude - l_string = gps_segments[5] - lon_degs = int(l_string[0:3]) - lon_mins = float(l_string[3:]) - lon_hemi = gps_segments[6] - except ValueError: - return False - - if lat_hemi not in self._HEMISPHERES: - return False - - if lon_hemi not in self._HEMISPHERES: - return False - - # Speed - try: - spd_knt = float(gps_segments[7]) - except ValueError: - return False - - # Course - try: - course = float(gps_segments[8]) - except ValueError: - return False - - # TODO - Add Magnetic Variation - - # Update Object Data - self._latitude = (lat_degs, lat_mins, lat_hemi) - self._longitude = (lon_degs, lon_mins, lon_hemi) - # Include mph and hm/h - self.speed = (spd_knt, spd_knt * 1.151, spd_knt * 1.852) - self.course = course - self.valid = True - - # Update Last Fix Time - self.new_fix_time() - - else: # Leave data unchanged if Sentence is 'Invalid' - self.valid = False - - return True - - def gpgll(self, gps_segments): - """Parse Geographic Latitude and Longitude (GLL)Sentence. Updates UTC timestamp, latitude, - longitude, and fix status""" - - # UTC Timestamp. If time/date not present retain last reading (if any). - try: - utc_string = gps_segments[5] - - if utc_string: # Possible timestamp found - hours = int(utc_string[0:2]) + self.local_offset - minutes = int(utc_string[2:4]) - seconds = float(utc_string[4:]) - self.timestamp = (hours, minutes, seconds) - - except ValueError: # Bad Timestamp value present - return False - - # Check Receiver Data Valid Flag - if gps_segments[6] == 'A': # Data from Receiver is Valid/Has Fix - - # Longitude / Latitude - try: - # Latitude - l_string = gps_segments[1] - lat_degs = int(l_string[0:2]) - lat_mins = float(l_string[2:]) - lat_hemi = gps_segments[2] - - # Longitude - l_string = gps_segments[3] - lon_degs = int(l_string[0:3]) - lon_mins = float(l_string[3:]) - lon_hemi = gps_segments[4] - except ValueError: - return False - - if lat_hemi not in self._HEMISPHERES: - return False - - if lon_hemi not in self._HEMISPHERES: - return False - - # Update Object Data - self._latitude = (lat_degs, lat_mins, lat_hemi) - self._longitude = (lon_degs, lon_mins, lon_hemi) - self.valid = True - - # Update Last Fix Time - self.new_fix_time() - - else: # Leave data unchanged if Sentence is 'Invalid' - self.valid = False - - return True - - def gpvtg(self, gps_segments): - """Parse Track Made Good and Ground Speed (VTG) Sentence. Updates speed and course""" - try: - course = float(gps_segments[1]) - spd_knt = float(gps_segments[5]) - except ValueError: - return False - - # Include mph and km/h - self.speed = (spd_knt, spd_knt * 1.151, spd_knt * 1.852) - self.course = course - return True - - def gpgga(self, gps_segments): - """Parse Global Positioning System Fix Data (GGA) Sentence. Updates UTC timestamp, latitude, longitude, - fix status, satellites in use, Horizontal Dilution of Precision (HDOP), altitude, geoid height and fix status""" - - try: - # UTC Timestamp - utc_string = gps_segments[1] - - # Skip timestamp if receiver doesn't have one yet - if utc_string: - hms = (int(utc_string[0:2]) + self.local_offset, - int(utc_string[2:4]), - float(utc_string[4:])) - else: - hms = None - - # 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]) - - except ValueError: - return False - - # Process Location and Speed Data if Fix is GOOD - if fix_stat: - - # Longitude / Latitude - try: - # Latitude - l_string = gps_segments[2] - lat_degs = int(l_string[0:2]) - lat_mins = float(l_string[2:]) - lat_hemi = gps_segments[3] - - # Longitude - l_string = gps_segments[4] - lon_degs = int(l_string[0:3]) - lon_mins = float(l_string[3:]) - lon_hemi = gps_segments[5] - except ValueError: - return False - - if lat_hemi not in self._HEMISPHERES: - return False - - if lon_hemi not in self._HEMISPHERES: - return False - - # Altitude / Height Above Geoid - try: - altitude = float(gps_segments[9]) - geoid_height = float(gps_segments[11]) - except ValueError: - return False - - # Update Object Data - self._latitude = (lat_degs, lat_mins, lat_hemi) - self._longitude = (lon_degs, lon_mins, lon_hemi) - self.altitude = altitude - self.geoid_height = geoid_height - - # Update Object Data - if hms is not None: - self.timestamp = hms - self.satellites_in_use = satellites_in_use - self.hdop = hdop - self.fix_stat = fix_stat - - # If Fix is GOOD, update fix timestamp - if fix_stat: - self.new_fix_time() - - return True - - def gpgsa(self, gps_segments): - """Parse GNSS DOP and Active Satellites (GSA) sentence. Updates GPS fix type, list of satellites used in - fix calculation, Position Dilution of Precision (PDOP), Horizontal Dilution of Precision (HDOP), Vertical - Dilution of Precision, and fix status""" - - # Fix Type (None,2D or 3D) - try: - fix_type = int(gps_segments[2]) - except ValueError: - return False - - # 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: - try: - sat_number = int(sat_number_str) - sats_used.append(sat_number) - except ValueError: - return False - else: - break - - # PDOP,HDOP,VDOP - try: - pdop = float(gps_segments[15]) - hdop = float(gps_segments[16]) - vdop = float(gps_segments[17]) - except ValueError: - return False - - # Update Object Data - self.fix_type = fix_type - - # If Fix is GOOD, update fix timestamp - if fix_type > self._NO_FIX: - self.new_fix_time() - - self.satellites_used = sats_used - self.hdop = hdop - self.vdop = vdop - self.pdop = pdop - - return True - - def gpgsv(self, gps_segments): - """Parse Satellites in View (GSV) sentence. Updates number of SV Sentences,the number of the last SV sentence - parsed, and data on each satellite present in the sentence""" - try: - num_sv_sentences = int(gps_segments[1]) - current_sv_sentence = int(gps_segments[2]) - sats_in_view = int(gps_segments[3]) - except ValueError: - return False - - # 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 (ValueError,IndexError): - return False - - 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) - - return True - - ######################################### - # User Helper Functions - # These functions make working with the GPS object data easier - ######################################### - - def satellite_data_updated(self): - """ - Checks if the all the GSV sentences in a group have been read, making satellite data complete - :return: boolean - """ - if self.total_sv_sentences > 0 and self.total_sv_sentences == self.last_sv_sentence: - return True - else: - return False - - def satellites_visible(self): - """ - Returns a list of of the satellite PRNs currently visible to the receiver - :return: list - """ - return list(self.satellite_data.keys()) - - def time_since_fix(self): - """Returns number of millisecond since the last sentence with a valid fix was parsed. - Returns -1 if no fix has been found""" - - # Test if a Fix has been found - if self.fix_time is None: - return -1 - - # Try calculating fix time using utime; default to seconds if not running MicroPython - try: - current = utime.ticks_diff(utime.ticks_ms(), self.fix_time) - except NameError: - current = (time.time() - self.fix_time) * 1000 # ms - - return current - - def compass_direction(self): - """ - Determine a cardinal or inter-cardinal direction based on current course. - :return: string - """ - # Calculate the offset for a rotated compass - if self.course >= 348.75: - offset_course = 360 - self.course - else: - offset_course = self.course + 11.25 - - # Each compass point is separated by 22.5 degrees, divide to find lookup value - dir_index = floor(offset_course / 22.5) - - final_dir = self._DIRECTIONS[dir_index] - - return final_dir - - def latitude_string(self, coord_format=None): - """ - Create a readable string of the current latitude data - :return: string - """ - if coord_format == 'dd': - form_lat = self.latitude(coord_format) - lat_string = str(form_lat[0]) + '° ' + str(self._latitude[2]) - elif coord_format == 'dms': - form_lat = self.latitude(coord_format) - lat_string = str(form_lat[0]) + '° ' + str(form_lat[1]) + "' " + str(form_lat[2]) + '" ' + str(form_lat[3]) - elif coord_format == 'kml': - form_lat = self.latitude('dd') - lat_string = str(form_lat[0] if self._latitude[2] == 'N' else -form_lat[0]) - else: - lat_string = str(self._latitude[0]) + '° ' + str(self._latitude[1]) + "' " + str(self._latitude[2]) - return lat_string - - def longitude_string(self, coord_format=None): - """ - Create a readable string of the current longitude data - :return: string - """ - if coord_format == 'dd': - form_long = self.longitude(coord_format) - lon_string = str(form_long[0]) + '° ' + str(self._longitude[2]) - elif coord_format == 'dms': - form_long = self.longitude(coord_format) - lon_string = str(form_long[0]) + '° ' + str(form_long[1]) + "' " + str(form_long[2]) + '" ' + str(form_long[3]) - elif coord_format == 'kml': - form_long = self.longitude('dd') - lon = form_long[0] if self._longitude[2] == 'E' else -form_long[0] - lon_string = str(lon) - else: - lon_string = str(self._longitude[0]) + '° ' + str(self._longitude[1]) + "' " + str(self._longitude[2]) - return lon_string - - def speed_string(self, unit='kph'): - """ - Creates a readable string of the current speed data in one of three units - :param unit: string of 'kph','mph, or 'knot' - :return: - """ - if unit == 'mph': - speed_string = str(self.speed[1]) + ' mph' - - elif unit == 'knot': - if self.speed[0] == 1: - unit_str = ' knot' - else: - unit_str = ' knots' - speed_string = str(self.speed[0]) + unit_str - - else: - speed_string = str(self.speed[2]) + ' km/h' - - return speed_string - - def date_string(self, formatting='s_mdy', century='20'): - """ - Creates a readable string of the current date. - Can select between long format: Januray 1st, 2014 - or two short formats: - 11/01/2014 (MM/DD/YYYY) - 01/11/2014 (DD/MM/YYYY) - :param formatting: string 's_mdy', 's_dmy', or 'long' - :param century: int delineating the century the GPS data is from (19 for 19XX, 20 for 20XX) - :return: date_string string with long or short format date - """ - - # Long Format Januray 1st, 2014 - if formatting == 'long': - # Retrieve Month string from private set - month = self._MONTHS[self.date[1] - 1] - - # Determine Date Suffix - if self.date[0] in (1, 21, 31): - suffix = 'st' - elif self.date[0] in (2, 22): - suffix = 'nd' - elif self.date[0] == 3: - suffix = 'rd' - else: - suffix = 'th' - - day = str(self.date[0]) + suffix # Create Day String - - year = century + str(self.date[2]) # Create Year String - - date_string = month + ' ' + day + ', ' + year # Put it all together - - else: - # Add leading zeros to day string if necessary - if self.date[0] < 10: - day = '0' + str(self.date[0]) - else: - day = str(self.date[0]) - - # Add leading zeros to month string if necessary - if self.date[1] < 10: - month = '0' + str(self.date[1]) - else: - month = str(self.date[1]) - - # Add leading zeros to year string if necessary - if self.date[2] < 10: - year = '0' + str(self.date[2]) - else: - year = str(self.date[2]) - - # Build final string based on desired formatting - if formatting == 's_dmy': - date_string = day + '/' + month + '/' + year - - else: # Default date format - date_string = month + '/' + day + '/' + year - - return date_string - - -if __name__ == "__main__": - pass diff --git a/gps/as_tGPS.py b/gps/as_tGPS.py new file mode 100644 index 0000000..9c65891 --- /dev/null +++ b/gps/as_tGPS.py @@ -0,0 +1,183 @@ +# as_tGPS.py Using GPS for precision timing and for calibrating Pyboard RTC +# This is STM-specific: requires pyb module. + +# Copyright (c) 2018 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +import math +import pyb +import utime +import micropython +import as_GPS + +micropython.alloc_emergency_exception_buf(100) + +rtc = pyb.RTC() + +# Convenience function. Return RTC seconds since midnight as float +def rtc_secs(): + dt = rtc.datetime() + return 3600*dt[4] + 60*dt[5] + dt[6] + (255 - dt[7])/256 + +class GPS_Timer(): + def __init__(self, gps, pps_pin, led=None): + self.gps = gps + self.led = led + self.secs = None # Integer time since midnight at last PPS + self.acquired = None # Value of ticks_us at edge of PPS + self.rtc_set = None # Data for setting RTC + loop = asyncio.get_event_loop() + loop.create_task(self._start(pps_pin)) + + async def _start(self, pps_pin): + await self.gps.data_received(date=True) + pyb.ExtInt(pps_pin, pyb.ExtInt.IRQ_RISING, pyb.Pin.PULL_NONE, self._isr) + + def _isr(self, _): + self.acquired = utime.ticks_us() # Save time of PPS + if self.rtc_set is not None: + rtc.datetime(self.rtc_set) + self.rtc_set = None + # Time in last NMEA sentence + t = self.gps.local_time + # secs is rounded down to an int: exact time of last PPS. + # This PPS is one second later + self.secs = 3600*t[0] + 60*t[1] + int(t[2]) + 1 + # Could be an outage here, so PPS arrives many secs after last sentence + # Is this right? Does PPS continue during outage? + if self.led is not None: + self.led.toggle() + + # Return accurate GPS time in seconds (float) since midnight + def get_secs(self): + t = self.secs + if t != self.secs: # An interrupt has occurred + # Re-read to ensure .acquired is correct. No need to get clever + t = self.secs # here as IRQ's are at only 1Hz + return t + utime.ticks_diff(utime.ticks_us(), self.acquired) / 1000000 + + # Return GPS time as hrs: int, mins: int, secs: int, fractional_secs: float + def _get_hms(self): + t = math.modf(self.get_secs()) + x, secs = divmod(int(t[1]), 60) + hrs, mins = divmod(x, 60) + return hrs, mins, secs, t[0] + + # Return accurate GPS time of day (hrs , mins , secs) + def get_t_split(self): + hrs, mins, secs, frac_secs = self._get_hms() + return hrs, mins, secs + frac_secs + + # Subsecs register is read-only. So need to set RTC on PPS leading edge. + # Calculate time of next edge, save the new RTC time, and let ISR update + # the RTC. + async def set_rtc(self): + d, m, y = self.gps.date + y += 2000 + t0 = self.acquired + while self.acquired == t0: # Busy-wait on PPS interrupt + await asyncio.sleep_ms(0) + hrs, mins, secs = self.gps.local_time + secs = int(secs) + 2 # Time of next PPS + self.rtc_set = [y, m, d, self.gps._week_day(y, m, d), hrs, mins, secs, 0] +# rtc.datetime((y, m, d, self.gps._week_day(y, m, d), hrs, mins, secs, 0)) + + # Time from GPS: integer μs since Y2K. Call after awaiting PPS: result is + # time when PPS leading edge occurred so fractional secs discarded. + def _get_gps_usecs(self): + d, m, y = self.gps.date + y += 2000 + hrs, mins, secs, _ = self._get_hms() + tim = utime.mktime((y, m, d, hrs, mins, secs, self.gps._week_day(y, m, d) - 1, 0)) + return tim * 1000000 + + # Value of RTC time at current instant. Units μs since Y2K. Tests OK. + 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): + rtc_time = await self._await_pps() # μs since Y2K at time of latest PPS + gps_time = self._get_gps_usecs() # μs since Y2K at previous PPS + return rtc_time - gps_time + 1000000 # so add 1s + + # 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 + await asyncio.sleep_ms(0) + 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) + t = self._get_rtc_usecs() # Read RTC now + return t - dt + + # 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 (μs since Y2K). + rtc_start = await self._await_pps() + # GPS start time in μs since Y2K: correct at the time of PPS edge. + gps_start = self._get_gps_usecs() + 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 = await self._await_pps() + gps_end = self._get_gps_usecs() + cal = self._calculate(gps_start, gps_end, rtc_start, rtc_end) + if abs(cal) > 2000: # Still occasionally occurs + rtc_start = rtc_end + gps_start = gps_end + cal = 0 + print('Restarting calibration.') + else: + 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): + 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)) diff --git a/gps/ast_pb.py b/gps/ast_pb.py index 1de4e7c..502b065 100644 --- a/gps/ast_pb.py +++ b/gps/ast_pb.py @@ -73,7 +73,8 @@ async def date(gps): await gps.data_received(date=True) print('***** DATE AND TIME *****') print('Data is Valid:', gps._valid) - print('UTC Timestamp:', gps.timestamp) + print('UTC time:', gps.utc) + print('Local time:', gps.local_time) print('Date:', gps.date_string(as_GPS.LONG)) print() @@ -85,7 +86,7 @@ async def gps_test(): sreader = asyncio.StreamReader(uart) timer = aswitch.Delay_ms(timeout) sentence_count = 0 - gps = as_GPS.AS_GPS(sreader, fix_cb=callback, fix_cb_args=(timer,)) + 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)) diff --git a/gps/ast_pbrw.py b/gps/ast_pbrw.py index 873dca9..e6692d0 100644 --- a/gps/ast_pbrw.py +++ b/gps/ast_pbrw.py @@ -91,7 +91,8 @@ async def date(gps): blue.toggle() print('***** DATE AND TIME *****') print('Data is Valid:', hex(gps._valid)) - print('UTC Timestamp:', gps.timestamp) + print('UTC Time:', gps.utc) + print('Local time:', gps.local_time) print('Date:', gps.date_string(as_GPS.LONG)) print() @@ -136,8 +137,8 @@ async def gps_test(): swriter = asyncio.StreamWriter(uart, {}) timer = aswitch.Delay_ms(timeout) sentence_count = 0 - gps = as_rwGPS.GPS(sreader, swriter, fix_cb=callback, fix_cb_args=(timer,), - msg_cb = message_cb) + 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') diff --git a/gps/astests.py b/gps/astests.py index 6606810..2204f68 100755 --- a/gps/astests.py +++ b/gps/astests.py @@ -53,7 +53,7 @@ def run_tests(): print('Parsed a', sentence, 'Sentence') print('Longitude:', my_gps.longitude()) print('Latitude', my_gps.latitude()) - print('UTC Timestamp:', my_gps.timestamp) + print('UTC Timestamp:', my_gps.utc) print('Speed:', my_gps.speed()) print('Date Stamp:', my_gps.date) print('Course', my_gps.course) @@ -71,7 +71,7 @@ def run_tests(): print('Parsed a', sentence, 'Sentence') print('Longitude:', my_gps.longitude()) print('Latitude', my_gps.latitude()) - print('UTC Timestamp:', my_gps.timestamp) + print('UTC Timestamp:', my_gps.utc) print('Data is Valid:', bool(my_gps._valid & 2)) print('') @@ -99,7 +99,7 @@ def run_tests(): print('Parsed a', sentence, 'Sentence') print('Longitude', my_gps.longitude()) print('Latitude', my_gps.latitude()) - print('UTC Timestamp:', my_gps.timestamp) + 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) @@ -166,15 +166,5 @@ def run_tests(): print('Unsupported sentences:', my_gps.unsupported_sentences) print('CRC_Fails:', my_gps.crc_fails) -import unittest - -class TestMicroPyGPS(unittest.TestCase): - - def test_smoke(self): - try: - run_tests() - except: - self.fail("smoke test raised exception") - if __name__ == "__main__": run_tests() diff --git a/gps/astests_pyb.py b/gps/astests_pyb.py index 8891935..efa4a64 100755 --- a/gps/astests_pyb.py +++ b/gps/astests_pyb.py @@ -14,7 +14,7 @@ import uasyncio as asyncio def callback(gps, _, arg): - print('Fix callback. Time:', gps.timestamp, arg) + print('Fix callback. Time:', gps.utc, arg) async def run_tests(): uart = UART(4, 9600, read_buf_len=200) @@ -58,7 +58,7 @@ async def run_tests(): await my_gps.data_received(position=True) print('Longitude:', my_gps.longitude()) print('Latitude', my_gps.latitude()) - print('UTC Timestamp:', my_gps.timestamp) + print('UTC Time:', my_gps.utc) print('Speed:', my_gps.speed()) print('Date Stamp:', my_gps.date) print('Course', my_gps.course) @@ -72,7 +72,7 @@ async def run_tests(): await my_gps.data_received(position=True) print('Longitude:', my_gps.longitude()) print('Latitude', my_gps.latitude()) - print('UTC Timestamp:', my_gps.timestamp) + print('UTC Time:', my_gps.utc) print('Data is Valid:', my_gps._valid) print('') @@ -92,7 +92,7 @@ async def run_tests(): await my_gps.data_received(position=True) print('Longitude', my_gps.longitude()) print('Latitude', my_gps.latitude()) - print('UTC Timestamp:', my_gps.timestamp) + 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) From e28603beb37aa2344f14bae7f9908ecb93fa9247 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 20 May 2018 10:17:27 +0100 Subject: [PATCH 014/472] Prior to branch --- gps/as_GPS.py | 2 +- gps/as_GPS_time.py | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/gps/as_GPS.py b/gps/as_GPS.py index 5d4e17e..6f10e84 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -649,7 +649,7 @@ def speed_string(self, unit=KPH): def time(self, local=True): t = self.local_time if local else self.utc - return '{:02d}:{:02d}:{:2.3f}'.format(*t) + return '{:02d}:{:02d}:{:06.3f}'.format(*t) def date_string(self, formatting=MDY): day, month, year = self.date diff --git a/gps/as_GPS_time.py b/gps/as_GPS_time.py index f4f5653..db5df18 100644 --- a/gps/as_GPS_time.py +++ b/gps/as_GPS_time.py @@ -10,6 +10,10 @@ import as_GPS import as_tGPS +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.') + # Setup for tests. Red LED toggles on fix, blue on PPS interrupt. async def setup(): red = pyb.LED(1) @@ -20,13 +24,20 @@ async def setup(): pps_pin = pyb.Pin('X3', pyb.Pin.IN) return as_tGPS.GPS_Timer(gps, pps_pin, blue) -async def drift_test(gps_tim, minutes): - for _ in range(minutes): - for _ in range(6): - dt = await gps_tim.delta() - print(gps_tim.get_t_split(), end='') - print('Delta {}'.format(dt)) - await asyncio.sleep(10) +running = True + +async def killer(minutes): + global running + await asyncio.sleep(minutes * 60) + running = False + +async def drift_test(gps_tim): + dstart = await gps_tim.delta() + while running: + dt = await gps_tim.delta() + print('{} Delta {}μs'.format(gps_tim.gps.time(), dt)) + await asyncio.sleep(10) + return dt - dstart # Calibrate and set the Pyboard RTC async def do_cal(minutes): @@ -39,9 +50,19 @@ def calibrate(minutes=5): # Every 10s print the difference between GPS time and RTC time async def do_drift(minutes): + print('Setting up GPS.') gps_tim = await setup() - await drift_test(gps_tim, minutes) + print('Waiting for time data.') + await gps_tim.ready() + print('Setting RTC.') + await gps_tim.set_rtc() + print('Measuring drift.') + change = await drift_test(gps_tim) + print('Rate of change {}μs/hr'.format(int(60 * change/minutes))) def drift(minutes=5): + global running + running = True loop = asyncio.get_event_loop() + loop.create_task(killer(minutes)) loop.run_until_complete(do_drift(minutes)) From 62bc0f04cdd8356498833713b26d4ee770c63c04 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 23 May 2018 08:57:52 +0100 Subject: [PATCH 015/472] Prior to subclass branch. --- gps/README.md | 29 ++++++---- gps/as_GPS.py | 108 +++++++++++++++++------------------- gps/as_GPS_time.py | 27 ++++++++- gps/as_tGPS.py | 133 +++++++++++++++++++++------------------------ gps/astests.py | 2 +- gps/astests_pyb.py | 9 +-- 6 files changed, 157 insertions(+), 151 deletions(-) diff --git a/gps/README.md b/gps/README.md index 484ce83..1679c52 100644 --- a/gps/README.md +++ b/gps/README.md @@ -1,3 +1,5 @@ +** WARNING: Under development and subject to change ** + # 1. as_GPS This is an asynchronous device driver for GPS devices which communicate with @@ -216,7 +218,8 @@ gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG) `as_GPS.DMY` returns 'DD/MM/YY'. `as_GPS.LONG` returns a string of form 'January 1st, 2014'. - * `time` No args. Returns the current time in form 'hh:mm:ss'. + * `time_string` Arg `local` default `True`. Returns the current time in form + 'hh:mm:ss.sss'. If `local` is `False` returns UTC time. ## 2.3 Public coroutines @@ -262,7 +265,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. -## 2.4 Public bound variables +## 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` @@ -291,18 +294,18 @@ The following are counts since instantiation. ### 2.4.3 Date and time - * `utc` [hrs: int, mins: int, secs: float] UTC time e.g. [23, 3, 58.0]. Note + * `utc` [hrs: int, mins: int, secs: int] UTC time e.g. [23, 3, 58]. Note that some GPS hardware may only provide integer seconds. The MTK3339 chip - provides a float. - * `local_time` [hrs: int, mins: int, secs: float] Local time. + provides a float whose value is always an integer. + * `local_time` [hrs: int, mins: int, secs: int] Local time. * `date` [day: int, month: int, year: int] e.g. [23, 3, 18] * `local_offset` Local time offset in hrs as specified to constructor. The `utc` bound variable updates on receipt of RMC, GLL or GGA messages. The `date` and `local_time` variables are updated when an RMC message is -received. A local time offset will result in date changes where the time -offset causes the local time to pass midnight. +received. A local time offset will affect the `date` value where the offset +causes the local time to pass midnight. ### 2.4.4 Satellite data @@ -504,13 +507,15 @@ This takes the following arguments: ## 4.3 Public methods -These return immediately. Times are derived from the GPS PPS signal. These -functions should not be called until a valid time/date message and PPS signal -have occurred: await the `ready` coroutine prior to first use. +These return an accurate GPS time of day. As such they return as fast as +possible without error checking: these functions 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. - * `get_secs` No args. Returns a float: the period past midnight in seconds. + * `get_ms` No args. Returns an integer: the period past midnight in ms. This + method does not allocate and may be called in an interrupt context. * `get_t_split` No args. Returns time of day tuple of form - (hrs: int, mins: int, secs: float). + (hrs: int, mins: int, secs: int, μs: int). ## 4.4 Public coroutines diff --git a/gps/as_GPS.py b/gps/as_GPS.py index 6f10e84..38e4ceb 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -62,8 +62,7 @@ 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 @@ -137,12 +136,13 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC ##################### # Data From Sentences - # Time. Ignore http://www.gpsinformation.org/dale/nmea.htm, hardware - # returns a float. - self.utc = [0, 0, 0.0] # [h: int, m: int, s: float] - self.local_time = [0, 0, 0.0] # [h: int, m: int, s: float] - self.date = [0, 0, 0] # [dd: int, mm: int, yy: int] + # 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._rtcbuf = [0]*8 # Buffer for RTC setting + self.epoch_time = 0 # Integer secs since epoch (Y2K under MicroPython) # Position/Motion self._latitude = [0, 0.0, 'N'] # (°, mins, N/S) @@ -202,7 +202,6 @@ def _update(self, line): if not self._crc_check(line, segs[-1]): self.crc_fails += 1 # Update statistics return None - self.clean_sentences += 1 # Sentence is good but unparsed. segs[0] = segs[0][1:] # discard $ segs = segs[:-1] # and checksum @@ -258,47 +257,33 @@ def _fix(self, gps_segments, idx_lat, idx_long): self._fix_time = self._get_time() return True - # Set timestamp. If time/date not present retain last reading (if any). - def _set_timestamp(self, utc_string): - if not utc_string: - return False - # Possible timestamp found - try: - self.utc[0] = int(utc_string[0:2]) # h - self.utc[1] = int(utc_string[2:4]) # mins - self.utc[2] = float(utc_string[4:]) # secs from chip is a float - return True - except ValueError: - return False - for idx in range(3): - self.local_time[idx] = self.utc[idx] - return True - # A local offset may exist so check for date rollover. Local offsets can # include fractions of an hour but not seconds (AFAIK). - def _set_date(self, date_string): - if not date_string: + def _set_date_time(self, utc_string, date_string): + if not date_string or not utc_string: return False try: + hrs = int(utc_string[0:2]) # h + mins = int(utc_string[2:4]) # mins + secs = int(utc_string[4:6]) # secs from chip is a float but FP is always 0 d = int(date_string[0:2]) # day m = int(date_string[2:4]) # month y = int(date_string[4:6]) + 2000 # year - except ValueError: # Bad Date stamp value present + except ValueError: # Bad date or time strings return False - hrs = self.utc[0] - mins = self.utc[1] - secs = self.utc[2] - wday = self._week_day(y, m, d) - 1 - t = self._mktime((y, m, d, hrs, mins, int(secs), wday, 0, 0)) + wday = self._week_day(y, m, d) + t = self._mktime((y, m, d, hrs, mins, int(secs), wday - 1, 0, 0)) + self.epoch_time = t # This is the fundamental datetime reference. + # Need local time for setting Pyboard RTC in interrupt context t += int(3600 * self.local_offset) - y, m, d, hrs, mins, *_ = self._localtime(t) # Preserve float seconds - y -= 2000 - self.local_time[0] = hrs - self.local_time[1] = mins - self.local_time[2] = secs - self.date[0] = d - self.date[1] = m - self.date[2] = y + 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 return True ######################################## @@ -313,10 +298,8 @@ def _set_date(self, date_string): def _gprmc(self, gps_segments): # Parse RMC sentence self._valid &= ~RMC - # UTC Timestamp. - if not self._set_timestamp(gps_segments[1]): - return False # Bad Timestamp value present - if not self._set_date(gps_segments[9]): + # UTC Timestamp and date. + if not self._set_date_time(gps_segments[1], gps_segments[9]): return False # Check Receiver Data Valid Flag @@ -356,12 +339,6 @@ def _gprmc(self, gps_segments): # Parse RMC sentence def _gpgll(self, gps_segments): # Parse GLL sentence self._valid &= ~GLL - # UTC Timestamp. - try: - self._set_timestamp(gps_segments[5]) - except ValueError: # Bad Timestamp value present - return False - # Check Receiver Data Valid Flag if gps_segments[6] != 'A': # Invalid. Don't update data return True # Correctly parsed @@ -391,18 +368,12 @@ def _gpvtg(self, gps_segments): # Parse VTG sentence def _gpgga(self, gps_segments): # Parse GGA sentence self._valid &= ~GGA try: - # UTC Timestamp - self._set_timestamp(gps_segments[1]) - # 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]) - except ValueError: return False @@ -647,9 +618,28 @@ def speed_string(self, unit=KPH): return sform.format(speed, 'knots') return sform.format(speed, 'km/h') - def time(self, local=True): - t = self.local_time if local else self.utc - return '{:02d}:{:02d}:{:06.3f}'.format(*t) + # 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 + int(3600 * self.local_offset) + _, _, _, 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): day, month, year = self.date diff --git a/gps/as_GPS_time.py b/gps/as_GPS_time.py index db5df18..418e8cc 100644 --- a/gps/as_GPS_time.py +++ b/gps/as_GPS_time.py @@ -13,6 +13,7 @@ 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.') # Setup for tests. Red LED toggles on fix, blue on PPS interrupt. async def setup(): @@ -35,7 +36,7 @@ async def drift_test(gps_tim): dstart = await gps_tim.delta() while running: dt = await gps_tim.delta() - print('{} Delta {}μs'.format(gps_tim.gps.time(), dt)) + print('{} Delta {}μs'.format(gps_tim.gps.time_string(), dt)) await asyncio.sleep(10) return dt - dstart @@ -58,7 +59,9 @@ async def do_drift(minutes): await gps_tim.set_rtc() print('Measuring drift.') change = await drift_test(gps_tim) - print('Rate of change {}μs/hr'.format(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)) def drift(minutes=5): global running @@ -66,3 +69,23 @@ def drift(minutes=5): loop = asyncio.get_event_loop() loop.create_task(killer(minutes)) loop.run_until_complete(do_drift(minutes)) + +# Every 10s print the difference between GPS time and RTC time +async def do_time(minutes): + print('Setting up GPS.') + gps_tim = await setup() + print('Waiting for time data.') + await gps_tim.ready() + print('Setting RTC.') + await gps_tim.set_rtc() + while running: + await asyncio.sleep(1) + hrs, mins, secs, us = gps_tim.get_t_split() + print('{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}'.format(gps_tim.get_ms(), hrs, mins, secs, us)) + +def time(minutes=1): + global running + running = True + loop = asyncio.get_event_loop() + loop.create_task(killer(minutes)) + loop.run_until_complete(do_time(minutes)) diff --git a/gps/as_tGPS.py b/gps/as_tGPS.py index 9c65891..66c20d7 100644 --- a/gps/as_tGPS.py +++ b/gps/as_tGPS.py @@ -5,10 +5,10 @@ # Released under the MIT License (MIT) - see LICENSE file import uasyncio as asyncio -import math import pyb import utime import micropython +import gc import as_GPS micropython.alloc_emergency_exception_buf(100) @@ -26,7 +26,7 @@ def __init__(self, gps, pps_pin, led=None): self.led = led self.secs = None # Integer time since midnight at last PPS self.acquired = None # Value of ticks_us at edge of PPS - self.rtc_set = None # Data for setting RTC + self._rtc_set = False # Set RTC flag loop = asyncio.get_event_loop() loop.create_task(self._start(pps_pin)) @@ -35,64 +35,36 @@ async def _start(self, pps_pin): pyb.ExtInt(pps_pin, pyb.ExtInt.IRQ_RISING, pyb.Pin.PULL_NONE, self._isr) def _isr(self, _): - self.acquired = utime.ticks_us() # Save time of PPS - if self.rtc_set is not None: - rtc.datetime(self.rtc_set) - self.rtc_set = None - # Time in last NMEA sentence - t = self.gps.local_time - # secs is rounded down to an int: exact time of last PPS. + acquired = utime.ticks_us() # Save time of PPS + # Time in last NMEA sentence was time of last PPS. + # Reduce to secs since midnight local time. + secs = (self.gps.epoch_time + int(3600*self.gps.local_offset)) % 86400 # This PPS is one second later - self.secs = 3600*t[0] + 60*t[1] + int(t[2]) + 1 + secs += 1 + if secs >= 86400: # Next PPS will deal with rollover + return + self.secs = secs + self.acquired = acquired + if self._rtc_set: + # Time in last NMEA sentence. Earlier test ensures no rollover. + self.gps._rtcbuf[6] = secs % 60 + rtc.datetime(self.gps._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? if self.led is not None: self.led.toggle() - # Return accurate GPS time in seconds (float) since midnight - def get_secs(self): - t = self.secs - if t != self.secs: # An interrupt has occurred - # Re-read to ensure .acquired is correct. No need to get clever - t = self.secs # here as IRQ's are at only 1Hz - return t + utime.ticks_diff(utime.ticks_us(), self.acquired) / 1000000 - - # Return GPS time as hrs: int, mins: int, secs: int, fractional_secs: float - def _get_hms(self): - t = math.modf(self.get_secs()) - x, secs = divmod(int(t[1]), 60) - hrs, mins = divmod(x, 60) - return hrs, mins, secs, t[0] - - # Return accurate GPS time of day (hrs , mins , secs) - def get_t_split(self): - hrs, mins, secs, frac_secs = self._get_hms() - return hrs, mins, secs + frac_secs - # Subsecs register is read-only. So need to set RTC on PPS leading edge. - # Calculate time of next edge, save the new RTC time, and let ISR update - # the RTC. + # Set flag and let ISR set the RTC. Pause until done. async def set_rtc(self): - d, m, y = self.gps.date - y += 2000 - t0 = self.acquired - while self.acquired == t0: # Busy-wait on PPS interrupt - await asyncio.sleep_ms(0) - hrs, mins, secs = self.gps.local_time - secs = int(secs) + 2 # Time of next PPS - self.rtc_set = [y, m, d, self.gps._week_day(y, m, d), hrs, mins, secs, 0] -# rtc.datetime((y, m, d, self.gps._week_day(y, m, d), hrs, mins, secs, 0)) - - # Time from GPS: integer μs since Y2K. Call after awaiting PPS: result is - # time when PPS leading edge occurred so fractional secs discarded. - def _get_gps_usecs(self): - d, m, y = self.gps.date - y += 2000 - hrs, mins, secs, _ = self._get_hms() - tim = utime.mktime((y, m, d, hrs, mins, secs, self.gps._week_day(y, m, d) - 1, 0)) - return tim * 1000000 - - # Value of RTC time at current instant. Units μs since Y2K. Tests OK. + 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)) @@ -101,9 +73,8 @@ def _get_rtc_usecs(self): # Return no. of μs RTC leads GPS. Done by comparing times at the instant of # PPS leading edge. async def delta(self): - rtc_time = await self._await_pps() # μs since Y2K at time of latest PPS - gps_time = self._get_gps_usecs() # μs since Y2K at previous PPS - return rtc_time - gps_time + 1000000 # so add 1s + 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 @@ -111,13 +82,18 @@ async def delta(self): async def _await_pps(self): t0 = self.acquired while self.acquired == t0: # Busy-wait on PPS interrupt - await asyncio.sleep_ms(0) + await asyncio.sleep_ms(0) # Interrupts here should be OK as ISR stored acquisition time + gc.collect() + # DISABLING INTS INCREASES UNCERTAINTY. Interferes with ticks_us (proved by test). +# istate = pyb.disable_irq() # But want to accurately time RTC change 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) - t = self._get_rtc_usecs() # Read RTC now - return t - dt + trtc = self._get_rtc_usecs() - dt # Read RTC now and adjust for PPS edge + tgps = 1000000 * (self.gps.epoch_time + 3600*self.gps.local_offset + 1) +# pyb.enable_irq(istate) # Have critical timings now + return trtc, tgps # Non-realtime calculation of calibration factor. times are in μs def _calculate(self, gps_start, gps_end, rtc_start, rtc_end): @@ -140,24 +116,16 @@ async def _getcal(self, minutes=5): 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 (μs since Y2K). - rtc_start = await self._await_pps() - # GPS start time in μs since Y2K: correct at the time of PPS edge. - gps_start = self._get_gps_usecs() + # 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 = await self._await_pps() - gps_end = self._get_gps_usecs() + rtc_end, gps_end = await self._await_pps() cal = self._calculate(gps_start, gps_end, rtc_start, rtc_end) - if abs(cal) > 2000: # Still occasionally occurs - rtc_start = rtc_end - gps_start = gps_end - cal = 0 - print('Restarting calibration.') - else: - 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) @@ -181,3 +149,26 @@ async def calibrate(self, minutes=5): 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 = pyb.disable_irq() + t = self.secs + acquired = self.acquired + pyb.enable_irq(state) + return 1000*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 subsequent ISR will see hms = 0, 0, 1 and a value of + # .acquired > 1000000. + def get_t_split(self): + secs, acquired = self.secs, self.acquired # Single LOC is not interruptable + x, secs = divmod(secs, 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. + return hrs, mins, secs + ds, us diff --git a/gps/astests.py b/gps/astests.py index 2204f68..c8bc533 100755 --- a/gps/astests.py +++ b/gps/astests.py @@ -156,7 +156,7 @@ def run_tests(): 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()) + print('Time:', my_gps.time_string()) print() print('### Final Results ###') diff --git a/gps/astests_pyb.py b/gps/astests_pyb.py index efa4a64..5846fe9 100755 --- a/gps/astests_pyb.py +++ b/gps/astests_pyb.py @@ -22,10 +22,7 @@ async def run_tests(): sreader = asyncio.StreamReader(uart) 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', + 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'] @@ -55,7 +52,7 @@ async def run_tests(): for sentence in test_RMC: sentence_count += 1 await swriter.awrite(sentence) - await my_gps.data_received(position=True) + await my_gps.data_received(date=True) print('Longitude:', my_gps.longitude()) print('Latitude', my_gps.latitude()) print('UTC Time:', my_gps.utc) @@ -139,7 +136,7 @@ async def run_tests(): 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()) + print('Time:', my_gps.time_string()) print() print('### Final Results ###') From 7b3028f13a61930ba9a0fcfe85004942af9d8efb Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 23 May 2018 15:18:53 +0100 Subject: [PATCH 016/472] Subclass mod complete. --- gps/README.md | 105 ++++++++++++++++++++++++++++++++------------- gps/as_GPS.py | 56 ++++-------------------- gps/as_GPS_time.py | 7 ++- gps/as_tGPS.py | 58 +++++++++++++++++++------ 4 files changed, 131 insertions(+), 95 deletions(-) diff --git a/gps/README.md b/gps/README.md index 1679c52..a25f1e3 100644 --- a/gps/README.md +++ b/gps/README.md @@ -1,10 +1,11 @@ -** WARNING: Under development and subject to change ** +**NOTE: Under development. API may be subject to change** # 1. as_GPS This is an asynchronous device driver for GPS devices which communicate with the driver via a UART. GPS NMEA sentence parsing is based on this excellent -library [micropyGPS]. +library [micropyGPS]. It was adapted for asynchronous use; also to reduce RAM +use and frequency of allocations and to correctly process local time values. The driver is designed to be extended by subclassing, for example to support additional sentence types. It is compatible with Python 3.5 or later and also @@ -12,12 +13,13 @@ with [MicroPython]. 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 -sentences on startup. An optional read-write driver is provided for +sentences on startup. The read-only driver is designed for use on resource +constrained hosts. An optional read-write subclass is provided for MTK3329/MTK3339 chips as used on the above board. This enables the device configuration to be altered. -A further driver, for the Pyboard and other boards based on STM processors, -provides for using the GPS device for precision timing. The chip's RTC may be +Further subclasses, for the Pyboard and other boards based on STM processors, +provide for using the GPS device for precision timing. The chip's RTC may be precisely set and calibrated using the PPS signal from the GPS chip. ###### [Main README](../README.md) @@ -78,10 +80,13 @@ 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. + +Special tests: * `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 @@ -95,11 +100,12 @@ Additional files relevant to the read/write driver are listed ### 1.4.1 Micropython -To install on "bare metal" hardware such as the Pyboard copy the file -`as_GPS.py` onto the device's filesystem and ensure that `uasyncio` is -installed. The code has been tested on the Pyboard with `uasyncio` V2 and the -Adafruit [Ultimate GPS Breakout] module. If memory errors are encountered on -resource constrained devices install as a [frozen module]. +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. For the @@ -108,9 +114,9 @@ should also be copied across. ### 1.4.2 Python 3.5 or later -On platforms with an underlying OS such as the Raspberry Pi ensure that -`as_GPS.py` (and optionally `as_rwGPS.py`) is on the Python path and that the -Python version is 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 @@ -124,7 +130,7 @@ 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 3.2](./README.md#231-data-validity). This ensures current data. + [section 2.3.1](./README.md#231-data-validity). This ensures current data. ## 2.1 Constructor @@ -138,7 +144,9 @@ Optional positional args: Default `RMC`: the callback will occur on RMC messages only (see below). * `fix_cb_args` A tuple of args for the callback (default `()`). -Note: +Notes: +`local_offset` correctly alters the date where 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 @@ -206,20 +214,21 @@ gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG) 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' + 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` Arg `local` default `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'. - - * `time_string` Arg `local` default `True`. Returns the current time in form - 'hh:mm:ss.sss'. If `local` is `False` returns UTC time. + Note that this requires the file `as_GPS_utils.py`. ## 2.3 Public coroutines @@ -280,7 +289,7 @@ The sentence type which updates a value is shown in brackets e.g. (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 and it will always read zero. + produce this data: it will always read zero. ### 2.4.2 Statistics and status @@ -490,18 +499,47 @@ Other `PMTK` messages are passed to the optional message callback as described 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 time reference. It may -be used to set and to calibrate the Pyboard realtime clock (RTC). +be used to set and to calibrate the Pyboard realtime clock (RTC). Note that +these drivers are for STM based targets only (at least until the `machine` +library supports an `RTC` class). ## 4.1 Files - * `as_tGPS.py` The library. Supports the `GPS_Timer` class. + * `as_tGPS.py` The library. Provides `GPS_Timer` and `GPS_RWTimer` classes. * `as_GPS_time.py` Test scripts for above. -## 4.2 GPS_Timer class Constructor +## 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 -This takes the following arguments: - * `gps` An instance of the `AS_GPS` (read-only) or `GPS` (read/write) classes. +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 `AS_GPS` details for these args. + * `fix_cb` + * `cb_mask` + * `fix_cb_args` + * `led` Default `None`. If an `LED` instance is passed, this will toggle each + time a PPS interrupt is handled. + +### 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 `GPS` details. + * `fix_cb` + * `cb_mask` + * `fix_cb_args` + * `msg_cb` + * `msg_cb_args` * `led` Default `None`. If an `LED` instance is passed, this will toggle each time a PPS interrupt is handled. @@ -538,6 +576,10 @@ 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 (and hence calibration factor) is temperature +dependent. For the most accurate possible results allow the Pyboard to reach +working temperature before calibrating. + # 5. Supported Sentences * GPRMC GP indicates NMEA sentence @@ -580,11 +622,14 @@ 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 must be increased or the satellite information messages should be disabled. -The PPS signal (not used by this driver) on the MTK3339 occurs only when a fix -has been achieved. The leading edge always occurs before a set of messages are -output. So, if the leading edge is to be used for precise timing, 1s should be -added to the `timestamp` value (beware of possible rollover into minutes and -hours). +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 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. [MicroPython]:https://micropython.org/ [frozen module]:https://learn.adafruit.com/micropython-basics-loading-modules/frozen-modules diff --git a/gps/as_GPS.py b/gps/as_GPS.py index 38e4ceb..3592b55 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -52,11 +52,6 @@ class AS_GPS(object): _SENTENCE_LIMIT = 76 # Max sentence length (based on GGA sentence) _NO_FIX = 1 - _DIRECTIONS = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', - 'WNW', 'NW', 'NNW') - _MONTHS = ('January', 'February', 'March', 'April', 'May', - 'June', 'July', 'August', 'September', 'October', - 'November', 'December') # 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 @@ -141,7 +136,6 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC # 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._rtcbuf = [0]*8 # Buffer for RTC setting self.epoch_time = 0 # Integer secs since epoch (Y2K under MicroPython) # Position/Motion @@ -257,6 +251,9 @@ def _fix(self, gps_segments, idx_lat, idx_long): self._fix_time = self._get_time() return True + 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). def _set_date_time(self, utc_string, date_string): @@ -274,16 +271,7 @@ def _set_date_time(self, utc_string, date_string): wday = self._week_day(y, m, d) t = self._mktime((y, m, d, hrs, mins, int(secs), wday - 1, 0, 0)) self.epoch_time = t # This is the fundamental datetime reference. - # Need local time for setting Pyboard RTC in interrupt context - t += 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 + self._dtset(wday) # Subclass may override return True ######################################## @@ -581,13 +569,8 @@ def time_since_fix(self): # ms since last valid fix return self._time_diff(self._get_time(), self._fix_time) def compass_direction(self): # Return cardinal point as string. - # Calculate the offset for a rotated compass - if self.course >= 348.75: - offset_course = 360 - self.course - else: - offset_course = self.course + 11.25 - # Each compass point is separated by 22.5°, divide to find lookup value - return self._DIRECTIONS[int(offset_course // 22.5)] + from as_GPS_utils import compass_direction + return compass_direction(self) def latitude_string(self, coord_format=DM): if coord_format == DD: @@ -633,7 +616,7 @@ def date(self): @property def utc(self): - t = self.epoch_time + int(3600 * self.local_offset) + t = self.epoch_time _, _, _, hrs, mins, secs, *_ = self._localtime(t) return hrs, mins, secs @@ -642,26 +625,5 @@ def time_string(self, local=True): return '{:02d}:{:02d}:{:02d}'.format(hrs, mins, secs) def date_string(self, formatting=MDY): - day, month, year = self.date - # Long Format January 1st, 2014 - if formatting == LONG: - dform = '{:s} {:2d}{:s}, 20{:2d}' - # Retrieve Month string from private set - month = self._MONTHS[month - 1] - # Determine Date Suffix - if day in (1, 21, 31): - suffix = 'st' - elif day in (2, 22): - suffix = 'nd' - elif day == 3: - 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.') + from as_GPS_utils import date_string + return date_string(self, formatting) diff --git a/gps/as_GPS_time.py b/gps/as_GPS_time.py index 418e8cc..1f30002 100644 --- a/gps/as_GPS_time.py +++ b/gps/as_GPS_time.py @@ -7,7 +7,6 @@ import uasyncio as asyncio import pyb -import as_GPS import as_tGPS print('Available tests:') @@ -21,9 +20,9 @@ async def setup(): blue = pyb.LED(4) uart = pyb.UART(4, 9600, read_buf_len=200) sreader = asyncio.StreamReader(uart) - gps = as_GPS.AS_GPS(sreader, local_offset=1, fix_cb=lambda *_: red.toggle()) pps_pin = pyb.Pin('X3', pyb.Pin.IN) - return as_tGPS.GPS_Timer(gps, pps_pin, blue) + return as_tGPS.GPS_Timer(sreader, pps_pin, local_offset=1, + fix_cb=lambda *_: red.toggle(), led=blue) running = True @@ -36,7 +35,7 @@ async def drift_test(gps_tim): dstart = await gps_tim.delta() while running: dt = await gps_tim.delta() - print('{} Delta {}μs'.format(gps_tim.gps.time_string(), dt)) + print('{} Delta {}μs'.format(gps_tim.time_string(), dt)) await asyncio.sleep(10) return dt - dstart diff --git a/gps/as_tGPS.py b/gps/as_tGPS.py index 66c20d7..d869029 100644 --- a/gps/as_tGPS.py +++ b/gps/as_tGPS.py @@ -1,5 +1,6 @@ # 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 @@ -10,6 +11,7 @@ import micropython import gc import as_GPS +import as_rwGPS micropython.alloc_emergency_exception_buf(100) @@ -20,25 +22,40 @@ def rtc_secs(): dt = rtc.datetime() return 3600*dt[4] + 60*dt[5] + dt[6] + (255 - dt[7])/256 -class GPS_Timer(): - def __init__(self, gps, pps_pin, led=None): - self.gps = gps +# 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=(), + led = None): + as_GPS.AS_GPS.__init__(self, sreader, local_offset, fix_cb, cb_mask, fix_cb_args) + self.setup(pps_pin, led) + +# 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=(), led = None): + 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, led) + +class GPS_Tbase(): + def setup(self, pps_pin, led=None): self.led = led self.secs = None # Integer time since midnight at last PPS 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 loop = asyncio.get_event_loop() loop.create_task(self._start(pps_pin)) async def _start(self, pps_pin): - await self.gps.data_received(date=True) + await self.data_received(date=True) pyb.ExtInt(pps_pin, pyb.ExtInt.IRQ_RISING, pyb.Pin.PULL_NONE, self._isr) def _isr(self, _): acquired = utime.ticks_us() # Save time of PPS # Time in last NMEA sentence was time of last PPS. # Reduce to secs since midnight local time. - secs = (self.gps.epoch_time + int(3600*self.gps.local_offset)) % 86400 + secs = (self.epoch_time + int(3600*self.local_offset)) % 86400 # This PPS is one second later secs += 1 if secs >= 86400: # Next PPS will deal with rollover @@ -47,14 +64,27 @@ def _isr(self, _): self.acquired = acquired if self._rtc_set: # Time in last NMEA sentence. Earlier test ensures no rollover. - self.gps._rtcbuf[6] = secs % 60 - rtc.datetime(self.gps._rtcbuf) + self._rtcbuf[6] = secs % 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? if self.led is not None: self.led.toggle() + # 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): @@ -81,18 +111,15 @@ async def delta(self): # (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 - await asyncio.sleep_ms(0) # Interrupts here should be OK as ISR stored acquisition time - gc.collect() - # DISABLING INTS INCREASES UNCERTAINTY. Interferes with ticks_us (proved by test). -# istate = pyb.disable_irq() # But want to accurately time RTC change + 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.gps.epoch_time + 3600*self.gps.local_offset + 1) -# pyb.enable_irq(istate) # Have critical timings now + tgps = 1000000 * (self.epoch_time + 3600*self.local_offset + 1) return trtc, tgps # Non-realtime calculation of calibration factor. times are in μs @@ -172,3 +199,6 @@ def get_t_split(self): ds, us = divmod(dt, 1000000) # If dt > 1e6 can add to secs without risk of rollover: see above. return hrs, mins, secs + ds, us + +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}) From 0842e3bcb1a7a441cc15bfc40b131364e0f07bad Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 26 May 2018 12:10:46 +0100 Subject: [PATCH 017/472] GPS driver release 0.1. --- exit_gate_test.py | 30 ----- gps/README.md | 323 +++++++++++++++++++++++++++++++++++---------- gps/as_GPS_time.py | 129 +++++++++++++----- gps/as_tGPS.py | 54 +++++--- gps/ast_pbrw.py | 4 +- 5 files changed, 394 insertions(+), 146 deletions(-) delete mode 100644 exit_gate_test.py diff --git a/exit_gate_test.py b/exit_gate_test.py deleted file mode 100644 index c30ad31..0000000 --- a/exit_gate_test.py +++ /dev/null @@ -1,30 +0,0 @@ -# exit_gate_test.py Test/demo of the ExitGate class -# Author: Peter Hinch -# Copyright Peter Hinch 2017 Released under the MIT license -import uasyncio as asyncio -from utime import ticks_ms, ticks_diff -from asyn import ExitGate - -async def bar(exit_gate, t): - async with exit_gate: - result = 'normal' if await exit_gate.sleep(t) else 'abort' - tim = ticks_diff(ticks_ms(), tstart) / 1000 - print('{:5.2f} bar() with time value {} completed. Result {}.'.format(tim, t, result)) - -async def foo(): - exit_gate = ExitGate() - loop = asyncio.get_event_loop() - for t in range(1, 10): - loop.create_task(bar(exit_gate, t)) - print('Task queue length = ', len(loop.q)) - await asyncio.sleep(3) - print('Task queue length = ', len(loop.q)) - print('Now foo is causing tasks to terminate.') - await exit_gate - print('foo() complete.') - print('Task queue length = ', len(loop.q)) - - -tstart = ticks_ms() -loop = asyncio.get_event_loop() -loop.run_until_complete(foo()) diff --git a/gps/README.md b/gps/README.md index a25f1e3..0e687f1 100644 --- a/gps/README.md +++ b/gps/README.md @@ -2,25 +2,47 @@ # 1. as_GPS -This is an asynchronous device driver for GPS devices which communicate with -the driver via a UART. GPS NMEA sentence parsing is based on this excellent -library [micropyGPS]. It was adapted for asynchronous use; also to reduce RAM -use and frequency of allocations and to correctly process local time values. - -The driver is designed to be extended by subclassing, for example to support -additional sentence types. It is compatible with Python 3.5 or later and also -with [MicroPython]. 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 -sentences on startup. The read-only driver is designed for use on resource -constrained hosts. An optional read-write subclass is provided for -MTK3329/MTK3339 chips as used on the above board. This enables the device -configuration to be altered. - -Further subclasses, for the Pyboard and other boards based on STM processors, -provide for using the GPS device for precision timing. The chip's RTC may be -precisely set and calibrated using the PPS signal from the GPS chip. +This repository offers a suite of asynchronous device drivers for GPS devices +which communicate with the host via a UART. GPS NMEA 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. + * 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] targets based on STM chips (e.g. the [Pyboad]) + extend the capabilities of the read-only and read-write drivers to provide + precision timing. The RTC may be calibrated and set 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 sentences on startup. + +## 1.2 Comparison with [micropyGPS] + +NMEA sentence parsing is based on [micropyGPS] but with significant changes. + + * As asynchronous drivers they require `uasyncio` on [MicroPython]. They use + 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 reduced by various techniques: this reduces heap + fragmentation, improving 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. + * Hooks are provided for user-designed subclassing, for example to parse + additional message types. ###### [Main README](../README.md) @@ -52,10 +74,11 @@ PPS connection is required only if using the device for precise timing ## 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 and therefore assumes that power has been applied -long enough for the GPS to have started to acquire data. +The test runs for 60 seconds once data has been received. ```python import uasyncio as asyncio @@ -66,14 +89,38 @@ def callback(gps, *_): # Runs for each valid fix uart = UART(4, 9600) sreader = asyncio.StreamReader(uart) # Create a StreamReader -my_gps = as_GPS.AS_GPS(sreader, fix_cb=callback) # Instantiate GPS +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. @@ -86,14 +133,12 @@ The following are relevant to the default read-only driver. * `log_kml.py` A simple demo which logs a route travelled to a .kml file which may be displayed on Google Earth. -Special tests: - * `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`. +On RAM-constrained devices `as_GPS_utils.py` may be omitted in which case the +`date_string` and `compass_direction` methods will be unavailable. -Additional files relevant to the read/write driver are listed -[here](./README.md#31-files). Files for the timing driver are listed +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 @@ -108,9 +153,12 @@ 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. For the -[timing driver](./README.md#4-using-gps-for-accurate-timing) `as_tGPS.py` -should also be copied across. +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 @@ -361,6 +409,8 @@ packets to GPS modules based on the MTK3329/MTK3339 chip. These include: * `as_rwGPS.py` Supports the `GPS` class. This subclass of `AS_GPS` enables writing a limited subset of the MTK commands used on many popular devices. + * `as_GPS.py` The library containing the base class. + * `as_GPS_utils.py` Additional formatted string methods. * `ast_pbrw.py` Test script which changes various attributes. This will pause until a fix has been achieved. After that changes are made for about 1 minute, then it runs indefinitely reporting data at the REPL and on the LEDs. It may @@ -371,6 +421,32 @@ packets to GPS modules based on the MTK3329/MTK3339 chip. These include: * 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: @@ -490,7 +566,8 @@ The default `parse` method is redefined. 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 +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-constructor). @@ -499,15 +576,56 @@ Other `PMTK` messages are passed to the optional message callback as described 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 time reference. It may -be used to set and to calibrate the Pyboard realtime clock (RTC). Note that -these drivers are for STM based targets only (at least until the `machine` -library supports an `RTC` class). +be used to set and to calibrate the Pyboard realtime clock (RTC). These drivers +are for MicroPython only. The RTC functionality is for STM chips (e.g. Pyboard) +only (at least until the `machine` library supports an `RTC` class). + +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 above. +### 4.1.1 Usage xxample + +```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 @@ -524,8 +642,12 @@ Optional positional args: * `fix_cb` * `cb_mask` * `fix_cb_args` - * `led` Default `None`. If an `LED` instance is passed, this will toggle each - time a PPS interrupt is handled. + * `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 @@ -540,36 +662,45 @@ Optional positional args: * `fix_cb_args` * `msg_cb` * `msg_cb_args` - * `led` Default `None`. If an `LED` instance is passed, this will toggle each - time a PPS interrupt is handled. + * `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.3 Public methods These return an accurate GPS time of day. As such they return as fast as -possible without error checking: these functions 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. +possible. To achieve this they avoid allocation and dispense with error +checking: these functions 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. A usage example is in the time +demo in `as_GPS_time.py`. - * `get_ms` No args. Returns an integer: the period past midnight in ms. This - method does not allocate and may be called in an interrupt context. - * `get_t_split` No args. Returns time of day tuple of form - (hrs: int, mins: int, secs: int, μs: int). + * `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]`. + +See [Absolute accuracy](./README.md#45-absolute-accuracy) for a discussion of +the accuracy of these methods. ## 4.4 Public coroutines * `ready` No args. Pauses until a valid time/date message and PPS signal have occurred. - * `set_rtc` No args. Sets the Pyboard RTC to GPS time. Coro 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 - Pyboard RTC and returns the calibration factor for it. This 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 1 - digit (or the spcified time has elapsed) it terminates. Typical run times are - on the order of two miutes. + * `set_rtc` No args. STM hosts only. Sets the Pyboard RTC to GPS time. Coro + pauses for up to 1s as it waits for a PPS pulse. + * `delta` No args. STM hosts only. Returns no. of μs RTC leads GPS. Coro + pauses for up to 1s. + * `calibrate` Arg: integer, no. of minutes to run default 5. STM hosts only. + Calibrates the Pyboard RTC and returns the calibration factor for it. This + 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 @@ -580,11 +711,52 @@ Crystal oscillator frequency (and hence calibration factor) is temperature dependent. For the most accurate possible results allow the Pyboard 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 ++-70μs (standard deviation). + +Without an atomic clock synchronised to a Tier 1 NTP server this 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. + +Sources of error are: + * Variations in interrupt latency. + * Inaccuracy in the `ticks_us` timer. + * Latency in the function used to retrieve the time. + +The test program `as_GPS_time.py` has a test `usecs` which aims to assess the +first two. It repeatedly uses `ticks_us` to measure the time between PPS pulses +over a minute then calculates some simple statistics on the result. + +## 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 + the absolute accuracy of the `get_t_split` method. + # 5. Supported Sentences - * GPRMC GP indicates NMEA sentence - * GLRMC GL indicates GLONASS (Russian system) - * GNRMC GN GNSS (Global Navigation Satellite System) + * GPRMC GP indicates NMEA sentence (US GPS system). + * GLRMC GL indicates GLONASS (Russian system). + * GNRMC GN GNSS (Global Navigation Satellite System). * GPGLL * GLGLL * GPGGA @@ -598,14 +770,18 @@ working temperature before calibrating. * GPGSV * GLGSV -# 6. Subclassing +# 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`. -An example of this may be found in the `as_rwGPS.py` module. +An example of overriding this method may be found in the `as_rwGPS.py` module. A subclass may redefine this to attempt to parse such sentences. The method receives an arg `segs` being a list of strings. These are the parts of the @@ -615,12 +791,25 @@ the leading '$' character removed. It should return `True` if the sentence was successfully parsed, otherwise `False`. +Where a sentence is successfully parsed, a null `reparse` method is called. +This may be overridden in a subclass. + +## 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 baudrate of 9600 I measured a time of 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 must -be increased or the satellite information messages should be disabled. +At the default baudrate of 9600 I measured a transmission time of 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 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 diff --git a/gps/as_GPS_time.py b/gps/as_GPS_time.py index 1f30002..665a8d4 100644 --- a/gps/as_GPS_time.py +++ b/gps/as_GPS_time.py @@ -1,45 +1,46 @@ # as_GPS_time.py Test scripts for as_tGPS # 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(4, 9600, read_buf_len=200) + uart = pyb.UART(UART_ID, 9600, read_buf_len=200) sreader = asyncio.StreamReader(uart) - pps_pin = pyb.Pin('X3', pyb.Pin.IN) + 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(), led=blue) - -running = True + fix_cb=lambda *_: red.toggle(), + pps_cb=lambda *_: blue.toggle()) -async def killer(minutes): - global running +# Test terminator: task sets the passed event after the passed time. +async def killer(end_event, minutes): await asyncio.sleep(minutes * 60) - running = False + end_event.set() -async def drift_test(gps_tim): - dstart = await gps_tim.delta() - while running: - dt = await gps_tim.delta() - print('{} Delta {}μs'.format(gps_tim.time_string(), dt)) - await asyncio.sleep(10) - return dt - dstart - -# Calibrate and set the Pyboard RTC +# ******** Calibrate and set the Pyboard RTC ******** async def do_cal(minutes): gps_tim = await setup() await gps_tim.calibrate(minutes) @@ -48,8 +49,17 @@ 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 do_drift(minutes): +async def drift_test(terminate, gps_tim): + dstart = await gps_tim.delta() + while not terminate.is_set(): + dt = await gps_tim.delta() + print('{} Delta {}μs'.format(gps_tim.time_string(), dt)) + await asyncio.sleep(10) + return dt - dstart + +async def do_drift(terminate, minutes): print('Setting up GPS.') gps_tim = await setup() print('Waiting for time data.') @@ -57,34 +67,91 @@ async def do_drift(minutes): print('Setting RTC.') await gps_tim.set_rtc() print('Measuring drift.') - change = await drift_test(gps_tim) + change = await drift_test(terminate, gps_tim) 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): - global running - running = True + terminate = asyn.Event() loop = asyncio.get_event_loop() - loop.create_task(killer(minutes)) - loop.run_until_complete(do_drift(minutes)) + loop.create_task(killer(terminate, minutes)) + loop.run_until_complete(do_drift(terminate, minutes)) +# ******** Time printing demo ******** # Every 10s print the difference between GPS time and RTC time -async def do_time(minutes): +async def do_time(terminate): + fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}' print('Setting up GPS.') gps_tim = await setup() print('Waiting for time data.') await gps_tim.ready() print('Setting RTC.') await gps_tim.set_rtc() - while running: + while not terminate.is_set(): await asyncio.sleep(1) - hrs, mins, secs, us = gps_tim.get_t_split() - print('{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}'.format(gps_tim.get_ms(), hrs, mins, secs, us)) + # 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])) def time(minutes=1): - global running - running = True + terminate = asyn.Event() + loop = asyncio.get_event_loop() + loop.create_task(killer(terminate, minutes)) + loop.run_until_complete(do_time(terminate)) + +# ******** Measure accracy of μs clock ******** +# Callback occurs in interrupt context +us_acquired = None +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): + 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(terminate): + tick = asyn.Event() + print('Setting up GPS.') + gps_tim = await us_setup(tick) + print('Waiting for time data.') + await gps_tim.ready() + max_us = 0 + min_us = 0 + sd = 0 + nsamples = 0 + count = 0 + while not terminate.is_set(): + await tick + usecs = tick.value() + tick.clear() + err = 1000000 - usecs + count += 1 + print('Error {: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('Error: {:5d}μs max {:5d}μs min. Standard deviation {:4d}μs'.format(max_us, min_us, sd)) + +def usec(minutes=1): + terminate = asyn.Event() loop = asyncio.get_event_loop() - loop.create_task(killer(minutes)) - loop.run_until_complete(do_time(minutes)) + loop.create_task(killer(terminate, minutes)) + loop.run_until_complete(do_usec(terminate)) diff --git a/gps/as_tGPS.py b/gps/as_tGPS.py index d869029..5f0c93c 100644 --- a/gps/as_tGPS.py +++ b/gps/as_tGPS.py @@ -4,9 +4,16 @@ # 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 pyb +import machine +try: + import pyb + on_pyboard = True + rtc = pyb.RTC() +except ImportError: + on_pyboard = False import utime import micropython import gc @@ -15,41 +22,44 @@ micropython.alloc_emergency_exception_buf(100) -rtc = pyb.RTC() - # 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=(), - led = None): + 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, led) + 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=(), led = None): + 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, led) + self.setup(pps_pin, pps_cb, pps_cb_args) class GPS_Tbase(): - def setup(self, pps_pin, led=None): - self.led = led + def setup(self, pps_pin, pps_cb, pps_cb_args): + self._pps_cb = pps_cb + self._pps_cb_args = pps_cb_args self.secs = None # Integer time since midnight at last PPS 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(pps_pin)) async def _start(self, pps_pin): await self.data_received(date=True) - pyb.ExtInt(pps_pin, pyb.ExtInt.IRQ_RISING, pyb.Pin.PULL_NONE, self._isr) + pps_pin.irq(self._isr, trigger = machine.Pin.IRQ_RISING) def _isr(self, _): acquired = utime.ticks_us() # Save time of PPS @@ -69,8 +79,7 @@ def _isr(self, _): 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? - if self.led is not None: - self.led.toggle() + 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 @@ -88,6 +97,8 @@ def _dtset(self, wday): # 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) @@ -103,6 +114,8 @@ def _get_rtc_usecs(self): # 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 @@ -167,6 +180,8 @@ async def ready(self): 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)) @@ -181,10 +196,10 @@ async def calibrate(self, minutes=5): # Return GPS time in ms since midnight (small int on 32 bit h/w). # No allocation. def get_ms(self): - state = pyb.disable_irq() + state = machine.disable_irq() t = self.secs acquired = self.acquired - pyb.enable_irq(state) + machine.enable_irq(state) return 1000*t + utime.ticks_diff(utime.ticks_us(), acquired) // 1000 # Return accurate GPS time of day (hrs: int, mins: int, secs: int, μs: int) @@ -192,13 +207,20 @@ def get_ms(self): # RMC handles this, so subsequent ISR will see hms = 0, 0, 1 and a value of # .acquired > 1000000. def get_t_split(self): - secs, acquired = self.secs, self.acquired # Single LOC is not interruptable + state = machine.disable_irq() + t = self.secs + acquired = self.acquired + machine.enable_irq(state) x, secs = divmod(secs, 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. - return hrs, mins, secs + ds, us + self._time[0] = hrs + self._time[1] = mins + self._time[2] = secs + ds + self._time[3] = us + 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/gps/ast_pbrw.py b/gps/ast_pbrw.py index e6692d0..3512e59 100644 --- a/gps/ast_pbrw.py +++ b/gps/ast_pbrw.py @@ -27,7 +27,7 @@ def callback(gps, _, timer): green.on() timer.trigger(10000) # Outage is declared after 10s -def timeout(): +def cb_timeout(): global ntimeouts green.off() ntimeouts += 1 @@ -135,7 +135,7 @@ async def gps_test(): # read_buf_len is precautionary: code runs reliably without it. sreader = asyncio.StreamReader(uart) swriter = asyncio.StreamWriter(uart, {}) - timer = aswitch.Delay_ms(timeout) + 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) From bc54c3f39fadfc04da5e1ab7a05ef681fa913093 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 27 May 2018 07:08:29 +0100 Subject: [PATCH 018/472] gps/README improvements part complete. --- gps/README.md | 154 ++++++++++++++++++++++++++------------------------ 1 file changed, 81 insertions(+), 73 deletions(-) diff --git a/gps/README.md b/gps/README.md index 0e687f1..c061dc0 100644 --- a/gps/README.md +++ b/gps/README.md @@ -1,5 +1,3 @@ -**NOTE: Under development. API may be subject to change** - # 1. as_GPS This repository offers a suite of asynchronous device drivers for GPS devices @@ -15,10 +13,10 @@ on this excellent library [micropyGPS]. * 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] targets based on STM chips (e.g. the [Pyboad]) - extend the capabilities of the read-only and read-write drivers to provide - precision timing. The RTC may be calibrated and set to achieve timepiece-level - accuracy. + * Timing drivers for [MicroPython] only extend the capabilities of the + read-only and read-write drivers to provide precision μs class 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. @@ -30,17 +28,17 @@ driver as they emit NMEA sentences on startup. NMEA sentence parsing is based on [micropyGPS] but with significant changes. - * As asynchronous drivers they require `uasyncio` on [MicroPython]. They use - asyncio under Python 3.5+. + * 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 reduced by various techniques: this reduces heap - fragmentation, improving application reliability on RAM constrained devices. + * 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. + high absolute accuracy. These are implemented by subclassing these drivers. * Hooks are provided for user-designed subclassing, for example to parse additional message types. @@ -152,7 +150,7 @@ 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 +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. @@ -185,7 +183,8 @@ Three mechanisms exist for responding to outages. 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). + * `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. @@ -255,8 +254,8 @@ gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG) ### 2.2.2 Course - * `speed` Optional arg `unit=KPH`. Returns the current speed in the specified - units. Options: `as_GPS.KPH`, `as_GPS.MPH`, `as_GPS.KNOT`. + * `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`. @@ -268,7 +267,7 @@ gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG) * `time_since_fix` No args. Returns time in milliseconds since last valid fix. - * `time_string` Arg `local` default `True`. Returns the current time in form + * `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 @@ -284,19 +283,20 @@ gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG) 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. +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 valid messages of the - specified types have been received. For example: + 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) - # can now access these data values with confidence + # Access these data values now ``` -No check is provided for satellite data as this is checked by the +No option is provided for satellite data: this functionality is provided by the `get_satellite_data` coroutine. ### 2.3.2 Satellite Data @@ -345,24 +345,27 @@ 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 with 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 5). + * `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` [hrs: int, mins: int, secs: int] UTC time e.g. [23, 3, 58]. Note - that some GPS hardware may only provide integer seconds. The MTK3339 chip - provides a float whose value is always an integer. - * `local_time` [hrs: int, mins: int, secs: int] Local time. - * `date` [day: int, month: int, year: int] e.g. [23, 3, 18] + * `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 since the epoch. Epoch start depends on whether + running under MicroPython or Python 3.5+. -The `utc` bound variable updates on receipt of RMC, GLL or GGA messages. - -The `date` and `local_time` variables are updated when an RMC message is -received. A local time offset will affect the `date` value where the offset -causes the local time to pass midnight. +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 @@ -394,28 +397,34 @@ and subsequent characters are stripped from the last. Thus if the string was received `reparse` would see `['GPGGA','123519','4807.038','N','01131.000','E','1','08','0.9','545.4','M','46.9','M','','']` -# 3. The GPS class read/write driver +# 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 limited support for sending PMTK command -packets to GPS modules based on the MTK3329/MTK3339 chip. These include: +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 a limited subset of the MTK commands used on many popular devices. - * `as_GPS.py` The library containing the base class. + 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. This will pause - until a fix has been achieved. After that changes are made for about 1 minute, - then 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: + * `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. @@ -452,6 +461,7 @@ loop.run_until_complete(test()) 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. @@ -469,21 +479,20 @@ If implemented the message callback will receive the following positional 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]. +relevant bound variable e.g. `['antenna', 3]`. -For unhandled messages text strings are as received, except that element 0 has -the '$' symbol removed. The last element is the last informational string - the -checksum has been verified and is not in the list. +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 + * `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 @@ -493,27 +502,26 @@ The args presented to the fix callback are as described in `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. + * `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 From 40d4a36887eb1844cc3d24635c6b2e14248aa278 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 27 May 2018 10:09:33 +0100 Subject: [PATCH 019/472] Prior to creating latency branch --- gps/README.md | 135 ++++++++++++++++++++++++++++++-------------------- gps/as_GPS.py | 2 +- 2 files changed, 81 insertions(+), 56 deletions(-) diff --git a/gps/README.md b/gps/README.md index c061dc0..bb63474 100644 --- a/gps/README.md +++ b/gps/README.md @@ -532,8 +532,8 @@ cleared by power cycling the GPS. ### 3.3.1 Changing baudrate -The if you change the GPS baudrate the UART should be re-initialised -immediately after the `baudrate` coroutine terminates: +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): @@ -561,21 +561,21 @@ 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. - * `antenna` Initially 0. Values: + 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 +## 3.5 The parse method (developer note) -The default `parse` method is redefined. 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. +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-constructor). @@ -583,10 +583,14 @@ Other `PMTK` messages are passed to the optional message callback as described # 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 time reference. It may -be used to set and to calibrate the Pyboard realtime clock (RTC). These drivers -are for MicroPython only. The RTC functionality is for STM chips (e.g. Pyboard) -only (at least until the `machine` library supports an `RTC` class). +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 @@ -603,7 +607,7 @@ and `GPS_RWTimer` for read/write access. * `as_tGPS.py` The library. Provides `GPS_Timer` and `GPS_RWTimer` classes. * `as_GPS_time.py` Test scripts for above. -### 4.1.1 Usage xxample +### 4.1.1 Usage example ```python import uasyncio as asyncio @@ -645,8 +649,10 @@ the base classes are supported. Additional functionality is detailed below. 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 `AS_GPS` details for these args. + * `local_offset` See [base class](./README.md##21-constructor) for details of + these args. * `fix_cb` * `cb_mask` * `fix_cb_args` @@ -663,8 +669,10 @@ 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 `GPS` details. + * `local_offset` See [base class](./README.md##32-gps-class-constructor) for + details of these args. * `fix_cb` * `cb_mask` * `fix_cb_args` @@ -674,7 +682,7 @@ Optional positional args: 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 + receives the `GPS_RWTimer` instance as the first arg, followed by any args in the tuple. ## 4.3 Public methods @@ -683,41 +691,47 @@ These return an accurate GPS time of day. As such they return as fast as possible. To achieve this they avoid allocation and dispense with error checking: these functions 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. A usage example is in the time -demo in `as_GPS_time.py`. +Subsequent calls may occur without restriction; see usage example above. * `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]`. +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`. + See [Absolute accuracy](./README.md#45-absolute-accuracy) for a discussion of the accuracy of these methods. ## 4.4 Public coroutines - * `ready` No args. Pauses until a valid time/date message and PPS signal have +All MicroPython targets: + * `ready` No args. Pauses until a valid time/date message and PPS signal have occurred. - * `set_rtc` No args. STM hosts only. Sets the Pyboard RTC to GPS time. Coro - pauses for up to 1s as it waits for a PPS pulse. - * `delta` No args. STM hosts only. Returns no. of μs RTC leads GPS. Coro - pauses for up to 1s. - * `calibrate` Arg: integer, no. of minutes to run default 5. STM hosts only. - Calibrates the Pyboard RTC and returns the calibration factor for it. This - 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. + +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 (and hence calibration factor) is temperature -dependent. For the most accurate possible results allow the Pyboard to reach -working temperature before calibrating. +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 @@ -725,11 +739,11 @@ 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 -+-70μs (standard deviation). ++-70μs (standard deviation). This is based on a Pyboard running at 168MHz. -Without an atomic clock synchronised to a Tier 1 NTP server this 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. +Without an atomic clock synchronised to a Tier 1 NTP server absolute accuracy +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 @@ -742,10 +756,12 @@ Sources of error are: * Variations in interrupt latency. * Inaccuracy in the `ticks_us` timer. * Latency in the function used to retrieve the time. + * Mean value of the interrupt latency. The test program `as_GPS_time.py` has a test `usecs` which aims to assess the first two. It repeatedly uses `ticks_us` to measure the time between PPS pulses -over a minute then calculates some simple statistics on the result. +over a minute then calculates some simple statistics on the result. On targets +other than a 168MHz Pyboard this offers a way of estimating overheads. ## 4.6 Test/demo program as_GPS_time.py @@ -758,7 +774,8 @@ runs. 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 - the absolute accuracy of the `get_t_split` method. + some limits to the absolute accuracy of the `get_t_split` method as discussed + above. # 5. Supported Sentences @@ -789,22 +806,23 @@ 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`. -An example of overriding this method may be found in the `as_rwGPS.py` module. +A subclass may override `parse` to parse such sentences. An example this may be +found in the `as_rwGPS.py` module. -A subclass may redefine this to attempt to parse such sentences. The method -receives an arg `segs` being a list of strings. These are the parts of the -sentence which were delimited by commas. `segs[0]` is the sentence type with -the leading '$' character removed. +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. -It should return `True` if the sentence was successfully parsed, otherwise -`False`. +The `parse` method should return `True` if the sentence was successfully +parsed, otherwise `False`. -Where a sentence is successfully parsed, a null `reparse` method is called. -This may be overridden in a subclass. +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 +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 @@ -823,10 +841,17 @@ 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 second boundary minutes, hours and the date may roll over. +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. +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. + +A discussion of how the precision timing methods interpolate between epoch +times may be found here [section 4.5](./README.md##45-absolute-accuracy). [MicroPython]:https://micropython.org/ [frozen module]:https://learn.adafruit.com/micropython-basics-loading-modules/frozen-modules diff --git a/gps/as_GPS.py b/gps/as_GPS.py index 3592b55..ad09b13 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -269,7 +269,7 @@ def _set_date_time(self, utc_string, date_string): except ValueError: # Bad date or time strings return False wday = self._week_day(y, m, d) - t = self._mktime((y, m, d, hrs, mins, int(secs), wday - 1, 0, 0)) + 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 return True From 8bfc5b0a8cc7b4bea4d9c52ea373eafc87ec76aa Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 27 May 2018 11:45:40 +0100 Subject: [PATCH 020/472] gps/README.md Improvements and corrections. --- gps/README.md | 72 ++++++++++++++++++++++++++++++---------------- gps/as_GPS_time.py | 4 +-- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/gps/README.md b/gps/README.md index bb63474..02e0560 100644 --- a/gps/README.md +++ b/gps/README.md @@ -739,29 +739,9 @@ 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 -+-70μs (standard deviation). This is based on a Pyboard running at 168MHz. - -Without an atomic clock synchronised to a Tier 1 NTP server absolute accuracy -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. - -Sources of error are: - * Variations in interrupt latency. - * Inaccuracy in the `ticks_us` timer. - * Latency in the function used to retrieve the time. - * Mean value of the interrupt latency. - -The test program `as_GPS_time.py` has a test `usecs` which aims to assess the -first two. It repeatedly uses `ticks_us` to measure the time between PPS pulses -over a minute then calculates some simple statistics on the result. On targets -other than a 168MHz Pyboard this offers a way of estimating overheads. +-5μs +65μs (standard deviation). This is based on a Pyboard running at 168MHz. +The reasoning behind this is discussed in +[section 2.5](./README.md#7-notes-on-timing). ## 4.6 Test/demo program as_GPS_time.py @@ -850,8 +830,50 @@ 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. -A discussion of how the precision timing methods interpolate between epoch -times may be found here [section 4.5](./README.md##45-absolute-accuracy). +## 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 1 second). + +With correct usage when the PPS interrupt occurs the UART will not be receiving +data (this can affect ISR latency). 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 it calculates some simple statistics on the +results. On targets other than a 168MHz Pyboard this may be run to estimate +overheads. + +Assuming the timing function 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 diff --git a/gps/as_GPS_time.py b/gps/as_GPS_time.py index 665a8d4..f09a03f 100644 --- a/gps/as_GPS_time.py +++ b/gps/as_GPS_time.py @@ -139,7 +139,7 @@ async def do_usec(terminate): tick.clear() err = 1000000 - usecs count += 1 - print('Error {: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) @@ -148,7 +148,7 @@ async def do_usec(terminate): nsamples += 1 # SD: apply Bessel's correction for infinite population sd = int(math.sqrt(sd/(nsamples - 1))) - print('Error: {:5d}μs max {:5d}μs min. Standard deviation {:4d}μs'.format(max_us, min_us, sd)) + print('Timing discrepancy is: {:5d}μs max {:5d}μs min. Standard deviation {:4d}μs'.format(max_us, min_us, sd)) def usec(minutes=1): terminate = asyn.Event() From 8054d3fb029fede7eb9d7e1cb4ace3a3ddbf63b2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 27 May 2018 11:51:38 +0100 Subject: [PATCH 021/472] gps/README.md Improvements and corrections. --- gps/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gps/README.md b/gps/README.md index 02e0560..c390507 100644 --- a/gps/README.md +++ b/gps/README.md @@ -578,7 +578,7 @@ 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-constructor). +[in section 3.2](./README.md#32-gps-class-constructor). # 4. Using GPS for accurate timing From 6e6401a3e7e5a6d0f942bc8c4786f162286cae83 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 27 May 2018 11:54:11 +0100 Subject: [PATCH 022/472] gps/README.md Fix broken links. --- gps/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gps/README.md b/gps/README.md index c390507..1d9c458 100644 --- a/gps/README.md +++ b/gps/README.md @@ -651,7 +651,7 @@ Mandatory positional args: * `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 + * `local_offset` See [base class](./README.md#21-constructor) for details of these args. * `fix_cb` * `cb_mask` @@ -671,7 +671,7 @@ This takes three mandatory positional args: * `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 + * `local_offset` See [base class](./README.md#32-gps-class-constructor) for details of these args. * `fix_cb` * `cb_mask` From bdff299f0b918cf8a02c6f14f62fd4b64118bed6 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 27 May 2018 11:55:55 +0100 Subject: [PATCH 023/472] gps/README.md Fix broken links. --- gps/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gps/README.md b/gps/README.md index 1d9c458..04bb49b 100644 --- a/gps/README.md +++ b/gps/README.md @@ -741,7 +741,7 @@ 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 2.5](./README.md#7-notes-on-timing). +[section 7](./README.md#7-notes-on-timing). ## 4.6 Test/demo program as_GPS_time.py From d0f464d69a41891bfa6d04894a53481a3e0dc42f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 27 May 2018 12:35:01 +0100 Subject: [PATCH 024/472] Fix bug in GPS_Tbase.get_t_split. --- gps/README.md | 41 +++++++++++++++++++++++------------------ gps/as_tGPS.py | 2 +- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/gps/README.md b/gps/README.md index 04bb49b..c49e2d4 100644 --- a/gps/README.md +++ b/gps/README.md @@ -811,11 +811,15 @@ These tests allow NMEA parsing to be verified in the absence of GPS hardware: # 7. Notes on timing -At the default baudrate of 9600 I measured a transmission time of 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. +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 @@ -854,26 +858,27 @@ Sources of variable error: * Inaccuracy in the `ticks_us` timer (significant over 1 second). With correct usage when the PPS interrupt occurs the UART will not be receiving -data (this can affect ISR latency). 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. +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 it calculates some simple statistics on the -results. On targets other than a 168MHz Pyboard this may be run to estimate +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. -Assuming the timing function 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. +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 diff --git a/gps/as_tGPS.py b/gps/as_tGPS.py index 5f0c93c..71b80e8 100644 --- a/gps/as_tGPS.py +++ b/gps/as_tGPS.py @@ -211,7 +211,7 @@ def get_t_split(self): t = self.secs acquired = self.acquired machine.enable_irq(state) - x, secs = divmod(secs, 60) + x, secs = divmod(t, 60) hrs, mins = divmod(x, 60) dt = utime.ticks_diff(utime.ticks_us(), acquired) # μs to time now ds, us = divmod(dt, 1000000) From 2980c2a7fb69562ee7347e5294599ac83e5111a4 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 27 May 2018 12:44:18 +0100 Subject: [PATCH 025/472] gps/README.md Add note on timing and update rate. --- gps/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gps/README.md b/gps/README.md index c49e2d4..4367209 100644 --- a/gps/README.md +++ b/gps/README.md @@ -699,8 +699,9 @@ 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`. - +target supporting `machine.ticks_us`. These methods are currently based on the +default 1s update rate. If this is increased they may return incorrect values. + See [Absolute accuracy](./README.md#45-absolute-accuracy) for a discussion of the accuracy of these methods. From 3a7c7c732baced6f2e5850cc40e395dabedd068f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 30 May 2018 09:38:09 +0100 Subject: [PATCH 026/472] Prior to asynchronous parsing. --- gps/README.md | 44 +++++--- gps/as_GPS.py | 60 ++++++++--- gps/as_GPS_time.py | 31 +++--- gps/as_GPS_utils.py | 48 +++++++++ gps/as_rwGPS.py | 1 + gps/as_rwGPS_time.py | 232 +++++++++++++++++++++++++++++++++++++++++++ gps/as_tGPS.py | 51 ++++++---- gps/ast_pbrw.py | 14 ++- 8 files changed, 412 insertions(+), 69 deletions(-) create mode 100644 gps/as_GPS_utils.py create mode 100644 gps/as_rwGPS_time.py diff --git a/gps/README.md b/gps/README.md index 4367209..f2c0920 100644 --- a/gps/README.md +++ b/gps/README.md @@ -14,9 +14,10 @@ on this excellent library [micropyGPS]. 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 precision μs class GPS timing. On + 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. + * Can write `.kml` files for displaying journeys on Google Earth. * Drivers may be extended via subclassing, for example to support additional sentence types. @@ -28,8 +29,8 @@ driver as they emit NMEA sentences on startup. NMEA sentence parsing is based on [micropyGPS] but with significant changes. - * As asynchronous drivers they require `uasyncio` on [MicroPython] or asyncio - under Python 3.5+. + * 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. @@ -359,8 +360,8 @@ The following are counts since instantiation. * `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 since the epoch. Epoch start depends on whether - running under MicroPython or Python 3.5+. + * `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 @@ -532,6 +533,15 @@ 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 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: @@ -605,7 +615,8 @@ and `GPS_RWTimer` for read/write access. * `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 above. + * `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 @@ -687,20 +698,21 @@ Optional positional args: ## 4.3 Public methods -These return an accurate GPS time of day. As such they return as fast as -possible. To achieve this they avoid allocation and dispense with error -checking: these functions 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. +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]`. - -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`. These methods are currently based on the -default 1s update rate. If this is increased they may return incorrect values. + * `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. diff --git a/gps/as_GPS.py b/gps/as_GPS.py index ad09b13..96c8e36 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -21,6 +21,8 @@ except ImportError: const = lambda x : x +from math import modf + # Angle formats DD = const(1) DMS = const(2) @@ -50,6 +52,8 @@ 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 @@ -71,7 +75,7 @@ def _week_day(year, month, day, offset = [0, 31, 59, 90, 120, 151, 181, 212, 243 day_of_week = day_of_week if day_of_week else 7 return day_of_week - # 8-bit xor of characters between "$" and "*" + # 8-bit xor of characters between "$" and "*". Takes 6ms on Pyboard! @staticmethod def _crc_check(res, ascii_crc): try: @@ -137,6 +141,8 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC # 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) @@ -151,6 +157,7 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC 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 @@ -182,20 +189,22 @@ async def _run(self): # Update takes a line of text def _update(self, line): line = line.rstrip() - try: - next(c for c in line if ord(c) < 10 or ord(c) > 126) - return None # Bad character received - except StopIteration: - pass # All good + if self.FULL_CHECK: # 9ms on Pyboard + try: + next(c for c in line if ord(c) < 10 or ord(c) > 126) + return None # Bad character received + except StopIteration: + pass # All good - if len(line) > self._SENTENCE_LIMIT or not '*' in line: - return None # Too long or malformed + if len(line) > self._SENTENCE_LIMIT or not '*' in line: + return None # Too long or malformed a = line.split(',') segs = a[:-1] + a[-1].split('*') - if not self._crc_check(line, segs[-1]): - self.crc_fails += 1 # Update statistics - return None + if self.FULL_CHECK: # 6ms on Pyboard + if not self._crc_check(line, segs[-1]): + self.crc_fails += 1 # Update statistics + return None self.clean_sentences += 1 # Sentence is good but unparsed. segs[0] = segs[0][1:] # discard $ segs = segs[:-1] # and checksum @@ -261,11 +270,28 @@ def _set_date_time(self, utc_string, date_string): return False try: hrs = int(utc_string[0:2]) # h + if hrs > 24 or hrs < 0: + return False mins = int(utc_string[2:4]) # mins - secs = int(utc_string[4:6]) # secs from chip is a float but FP is always 0 + if mins > 60 or mins < 0: + return False + # 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) + if secs > 60 or secs < 0: + return False + self.msecs = int(fss * 1000) d = int(date_string[0:2]) # day + if d > 31 or d < 1: + return False m = int(date_string[2:4]) # month + if m > 12 or m < 1: + return False y = int(date_string[4:6]) + 2000 # year + if y < 2018 or y > 2030: + return False except ValueError: # Bad date or time strings return False wday = self._week_day(y, m, d) @@ -284,16 +310,18 @@ def _set_date_time(self, utc_string, date_string): # The ._received dict entry is initially set False and is set only if the data # was successfully updated. Valid because parsers are synchronous methods. +# Chip sends rubbish RMC messages before first PPS pulse, but these have data +# valid False def _gprmc(self, gps_segments): # Parse RMC sentence self._valid &= ~RMC - # UTC Timestamp and date. - if not self._set_date_time(gps_segments[1], gps_segments[9]): - return False - # Check Receiver Data Valid Flag if gps_segments[2] != 'A': return True # Correctly parsed + # UTC Timestamp and date. + if not self._set_date_time(gps_segments[1], gps_segments[9]): + return False + # Data from Receiver is Valid/Has Fix. Longitude / Latitude if not self._fix(gps_segments, 3, 5): return False diff --git a/gps/as_GPS_time.py b/gps/as_GPS_time.py index f09a03f..ff090a1 100644 --- a/gps/as_GPS_time.py +++ b/gps/as_GPS_time.py @@ -1,5 +1,6 @@ -# as_GPS_time.py Test scripts for as_tGPS +# 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. @@ -37,6 +38,7 @@ async def setup(): # 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() @@ -59,11 +61,14 @@ async def drift_test(terminate, gps_tim): await asyncio.sleep(10) return dt - dstart -async def do_drift(terminate, minutes): +async def do_drift(minutes): print('Setting up GPS.') gps_tim = await setup() print('Waiting for time data.') await gps_tim.ready() + terminate = asyn.Event() + loop = asyncio.get_event_loop() + loop.create_task(killer(terminate, minutes)) print('Setting RTC.') await gps_tim.set_rtc() print('Measuring drift.') @@ -73,14 +78,12 @@ async def do_drift(terminate, minutes): print('Rate of change {}μs/hr {}secs/year'.format(ush, spa)) def drift(minutes=5): - terminate = asyn.Event() loop = asyncio.get_event_loop() - loop.create_task(killer(terminate, minutes)) - loop.run_until_complete(do_drift(terminate, minutes)) + 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(terminate): +async def do_time(minutes): fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}' print('Setting up GPS.') gps_tim = await setup() @@ -88,6 +91,9 @@ async def do_time(terminate): await gps_tim.ready() print('Setting RTC.') await gps_tim.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: @@ -95,10 +101,8 @@ async def do_time(terminate): print(fstr.format(gps_tim.get_ms(), t[0], t[1], t[2], t[3])) def time(minutes=1): - terminate = asyn.Event() loop = asyncio.get_event_loop() - loop.create_task(killer(terminate, minutes)) - loop.run_until_complete(do_time(terminate)) + loop.run_until_complete(do_time(minutes)) # ******** Measure accracy of μs clock ******** # Callback occurs in interrupt context @@ -122,7 +126,7 @@ async def us_setup(tick): fix_cb=lambda *_: red.toggle(), pps_cb=us_cb, pps_cb_args=(tick, blue)) -async def do_usec(terminate): +async def do_usec(minutes): tick = asyn.Event() print('Setting up GPS.') gps_tim = await us_setup(tick) @@ -133,6 +137,9 @@ async def do_usec(terminate): 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() @@ -151,7 +158,5 @@ async def do_usec(terminate): print('Timing discrepancy is: {:5d}μs max {:5d}μs min. Standard deviation {:4d}μs'.format(max_us, min_us, sd)) def usec(minutes=1): - terminate = asyn.Event() loop = asyncio.get_event_loop() - loop.create_task(killer(terminate, minutes)) - loop.run_until_complete(do_usec(terminate)) + loop.run_until_complete(do_usec(minutes)) diff --git a/gps/as_GPS_utils.py b/gps/as_GPS_utils.py new file mode 100644 index 0000000..7deb5d6 --- /dev/null +++ b/gps/as_GPS_utils.py @@ -0,0 +1,48 @@ +# 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/gps/as_rwGPS.py b/gps/as_rwGPS.py index 70cf86d..2cb5540 100644 --- a/gps/as_rwGPS.py +++ b/gps/as_rwGPS.py @@ -77,6 +77,7 @@ async def update_interval(self, ms=1000): 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' diff --git a/gps/as_rwGPS_time.py b/gps/as_rwGPS_time.py new file mode 100644 index 0000000..5361fa5 --- /dev/null +++ b/gps/as_rwGPS_time.py @@ -0,0 +1,232 @@ +# 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 + +# NOTE: After running any of these tests the tests in as_GPS_time hang waiting +# for time data (until GPS is power cycled). This is because resetting the baudrate +# to 9600 does not work. Setting baudrate to 4800 gave 115200. It seems this +# chip functionality is dodgy. +# String sent for 9600: $PMTK251,9600*17\r\n +# Data has (for 38400): $PMTK251,38400*27 +# Sending: $PMTK251,38400*27\r\n' +# Issuing a factory reset in shutdown does not fix this. + +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 + +# Avoid multiple baudrates. Tests use 9600 or 115200 only. +# *** Baudrate over 19200 causes messages not to be received *** +BAUDRATE = 19200 +UPDATE_INTERVAL = 200 # 100 +READ_BUF_LEN = 1000 # 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 + # 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 **** DO WE NEED TO SET END EVENT? ***** + #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()) + 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 ******** +# Callback occurs in interrupt context +us_acquired = None +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)) + 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) + +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/gps/as_tGPS.py b/gps/as_tGPS.py index 71b80e8..b289be0 100644 --- a/gps/as_tGPS.py +++ b/gps/as_tGPS.py @@ -47,34 +47,45 @@ def gps_rw_t_init(self, sreader, swriter, pps_pin, local_offset=0, 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.secs = None # Integer time since midnight at last PPS + 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(pps_pin)) + loop.create_task(self._start()) - async def _start(self, pps_pin): + async def _start(self): await self.data_received(date=True) - 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) + + # 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 secs since midnight local time. - secs = (self.epoch_time + int(3600*self.local_offset)) % 86400 - # This PPS is one second later - secs += 1 - if secs >= 86400: # Next PPS will deal with rollover + # 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 - self.secs = secs + 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 in last NMEA sentence. Earlier test ensures no rollover. - self._rtcbuf[6] = secs % 60 + # 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 @@ -197,21 +208,23 @@ async def calibrate(self, minutes=5): # No allocation. def get_ms(self): state = machine.disable_irq() - t = self.secs + t = self.t_ms acquired = self.acquired machine.enable_irq(state) - return 1000*t + utime.ticks_diff(utime.ticks_us(), acquired) // 1000 + 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 subsequent ISR will see hms = 0, 0, 1 and a value of - # .acquired > 1000000. + # 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.secs + t = self.t_ms acquired = self.acquired machine.enable_irq(state) - x, secs = divmod(t, 60) + 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) @@ -219,7 +232,7 @@ def get_t_split(self): self._time[0] = hrs self._time[1] = mins self._time[2] = secs + ds - self._time[3] = us + 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}) diff --git a/gps/ast_pbrw.py b/gps/ast_pbrw.py index 3512e59..ec5a760 100644 --- a/gps/ast_pbrw.py +++ b/gps/ast_pbrw.py @@ -3,7 +3,7 @@ # 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_pyGPS +# Test asynchronous GPS device driver as_rwGPS # LED's: # Green indicates data is being received. @@ -18,6 +18,7 @@ import as_GPS import as_rwGPS +# Avoid multiple baudrates. Tests use 9600 or 19200 only. BAUDRATE = 19200 red, green, yellow, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) ntimeouts = 0 @@ -155,12 +156,15 @@ async def gps_test(): loop.create_task(change_status(gps, uart)) async def shutdown(): - # If power was lost in last session can retrieve connectivity in the subsequent - # stuck session with ctrl-c + # 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) - print('Restoring default baudrate.') - await gps.baudrate(9600) + 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()) From def82bec7e6924d77d413e21c8b17ee53af7e3ba Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 31 May 2018 07:09:44 +0100 Subject: [PATCH 027/472] Prior to improving error handling. --- TUTORIAL.md | 5 +++++ gps/README.md | 13 ++++++++++--- gps/as_GPS.py | 16 +++++++++++----- gps/as_GPS_time.py | 9 ++++++++- gps/as_rwGPS_time.py | 18 +++++++++++++----- 5 files changed, 47 insertions(+), 14 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index b40e29a..67992a8 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1078,6 +1078,11 @@ The mechanism works because the device driver (written in C) implements the following methods: `ioctl`, `read`, `write`, `readline` and `close`. See section 5.3 for further discussion. +Applications using the UART should be designed such that all coros minimise +blocking periods. This is because blocking while the UART is receiving data can +lead to buffer overflows with consequent loss of data. This can be ameliorated +by using a larger UART read buffer length or a lower baudrate. + ### 5.1.1 A UART driver example The program `auart_hd.py` illustrates a method of communicating with a half diff --git a/gps/README.md b/gps/README.md index f2c0920..8a94299 100644 --- a/gps/README.md +++ b/gps/README.md @@ -398,6 +398,12 @@ and subsequent characters are stripped from the last. Thus if the string 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. This is intended for use at high baudrates + where the time consumed by these checks can be excessive. + # 3. The GPS class read-write driver This is a subclass of `AS_GPS` and supports all its public methods, coroutines @@ -538,9 +544,10 @@ Under investigation. ** 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 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. +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: diff --git a/gps/as_GPS.py b/gps/as_GPS.py index 96c8e36..4d64251 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -171,20 +171,21 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC 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.create_task(self._run(loop)) ########################################## # Data Stream Handler Functions ########################################## - async def _run(self): + 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 - self._update(res) + loop.create_task(self._update(res)) + await asyncio.sleep_ms(0) # Ensure task runs and res is copied # Update takes a line of text def _update(self, line): @@ -195,21 +196,26 @@ def _update(self, line): return None # Bad character received except StopIteration: pass # All good - + await asyncio.sleep_ms(0) if len(line) > self._SENTENCE_LIMIT or not '*' in line: return None # Too long or malformed a = line.split(',') segs = a[:-1] + a[-1].split('*') + await asyncio.sleep_ms(0) + if self.FULL_CHECK: # 6ms on Pyboard if not self._crc_check(line, segs[-1]): self.crc_fails += 1 # Update statistics return None + await asyncio.sleep_ms(0) + self.clean_sentences += 1 # Sentence is good but unparsed. segs[0] = segs[0][1:] # discard $ segs = segs[:-1] # and checksum if segs[0] in self.supported_sentences: - s_type = self.supported_sentences[segs[0]](segs) + s_type = self.supported_sentences[segs[0]](segs) # Parse + await asyncio.sleep_ms(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 diff --git a/gps/as_GPS_time.py b/gps/as_GPS_time.py index ff090a1..ba9a138 100644 --- a/gps/as_GPS_time.py +++ b/gps/as_GPS_time.py @@ -105,10 +105,17 @@ def time(minutes=1): 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 + 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)) diff --git a/gps/as_rwGPS_time.py b/gps/as_rwGPS_time.py index 5361fa5..161bd1a 100644 --- a/gps/as_rwGPS_time.py +++ b/gps/as_rwGPS_time.py @@ -29,9 +29,9 @@ # Avoid multiple baudrates. Tests use 9600 or 115200 only. # *** Baudrate over 19200 causes messages not to be received *** -BAUDRATE = 19200 -UPDATE_INTERVAL = 200 # 100 -READ_BUF_LEN = 1000 # test +BAUDRATE = 57600 +UPDATE_INTERVAL = 100 +READ_BUF_LEN = 200 # 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.') @@ -58,7 +58,7 @@ async def shutdown(): #gps.close() # Stop ISR #print('Restoring default 1s update rate.') #await asyncio.sleep(0.5) - #await gps.update_interval(1000) # 1s update rate **** DO WE NEED TO SET END EVENT? ***** + #await gps.update_interval(1000) # 1s update rate #print('Restoring satellite data.') #await gps.command(as_rwGPS.DEFAULT_SENTENCES) # Restore satellite data @@ -73,6 +73,7 @@ async def setup(): 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) @@ -164,8 +165,12 @@ def time(minutes=1): 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 +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: @@ -185,12 +190,15 @@ async def us_setup(tick): 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 From 0177175dbd17e2822e127ba9d6322d717efdafe3 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 31 May 2018 11:57:03 +0100 Subject: [PATCH 028/472] V0.15 Exception handling simplified. FULL_CHECK optional. --- TUTORIAL.md | 12 ++- gps/README.md | 31 +++--- gps/as_GPS.py | 222 ++++++++++++++++--------------------------- gps/as_GPS_time.py | 38 ++++---- gps/as_rwGPS_time.py | 13 +-- gps/astests.py | 24 +++-- 6 files changed, 149 insertions(+), 191 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 67992a8..603b765 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1078,10 +1078,14 @@ The mechanism works because the device driver (written in C) implements the following methods: `ioctl`, `read`, `write`, `readline` and `close`. See section 5.3 for further discussion. -Applications using the UART should be designed such that all coros minimise -blocking periods. This is because blocking while the UART is receiving data can -lead to buffer overflows with consequent loss of data. This can be ameliorated -by using a larger UART read buffer length or a lower baudrate. +A UART can receive data at any time. The IORead 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 all coros minimise blocking periods 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. ### 5.1.1 A UART driver example diff --git a/gps/README.md b/gps/README.md index 8a94299..8530be0 100644 --- a/gps/README.md +++ b/gps/README.md @@ -1,8 +1,8 @@ # 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 sentence parsing is based -on this excellent library [micropyGPS]. +which communicate with the host via a UART. GPS [NMEA-0183] sentence parsing is +based on this excellent library [micropyGPS]. ## 1.1 Driver characteristics @@ -10,6 +10,7 @@ on this excellent library [micropyGPS]. 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. @@ -17,17 +18,17 @@ on this excellent library [micropyGPS]. 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. - * Can write `.kml` files for displaying journeys on Google Earth. * 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 sentences on startup. +driver as they emit [NMEA-0183] sentences on startup. ## 1.2 Comparison with [micropyGPS] -NMEA sentence parsing is based on [micropyGPS] but with significant changes. +[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+. @@ -47,8 +48,8 @@ NMEA sentence parsing is based on [micropyGPS] but with significant changes. ## 1.1 Overview -The `AS_GPS` object runs a coroutine which receives GPS NMEA sentences from the -UART and parses them as they arrive. Valid sentences cause local bound +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. @@ -68,8 +69,8 @@ the Pyboard is run from a voltage >5V the Pyboard 3V3 pin should be used. 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 device for precise timing -(`as_tGPS.py`). Any pin may be used. +PPS connection is required only if using the timing driver `as_tGPS.py`. Any +pin may be used. ## 1.2 Basic Usage @@ -170,8 +171,7 @@ or later. 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. Whether this matters and any action to take is application -dependent. +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). @@ -193,8 +193,7 @@ Optional positional args: * `fix_cb_args` A tuple of args for the callback (default `()`). Notes: -`local_offset` correctly alters the date where time passes the 00.00.00 -boundary. +`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 @@ -401,8 +400,8 @@ was received `reparse` would see ## 2.6 Public class variable * `FULL_CHECK` Default `True`. If set `False` disables CRC checking and other - basic checks on received sentences. This is intended for use at high baudrates - where the time consumed by these checks can be excessive. + 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 @@ -875,7 +874,7 @@ Sources of fixed lag: Sources of variable error: * Variations in interrupt latency (small on Pyboard). - * Inaccuracy in the `ticks_us` timer (significant over 1 second). + * 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 diff --git a/gps/as_GPS.py b/gps/as_GPS.py index 4d64251..93b28a3 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -185,37 +185,40 @@ async def _run(self, loop): except UnicodeError: # Garbage: can happen e.g. on baudrate change continue loop.create_task(self._update(res)) - await asyncio.sleep_ms(0) # Ensure task runs and res is copied + await asyncio.sleep(0) # Ensure task runs and res is copied # Update takes a line of text - def _update(self, line): - line = line.rstrip() + async def _update(self, line): + line = line.rstrip() # Copy line if self.FULL_CHECK: # 9ms on Pyboard try: next(c for c in line if ord(c) < 10 or ord(c) > 126) - return None # Bad character received + return # Bad character received except StopIteration: pass # All good - await asyncio.sleep_ms(0) + await asyncio.sleep(0) if len(line) > self._SENTENCE_LIMIT or not '*' in line: - return None # Too long or malformed + return # Too long or malformed a = line.split(',') segs = a[:-1] + a[-1].split('*') - await asyncio.sleep_ms(0) + 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 None - await asyncio.sleep_ms(0) + 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 if segs[0] in self.supported_sentences: - s_type = self.supported_sentences[segs[0]](segs) # Parse - await asyncio.sleep_ms(0) + try: + s_type = self.supported_sentences[segs[0]](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 @@ -240,23 +243,21 @@ def reparse(self, segs): # Re-parse supported sentences # Fix and Time Functions ######################################## + # Caller traps ValueError def _fix(self, gps_segments, idx_lat, idx_long): - try: - # 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] - except ValueError: - return False + # 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': - return False + raise ValueError self._latitude[0] = lat_degs # In-place to avoid allocation self._latitude[1] = lat_mins self._latitude[2] = lat_hemi @@ -264,94 +265,66 @@ def _fix(self, gps_segments, idx_lat, idx_long): self._longitude[1] = lon_mins self._longitude[2] = lon_hemi self._fix_time = self._get_time() - return True 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: - return False - try: - hrs = int(utc_string[0:2]) # h - if hrs > 24 or hrs < 0: - return False - mins = int(utc_string[2:4]) # mins - if mins > 60 or mins < 0: - return False - # 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) - if secs > 60 or secs < 0: - return False - self.msecs = int(fss * 1000) - d = int(date_string[0:2]) # day - if d > 31 or d < 1: - return False - m = int(date_string[2:4]) # month - if m > 12 or m < 1: - return False - y = int(date_string[4:6]) + 2000 # year - if y < 2018 or y > 2030: - return False - except ValueError: # Bad date or time strings - return False + 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 - return True ######################################## # Sentence Parsers ######################################## # For all parsers: -# Return value: True if sentence was correctly parsed. This includes cases where -# data from receiver is correctly formed but reports an invalid state. -# The ._received dict entry is initially set False and is set only if the data -# was successfully updated. Valid because parsers are synchronous methods. - -# Chip sends rubbish RMC messages before first PPS pulse, but these have data -# valid False +# 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 + # Check Receiver Data Valid Flag ('A' active) if gps_segments[2] != 'A': - return True # Correctly parsed + raise ValueError - # UTC Timestamp and date. - if not self._set_date_time(gps_segments[1], gps_segments[9]): - return False + # UTC Timestamp and date. Can raise ValueError. + self._set_date_time(gps_segments[1], gps_segments[9]) # Data from Receiver is Valid/Has Fix. Longitude / Latitude - if not self._fix(gps_segments, 3, 5): - return False - + # Can raise ValueError. + self._fix(gps_segments, 3, 5) # Speed - try: - spd_knt = float(gps_segments[7]) - except ValueError: - return False - + spd_knt = float(gps_segments[7]) # Course - try: - course = float(gps_segments[8]) - except ValueError: - return False - + course = float(gps_segments[8]) # Add Magnetic Variation if firmware supplies it if gps_segments[10]: - try: - mv = float(gps_segments[10]) - except ValueError: - return False + mv = float(gps_segments[10]) if gps_segments[11] not in ('EW'): - return False + raise ValueError self.magvar = mv if gps_segments[11] == 'E' else -mv # Update Object Data self._speed = spd_knt @@ -363,12 +336,10 @@ 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 - return True # Correctly parsed + raise ValueError # Data from Receiver is Valid/Has Fix. Longitude / Latitude - if not self._fix(gps_segments, 1, 3): - return False - + self._fix(gps_segments, 1, 3) # Update Last Fix Time self._valid |= GLL return GLL @@ -376,12 +347,8 @@ def _gpgll(self, gps_segments): # Parse GLL sentence # Chip sends VTG messages with meaningless data before getting a fix. def _gpvtg(self, gps_segments): # Parse VTG sentence self._valid &= ~VTG - try: - course = float(gps_segments[1]) - spd_knt = float(gps_segments[5]) - except ValueError: - return False - + course = float(gps_segments[1]) + spd_knt = float(gps_segments[5]) self._speed = spd_knt self.course = course self._valid |= VTG @@ -389,29 +356,20 @@ def _gpvtg(self, gps_segments): # Parse VTG sentence def _gpgga(self, gps_segments): # Parse GGA sentence self._valid &= ~GGA - try: - # 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]) - except ValueError: - return False + # 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 - if not self._fix(gps_segments, 2, 4): - return False - + self._fix(gps_segments, 2, 4) # Altitude / Height Above Geoid - try: - altitude = float(gps_segments[9]) - geoid_height = float(gps_segments[11]) - except ValueError: - return False - + altitude = float(gps_segments[9]) + geoid_height = float(gps_segments[11]) # Update Object Data self.altitude = altitude self.geoid_height = geoid_height @@ -425,33 +383,24 @@ def _gpgga(self, gps_segments): # Parse GGA sentence def _gpgsa(self, gps_segments): # Parse GSA sentence self._valid &= ~GSA # Fix Type (None,2D or 3D) - try: - fix_type = int(gps_segments[2]) - except ValueError: - return False + 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: - try: - sat_number = int(sat_number_str) - sats_used.append(sat_number) - except ValueError: - return False + sat_number = int(sat_number_str) + sats_used.append(sat_number) else: break # PDOP,HDOP,VDOP - try: - pdop = float(gps_segments[15]) - hdop = float(gps_segments[16]) - vdop = float(gps_segments[17]) - except ValueError: - return False + 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? - return False + raise ValueError self.satellites_used = sats_used self.hdop = hdop self.vdop = vdop @@ -464,12 +413,9 @@ def _gpgsv(self, gps_segments): # the no. of the last SV sentence parsed, and data on each satellite # present in the sentence. self._valid &= ~GSV - try: - num_sv_sentences = int(gps_segments[1]) - current_sv_sentence = int(gps_segments[2]) - sats_in_view = int(gps_segments[3]) - except ValueError: - return False + 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 @@ -488,8 +434,8 @@ def _gpgsv(self, gps_segments): if gps_segments[sats]: try: sat_id = int(gps_segments[sats]) - except (ValueError,IndexError): - return False + except IndexError: + raise ValueError # Abandon try: # elevation can be null (no value) when not tracking elevation = int(gps_segments[sats+1]) @@ -592,9 +538,9 @@ def speed(self, units=KNOT): async def get_satellite_data(self): self._total_sv_sentences = 0 while self._total_sv_sentences == 0: - await asyncio.sleep_ms(200) + await asyncio.sleep(0) while self._total_sv_sentences > self._last_sv_sentence: - await asyncio.sleep_ms(100) + await asyncio.sleep(0) return self._satellite_data def time_since_fix(self): # ms since last valid fix diff --git a/gps/as_GPS_time.py b/gps/as_GPS_time.py index ba9a138..02028d1 100644 --- a/gps/as_GPS_time.py +++ b/gps/as_GPS_time.py @@ -44,8 +44,9 @@ async def killer(end_event, minutes): # ******** Calibrate and set the Pyboard RTC ******** async def do_cal(minutes): - gps_tim = await setup() - await gps_tim.calibrate(minutes) + gps = await setup() + await gps.calibrate(minutes) + gps.close() def calibrate(minutes=5): loop = asyncio.get_event_loop() @@ -53,29 +54,30 @@ def calibrate(minutes=5): # ******** Drift test ******** # Every 10s print the difference between GPS time and RTC time -async def drift_test(terminate, gps_tim): - dstart = await gps_tim.delta() +async def drift_test(terminate, gps): + dstart = await gps.delta() while not terminate.is_set(): - dt = await gps_tim.delta() - print('{} Delta {}μs'.format(gps_tim.time_string(), dt)) + 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_tim = await setup() + gps = await setup() print('Waiting for time data.') - await gps_tim.ready() + await gps.ready() terminate = asyn.Event() loop = asyncio.get_event_loop() loop.create_task(killer(terminate, minutes)) print('Setting RTC.') - await gps_tim.set_rtc() + await gps.set_rtc() print('Measuring drift.') - change = await drift_test(terminate, gps_tim) + 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() @@ -86,19 +88,20 @@ def drift(minutes=5): async def do_time(minutes): fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}' print('Setting up GPS.') - gps_tim = await setup() + gps = await setup() print('Waiting for time data.') - await gps_tim.ready() + await gps.ready() print('Setting RTC.') - await gps_tim.set_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_tim.get_t_split() - print(fstr.format(gps_tim.get_ms(), t[0], t[1], t[2], t[3])) + 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() @@ -136,9 +139,9 @@ async def us_setup(tick): async def do_usec(minutes): tick = asyn.Event() print('Setting up GPS.') - gps_tim = await us_setup(tick) + gps = await us_setup(tick) print('Waiting for time data.') - await gps_tim.ready() + await gps.ready() max_us = 0 min_us = 0 sd = 0 @@ -163,6 +166,7 @@ async def do_usec(minutes): # 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() diff --git a/gps/as_rwGPS_time.py b/gps/as_rwGPS_time.py index 161bd1a..09c7f13 100644 --- a/gps/as_rwGPS_time.py +++ b/gps/as_rwGPS_time.py @@ -6,14 +6,12 @@ # Copyright (c) 2018 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -# NOTE: After running any of these tests the tests in as_GPS_time hang waiting -# for time data (until GPS is power cycled). This is because resetting the baudrate -# to 9600 does not work. Setting baudrate to 4800 gave 115200. It seems this -# chip functionality is dodgy. +# 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' -# Issuing a factory reset in shutdown does not fix this. import uasyncio as asyncio import pyb @@ -27,11 +25,10 @@ PPS_PIN = pyb.Pin.board.X3 UART_ID = 4 -# Avoid multiple baudrates. Tests use 9600 or 115200 only. -# *** Baudrate over 19200 causes messages not to be received *** BAUDRATE = 57600 UPDATE_INTERVAL = 100 -READ_BUF_LEN = 200 # test +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.') diff --git a/gps/astests.py b/gps/astests.py index c8bc533..6bfbebd 100755 --- a/gps/astests.py +++ b/gps/astests.py @@ -8,11 +8,15 @@ # Copyright (c) 2018 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -# Run under CPython 3.x or MicroPython +# Run under CPython 3.5+ or MicroPython import as_GPS +try: + import uasyncio as asyncio +except ImportError: + import asyncio -def run_tests(): +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', @@ -46,7 +50,7 @@ def run_tests(): for sentence in test_RMC: my_gps._valid = 0 sentence_count += 1 - sentence = my_gps._update(sentence) + sentence = await my_gps._update(sentence) if sentence is None: print('RMC sentence is invalid.') else: @@ -64,7 +68,7 @@ def run_tests(): for sentence in test_GLL: my_gps._valid = 0 sentence_count += 1 - sentence = my_gps._update(sentence) + sentence = await my_gps._update(sentence) if sentence is None: print('GLL sentence is invalid.') else: @@ -78,7 +82,7 @@ def run_tests(): for sentence in test_VTG: my_gps._valid = 0 sentence_count += 1 - sentence = my_gps._update(sentence) + sentence = await my_gps._update(sentence) if sentence is None: print('VTG sentence is invalid.') else: @@ -92,7 +96,7 @@ def run_tests(): for sentence in test_GGA: my_gps._valid = 0 sentence_count += 1 - sentence = my_gps._update(sentence) + sentence = await my_gps._update(sentence) if sentence is None: print('GGA sentence is invalid.') else: @@ -110,7 +114,7 @@ def run_tests(): for sentence in test_GSA: my_gps._valid = 0 sentence_count += 1 - sentence = my_gps._update(sentence) + sentence = await my_gps._update(sentence) if sentence is None: print('GSA sentence is invalid.') else: @@ -125,7 +129,7 @@ def run_tests(): for sentence in test_GSV: my_gps._valid = 0 sentence_count += 1 - sentence = my_gps._update(sentence) + sentence = await my_gps._update(sentence) if sentence is None: print('GSV sentence is invalid.') else: @@ -166,5 +170,9 @@ def run_tests(): 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() From 26fcad8c2b4bebacdd7506a883ae6703608dfb92 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 2 Jun 2018 08:21:27 +0100 Subject: [PATCH 029/472] README.md Update comments re micropython-lib. --- README.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 377c37b..fe85c12 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This GitHub repository consists of the following parts: * [A tutorial](./TUTORIAL.md) An introductory tutorial on asynchronous - programming and the use of the uasyncio library is offered. + programming and the use of the uasyncio library (asyncio subset). * [Asynchronous device drivers](./DRIVERS.md). A module providing drivers for devices such as switches and pushbuttons. * [Synchronisation primitives](./PRIMITIVES.md). Provides commonly used @@ -38,10 +38,6 @@ Version 2.0 brings only one API change over V1.7.1, namely the arguments to code samples use default args so will work under either version. The priority version requires the later version and firmware. -[Paul Sokolovsky's library](https://github.com/pfalcon/micropython-lib) has the -latest `uasyncio` code. At the time of writing (Feb 27th 2018) the version in -[micropython-lib](https://github.com/micropython/micropython-lib) is 1.7.1. - See [tutorial](./TUTORIAL.md#installing-uasyncio-on-bare-metal) for installation instructions. @@ -69,8 +65,7 @@ It supports millisecond level timing with the following: * Event loop method `call_later_ms` * uasyncio `sleep_ms(time)` -As of `uasyncio.core` V1.7.1 (7th Jan 2018) it supports coroutine timeouts and -cancellation. +`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. @@ -79,8 +74,8 @@ Classes `Task` and `Future` are not supported. ## 3.1 Asynchronous I/O -Asynchronous I/O works with devices whose drivers support streaming, such as -UARTs. +Asynchronous I/O (`StreamReader` and `StreamWriter` classes) support devices +with streaming drivers, such as UARTs and sockets. ## 3.2 Time values From 6607123a99144379e1b66885990ec215f45e2205 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 2 Jun 2018 10:16:56 +0100 Subject: [PATCH 030/472] DRIVERS.md Improve and add code samples. --- DRIVERS.md | 118 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 92 insertions(+), 26 deletions(-) diff --git a/DRIVERS.md b/DRIVERS.md index 2d7c9fb..6b61ac7 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -33,14 +33,16 @@ This module provides the following classes: 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. +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. ## 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. Functions may be specified to -run on contact closure or opening. Functions can be callbacks or coroutines; -coroutines will be scheduled for execution and will run asynchronously. +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. @@ -50,10 +52,10 @@ Constructor argument (mandatory): 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 `()`) + 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`. @@ -62,6 +64,28 @@ 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` @@ -70,11 +94,11 @@ 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. +is considered `True` if pressed, otherwise `False` regardless of its physical +implementation. -Functions may be specified to run on button press, release, double click or -long press events. Functions can be callbacks or coroutines; coroutines will be +**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. Constructor argument (mandatory): @@ -83,14 +107,14 @@ Constructor argument (mandatory): 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 `()`). + 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 @@ -103,6 +127,26 @@ Class attributes: 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 +``` + ## 3.3 Delay_ms class This implements the software equivalent of a retriggerable monostable or a @@ -114,15 +158,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. The -function can be a callback or a coroutine; in the latter case it will be -scheduled for execution and will run asynchronously. +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 `()`). + 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. Methods: @@ -138,6 +182,28 @@ If the `trigger` method is to be called from an interrupt service routine the 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) # Dummy + +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` From 093f07b0668b9955591bb7d60fdcbdf87a717c69 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 8 Jun 2018 09:47:29 +0100 Subject: [PATCH 031/472] iotest4.py added. --- DRIVERS.md | 7 +++- aswitch.py | 9 ++-- gps/as_GPS.py | 9 +++- gps/as_tGPS.py | 2 + iotest4.py | 112 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 iotest4.py diff --git a/DRIVERS.md b/DRIVERS.md index 6b61ac7..e6c42f6 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -168,11 +168,14 @@ 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` mandatory argument `duration`. A timeout will occur after - `duration` ms unless retriggered. + 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. 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. diff --git a/aswitch.py b/aswitch.py index 80ec4cf..b086633 100644 --- a/aswitch.py +++ b/aswitch.py @@ -38,10 +38,11 @@ class Delay_ms(object): - def __init__(self, func=None, args=(), can_alloc=True): + 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 # Not running self.loop = asyncio.get_event_loop() if not can_alloc: @@ -57,8 +58,10 @@ async def _run(self): def stop(self): self.tstop = None - def trigger(self, duration): # Update end time - if self.can_alloc and self.tstop is None: + def trigger(self, duration=0): # Update end time + if duration <= 0: + duration = self.duration + if self.can_alloc and self.tstop is None: # No killer task is running self.tstop = time.ticks_add(time.ticks_ms(), duration) # Start a task which stops the delay after its period has elapsed self.loop.create_task(self.killer()) diff --git a/gps/as_GPS.py b/gps/as_GPS.py index 93b28a3..fdbddb2 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -94,6 +94,7 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC 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: @@ -307,11 +308,15 @@ def _set_date_time(self, utc_string, date_string): def _gprmc(self, gps_segments): # Parse RMC sentence self._valid &= ~RMC # Check Receiver Data Valid Flag ('A' active) - if gps_segments[2] != 'A': - raise ValueError + 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. diff --git a/gps/as_tGPS.py b/gps/as_tGPS.py index b289be0..df7c2aa 100644 --- a/gps/as_tGPS.py +++ b/gps/as_tGPS.py @@ -79,6 +79,8 @@ def _isr(self, _): 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 diff --git a/iotest4.py b/iotest4.py new file mode 100644 index 0000000..cfaa98e --- /dev/null +++ b/iotest4.py @@ -0,0 +1,112 @@ +# iotest4.py Test PR #3836. Demonstrate the anomaly with a read/write device. +# User class write() performs unbuffered writing. +# For simplicity this uses buffered read: unbuffered is tested by iotest2.py. + +# Run iotest4.test() to see expected output +# iotest4.test(False) to demonstrate the issue. + +# Pass/Fail is determined by whether the StreamReader and StreamWriter operate +# on the same (fail) or different (pass) objects. +# I suspect that the issue is with select/ipoll (uasyncio __init__.py) +# The fault is either in select/poll or uasyncio __init__.py. +# As soon as PollEventLoop.add_writer() is called, reading stops. +# PollEventLoop.add_writer() is called when StreamWriter.awrite() issues +# yield IOWrite(self.s), which for unbuffered devices is after the 1st char +# of a multi-char buf is written. + +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(this_io.wbuf[:this_io.wprint_len]) + +class MyIO(io.IOBase): + def __init__(self, read=False, write=False): + if read: + self.ready_rd = False + self.rbuf = b'ready\n' # Read buffer + pyb.Timer(4, freq = 1, callback = self.do_input) + if write: + self.wbuf = bytearray(100) # Write buffer + self.wprint_len = 0 + self.widx = 0 + self.wch = b'' + 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 + + # Emulate a device with buffered read. Return the buffer, falsify read ready + # Read timer sets ready. + def readline(self): + self.ready_rd = False + return self.rbuf + + # 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) + +def test(good=True): + if good: + myior = MyIO(read=True) + myiow = MyIO(write=True) + else: + myior = MyIO(read=True, write=True) + myiow = myior + loop = asyncio.get_event_loop() + loop.create_task(receiver(myior)) + loop.create_task(sender(myiow)) + loop.run_forever() From d74e3bcba98b72cfa455b100e6c37eecc90a0f34 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 10 Jun 2018 11:17:18 +0100 Subject: [PATCH 032/472] Changes to iotest4.py --- iotest4.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/iotest4.py b/iotest4.py index cfaa98e..d28ea06 100644 --- a/iotest4.py +++ b/iotest4.py @@ -1,18 +1,13 @@ -# iotest4.py Test PR #3836. Demonstrate the anomaly with a read/write device. +# iotest4.py Test PR #3836. # User class write() performs unbuffered writing. # For simplicity this uses buffered read: unbuffered is tested by iotest2.py. -# Run iotest4.test() to see expected output -# iotest4.test(False) to demonstrate the issue. +# This test was to demonstrate the original issue. +# With modified moduselect.c and uasyncio.__init__.py the test now passes. + +# iotest4.test() uses separate read and write objects. +# iotest4.test(False) uses a common object (failed without the mod). -# Pass/Fail is determined by whether the StreamReader and StreamWriter operate -# on the same (fail) or different (pass) objects. -# I suspect that the issue is with select/ipoll (uasyncio __init__.py) -# The fault is either in select/poll or uasyncio __init__.py. -# As soon as PollEventLoop.add_writer() is called, reading stops. -# PollEventLoop.add_writer() is called when StreamWriter.awrite() issues -# yield IOWrite(self.s), which for unbuffered devices is after the 1st char -# of a multi-char buf is written. import io, pyb import uasyncio as asyncio @@ -25,19 +20,20 @@ MP_STREAM_ERROR = const(-1) def printbuf(this_io): - print(this_io.wbuf[:this_io.wprint_len]) + for ch in this_io.wbuf[:this_io.wprint_len]: + print(chr(ch), end='') class MyIO(io.IOBase): def __init__(self, read=False, write=False): + self.ready_rd = False # Read and write not ready + self.wch = b'' if read: - self.ready_rd = False self.rbuf = b'ready\n' # Read buffer pyb.Timer(4, freq = 1, callback = self.do_input) if write: self.wbuf = bytearray(100) # Write buffer self.wprint_len = 0 self.widx = 0 - self.wch = b'' pyb.Timer(5, freq = 10, callback = self.do_output) # Read callback: emulate asynchronous input from hardware. From 48d1d52b922680b28644acdb65d4465b4c617c16 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 10 Jun 2018 11:30:35 +0100 Subject: [PATCH 033/472] Remove iotest4.py --- iotest4.py | 108 ----------------------------------------------------- 1 file changed, 108 deletions(-) delete mode 100644 iotest4.py diff --git a/iotest4.py b/iotest4.py deleted file mode 100644 index d28ea06..0000000 --- a/iotest4.py +++ /dev/null @@ -1,108 +0,0 @@ -# iotest4.py Test PR #3836. -# User class write() performs unbuffered writing. -# For simplicity this uses buffered read: unbuffered is tested by iotest2.py. - -# This test was to demonstrate the original issue. -# With modified moduselect.c and uasyncio.__init__.py the test now passes. - -# iotest4.test() uses separate read and write objects. -# iotest4.test(False) uses a common object (failed without the mod). - - -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): - for ch in this_io.wbuf[:this_io.wprint_len]: - print(chr(ch), end='') - -class MyIO(io.IOBase): - def __init__(self, read=False, write=False): - self.ready_rd = False # Read and write not ready - self.wch = b'' - if read: - self.rbuf = b'ready\n' # Read buffer - pyb.Timer(4, freq = 1, callback = self.do_input) - if write: - 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 - - # Emulate a device with buffered read. Return the buffer, falsify read ready - # Read timer sets ready. - def readline(self): - self.ready_rd = False - return self.rbuf - - # 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) - -def test(good=True): - if good: - myior = MyIO(read=True) - myiow = MyIO(write=True) - else: - myior = MyIO(read=True, write=True) - myiow = myior - loop = asyncio.get_event_loop() - loop.create_task(receiver(myior)) - loop.create_task(sender(myiow)) - loop.run_forever() From 68ff1f8cf02446c7832ca9e9bdfd7cafd8e6942d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 19 Jun 2018 13:39:14 +0100 Subject: [PATCH 034/472] Tutorial: update stream I/O information. --- FASTPOLL.md | 5 +++ README.md | 3 +- TUTORIAL.md | 114 +++++++++++++++++++++++++++++++++++++-------------- aledflash.py | 5 +-- apoll.py | 5 +-- aqtest.py | 5 +-- 6 files changed, 93 insertions(+), 44 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 28cb42a..b07b1c9 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -4,6 +4,11 @@ This document describes a "priority" version of uasyncio. Its purpose is to provide a simple priority mechanism to facilitate the design of applications with improved millisecond-level timing accuracy and reduced scheduling latency. +I remain hopeful that uasyncio will mature natively to support fast I/O +polling: if this occurs I plan to deprecate this solution. See +[this thread](https://github.com/micropython/micropython/pull/3836#issuecomment-397317408) +and [this one](https://github.com/micropython/micropython/issues/2664). + V0.3 Feb 2018. A single module designed to work with the official `uasyncio` library. This requires `uasyncio` V2.0 which requires firmware dated 22nd Feb 2018 or later. diff --git a/README.md b/README.md index fe85c12..cb088a9 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,8 @@ Classes `Task` and `Future` are not supported. ## 3.1 Asynchronous I/O Asynchronous I/O (`StreamReader` and `StreamWriter` classes) support devices -with streaming drivers, such as UARTs and sockets. +with streaming drivers, such as UARTs and sockets. It is now possible to write +streaming device drivers in Python. ## 3.2 Time values diff --git a/TUTORIAL.md b/TUTORIAL.md index 603b765..f48d554 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -121,13 +121,13 @@ and rebuilding. 5. [Device driver examples](./TUTORIAL.md#5-device-driver-examples) - 5.1 [The IORead mechnaism](./TUTORIAL.md#51-the-ioread-mechanism) + 5.1 [Using the IORead mechnanism](./TUTORIAL.md#51-using-the-ioread-mechanism) 5.1.1 [A UART driver example](./TUTORIAL.md#511-a-uart-driver-example) - 5.2 [Using a coro to poll hardware](./TUTORIAL.md#52-using-a-coro-to-poll-hardware) + 5.2 [Writing IORead device drivers](./TUTORIAL.md#52-writing-ioread-device-drivers) - 5.3 [Using IORead to poll hardware](./TUTORIAL.md#53-using-ioread-to-poll-hardware) + 5.3 [Polling hardware without IORead](./TUTORIAL.md#53-polling-hardware-without-ioread) 5.4 [A complete example: aremote.py](./TUTORIAL.md#54-a-complete-example-aremotepy) A driver for an IR remote control receiver. @@ -216,6 +216,7 @@ results by accessing Pyboard hardware. 11. `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` Demo of a read/write device driver using the IORead mechanism. **Test Programs** @@ -1012,11 +1013,11 @@ acquire data. In the case of a driver written in Python this must be done by having a coro which does this periodically. This may present problems if there is a requirement for rapid polling owing to the round-robin nature of uasyncio scheduling: the coro will compete for execution with others. There are two -solutions to this. One is to use the experimental version of uasyncio presented -[here](./FASTPOLL.md). +solutions to this. The official solution is to delegate polling to the +scheduler using the IORead mechanism. This is currently subject to limitations. -The other potential solution is to delegate the polling to the scheduler using -the IORead mechanism. This is unsupported for Python drivers: see section 5.3. +An alternative is to use the experimental version of uasyncio presented +[here](./FASTPOLL.md). Note that where a very repeatable polling interval is required, it should be done using a hardware timer with a hard interrupt callback. For "very" @@ -1044,7 +1045,7 @@ which offers a means of reducing this latency for critical tasks. ###### [Contents](./TUTORIAL.md#contents) -## 5.1 The IORead Mechanism +## 5.1 Using the IORead 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 @@ -1073,19 +1074,20 @@ loop.create_task(receiver()) loop.run_forever() ``` -The supporting code may be found in `__init__.py` in the uasyncio library. +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`, `write`, `readline` and `close`. See -section 5.3 for further discussion. +following methods: `ioctl`, `read`, `readline` and `write`. See +[section 5.2](./TUTORIAL.md#52-writing-ioread-device-drivers) for details on +how such drivers may be written in Python. A UART can receive data at any time. The IORead 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 all coros minimise blocking periods 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. +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. ### 5.1.1 A UART driver example @@ -1111,7 +1113,72 @@ returned. See the code comments for more details. ###### [Contents](./TUTORIAL.md#contents) -## 5.2 Using a coro to poll hardware +## 5.2 Writing IORead device drivers + +The `IORead` 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. + +It should be noted that currently the task polling I/O devices effectively runs +in round-robin fashion along with other coroutines. This is arguably sub +optimal: [see this GitHub RFC](https://github.com/micropython/micropython/issues/2664). + +A device driver capable of employing the IORead 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 if you plan 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 +MP_STREAM_POLL_RD = const(1) +MP_STREAM_POLL_WR = const(4) +MP_STREAM_POLL = const(3) +MP_STREAM_ERROR = const(-1) + + 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 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 woking. +See [this GitHub thread](https://github.com/micropython/micropython/pull/3836#issuecomment-397317408). +The workround is to write two separate drivers, one read-only and the other +write-only. + +###### [Contents](./TUTORIAL.md#contents) + +## 5.3 Polling hardware without IORead This is a simple approach, but is only appropriate to hardware which is to be polled at a relatively low rate. This is for two reasons. Firstly the variable @@ -1193,21 +1260,6 @@ loop.run_until_complete(run()) ###### [Contents](./TUTORIAL.md#contents) -## 5.3 Using IORead to poll hardware - -The uasyncio `IORead` class is provided to support IO to stream devices. It -may be employed by drivers of devices which need to be polled: the polling will -be delegated to the scheduler which uses `select` to schedule the first -stream or device driver to be ready. This is more efficient, and offers lower -latency, than running multiple coros each polling a device. - -At the time of writing firmware support for using this mechanism in device -drivers written in Python has not been implemented, and the final comment to -[this](https://github.com/micropython/micropython/issues/2664) issue suggests -that it may never be done. So streaming device drivers must be written in C. - -###### [Contents](./TUTORIAL.md#contents) - ## 5.4 A complete example: aremote.py This may be found in the `nec_ir` directory. Its use is documented diff --git a/aledflash.py b/aledflash.py index ea40854..420a0d4 100644 --- a/aledflash.py +++ b/aledflash.py @@ -5,10 +5,7 @@ # Run on MicroPython board bare hardware import pyb -try: - import asyncio_priority as asyncio -except ImportError: - import uasyncio as asyncio +import uasyncio as asyncio async def killer(duration): await asyncio.sleep(duration) diff --git a/apoll.py b/apoll.py index 963c8c0..eeff59a 100644 --- a/apoll.py +++ b/apoll.py @@ -5,10 +5,7 @@ # Author: Peter Hinch # Copyright Peter Hinch 2017 Released under the MIT license -try: - import asyncio_priority as asyncio -except ImportError: - import uasyncio as asyncio +import uasyncio as asyncio import pyb import utime as time diff --git a/aqtest.py b/aqtest.py index 2216f09..afb5ffb 100644 --- a/aqtest.py +++ b/aqtest.py @@ -2,10 +2,7 @@ # Author: Peter Hinch # Copyright Peter Hinch 2017 Released under the MIT license -try: - import asyncio_priority as asyncio -except ImportError: - import uasyncio as asyncio +import uasyncio as asyncio from uasyncio.queues import Queue From 3cff4de5c6241cc1b73e35b2d7df6d24846f0e84 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 19 Jun 2018 19:03:09 +0100 Subject: [PATCH 035/472] Tutorial: further update to stream I/O. --- TUTORIAL.md | 297 +++++++++++++++++++++++++++++----------------------- 1 file changed, 166 insertions(+), 131 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index f48d554..23f06a0 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -119,20 +119,22 @@ and rebuilding. 4.5 [Exceptions](./TUTORIAL.md#45-exceptions) - 5. [Device driver examples](./TUTORIAL.md#5-device-driver-examples) + 5. [Interfacing hardware](./TUTORIAL.md#5-interfacing-hardware) - 5.1 [Using the IORead mechnanism](./TUTORIAL.md#51-using-the-ioread-mechanism) + 5.1 [Timing issues](./TUTORIAL.md#51-timing-issues) - 5.1.1 [A UART driver example](./TUTORIAL.md#511-a-uart-driver-example) + 5.2 [Polling hardware with a coroutine](./TUTORIAL.md#52-polling-hardware-with-a-coroutine) - 5.2 [Writing IORead device drivers](./TUTORIAL.md#52-writing-ioread-device-drivers) + 5.3 [Using the IORead mechnanism](./TUTORIAL.md#53-using-the-ioread-mechanism) - 5.3 [Polling hardware without IORead](./TUTORIAL.md#53-polling-hardware-without-ioread) + 5.3.1 [A UART driver example](./TUTORIAL.md#531-a-uart-driver-example) - 5.4 [A complete example: aremote.py](./TUTORIAL.md#54-a-complete-example-aremotepy) + 5.4 [Writing IORead device drivers](./TUTORIAL.md#54-writing-ioread-device-drivers) + + 5.5 [A complete example: aremote.py](./TUTORIAL.md#55-a-complete-example-aremotepy) A driver for an IR remote control receiver. - 5.5 [Driver for HTU21D](./TUTORIAL.md#55-htu21d-environment-sensor) A + 5.6 [Driver for HTU21D](./TUTORIAL.md#56-htu21d-environment-sensor) A temperature and humidity sensor. 6. [Hints and tips](./TUTORIAL.md#6-hints-and-tips) @@ -380,7 +382,7 @@ 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 5](./TUTORIAL.md#5-device-driver-examples). +this is discussed further in [Section 5](./TUTORIAL.md#5-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 @@ -1006,46 +1008,161 @@ a keyboard interrupt should trap the exception at the event loop level. ###### [Contents](./TUTORIAL.md#contents) -# 5 Device driver examples - -Many devices such as sensors are read-only in nature and need to be polled to -acquire data. In the case of a driver written in Python this must be done by -having a coro which does this periodically. This may present problems if there -is a requirement for rapid polling owing to the round-robin nature of uasyncio -scheduling: the coro will compete for execution with others. There are two -solutions to this. The official solution is to delegate polling to the -scheduler using the IORead mechanism. This is currently subject to limitations. - -An alternative is to use the experimental version of uasyncio presented -[here](./FASTPOLL.md). - -Note that where a very repeatable polling interval is required, it should be -done using a hardware timer with a hard interrupt callback. For "very" -repeatable read microsecond level (depending on platform). - -In many cases less precise timing is acceptable. The definition of "less" is -application dependent but the latency associated with scheduling the coro which -is performing the polling may be variable on the order of tens or hundreds of -milliseconds. Latency is determined as follows. When `await asyncio.sleep(0)` -is issued all other pending coros will be scheduled in "fair round-robin" -fashion before it is re-scheduled. Thus its worst-case latency may be +# 5 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 `IORead` 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#52-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 `IORead`. 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 `IORead` is discussed +[here](./TUTORIAL.md#53-using-the-ioread-mechanism). + +###### [Contents](./TUTORIAL.md#contents) + +## 5.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. -If `await asyncio.sleep_ms(t)` is issued where t > 0 the coro is guaranteed not -to be rescheduled until t has elapsed. If, at that time, all other coros are -waiting on nonzero delays, it will immediately be scheduled. But if other coros -are pending execution (either because they issued a zero delay or because their -time has elapsed) they may be scheduled first. This introduces a timing -uncertainty into the `sleep()` and `sleep_ms()` functions. The worst-case value -for this may be calculated as above. +There is an experimental version of uasyncio presented [here](./FASTPOLL.md). +This provides for callbacks which run on every iteration of the scheduler +enabling a coro to wait on an event with much reduced latency. It is hoped +that improvements to `uasyncio` will remove the need for this in future. + +###### [Contents](./TUTORIAL.md#contents) -[This document](./FASTPOLL.md) describes an experimental version of uasyncio -which offers a means of reducing this latency for critical tasks. +## 5.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 +IORead. + +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 __await__(self): + data = b'' + while not data.endswith(self.DELIMITER): + yield from asyncio.sleep(0) # Neccessary 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 + + __iter__ = __await__ # workround for issue #2678 + + 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) -## 5.1 Using the IORead Mechanism +## 5.3 Using the IORead 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 @@ -1077,8 +1194,8 @@ 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 -[section 5.2](./TUTORIAL.md#52-writing-ioread-device-drivers) for details on -how such drivers may be written in Python. +[Writing IORead device drivers](./TUTORIAL.md#54-writing-ioread-device-drivers) +for details on how such drivers may be written in Python. A UART can receive data at any time. The IORead mechanism checks for pending incoming characters whenever the scheduler has control. When a coro is running @@ -1089,7 +1206,7 @@ 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. -### 5.1.1 A UART driver example +### 5.3.1 A UART driver example The program `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 @@ -1113,7 +1230,7 @@ returned. See the code comments for more details. ###### [Contents](./TUTORIAL.md#contents) -## 5.2 Writing IORead device drivers +## 5.4 Writing IORead device drivers The `IORead` 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 @@ -1123,8 +1240,8 @@ handlers for any devices which are ready. This is more efficient than running multiple coros each polling a device. It should be noted that currently the task polling I/O devices effectively runs -in round-robin fashion along with other coroutines. This is arguably sub -optimal: [see this GitHub RFC](https://github.com/micropython/micropython/issues/2664). +in round-robin fashion along with other coroutines. Arguably this could be +improved: [see this GitHub RFC](https://github.com/micropython/micropython/issues/2664). A device driver capable of employing the IORead mechanism may support `StreamReader`, `StreamWriter` instances or both. A readable device must @@ -1178,89 +1295,7 @@ write-only. ###### [Contents](./TUTORIAL.md#contents) -## 5.3 Polling hardware without IORead - -This is a simple approach, but is only appropriate to hardware which is to be -polled at a relatively low rate. This is for two reasons. Firstly the variable -latency caused by the execution of other coros will result in variable polling -intervals - this may or may not matter depending on the device and application. -Secondly, attempting to poll with a short 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 -IORead. - -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 __await__(self): - data = b'' - while not data.endswith(self.DELIMITER): - yield from asyncio.sleep(0) # Neccessary 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 - - __iter__ = __await__ # workround for issue #2678 - - 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) - -## 5.4 A complete example: aremote.py +## 5.5 A complete example: aremote.py This may be found in the `nec_ir` directory. Its use is documented [here](./nec_ir/README.md). The demo provides a complete device driver example: @@ -1277,7 +1312,7 @@ any asyncio latency when setting its delay period. ###### [Contents](./TUTORIAL.md#contents) -## 5.5 HTU21D environment sensor +## 5.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 From 1fc16fbf30d716e04b6e76d4764c32e2e95d5066 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 27 Jun 2018 16:44:57 +0100 Subject: [PATCH 036/472] Documentation updates. --- FASTPOLL.md | 11 ++++++----- README.md | 7 +++++-- TUTORIAL.md | 11 ++++++----- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index b07b1c9..675b19a 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -1,13 +1,14 @@ -# A modified version of uasyncio +# An experimental modified version of uasyncio This document describes a "priority" version of uasyncio. Its purpose is to provide a simple priority mechanism to facilitate the design of applications with improved millisecond-level timing accuracy and reduced scheduling latency. -I remain hopeful that uasyncio will mature natively to support fast I/O -polling: if this occurs I plan to deprecate this solution. See -[this thread](https://github.com/micropython/micropython/pull/3836#issuecomment-397317408) -and [this one](https://github.com/micropython/micropython/issues/2664). +I am hopeful that uasyncio will support fast I/O polling and have a +[PR](https://github.com/micropython/micropython-lib/pull/287) in place to +implement this. If this (or other solution) is implemented I will deprecate +this module as the I/O mechanism is inherently more efficient with polling +implemented in C. V0.3 Feb 2018. A single module designed to work with the official `uasyncio` library. This requires `uasyncio` V2.0 which requires firmware dated diff --git a/README.md b/README.md index cb088a9..6d0e31c 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,11 @@ This GitHub repository consists of the following parts: * [A modified uasyncio](./FASTPOLL.md) This incorporates a simple priority mechanism. With suitable application design this improves the rate at which devices can be polled and improves the accuracy of time delays. Also provides - for low priority tasks which are only scheduled when normal tasks are paused. - NOTE: this requires uasyncio V2.0. + for low priority tasks which are only scheduled when normal tasks are paused. + NOTE1: this requires uasyncio V2.0. + NOTE2: I have a PR in place which, if accepted, will largely supersede this + with a faster and more efficient way of handling fast I/O. This modified + version should be regarded as "experimental". It may stop being supported. * [Communication between devices](./syncom_as/README.md) Enables MicroPython boards to communicate without using a UART. Primarily intended to enable a a Pyboard-like device to achieve bidirectional communication with an ESP8266. diff --git a/TUTORIAL.md b/TUTORIAL.md index 23f06a0..9520680 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -908,11 +908,9 @@ comes from the `Lock` class: If the `async with` has an `as variable` clause the variable receives the value returned by `__aenter__`. -Note there is currently a bug in the implementation whereby if an explicit -`return` is issued within an `async with` block, the `__aexit__` method -is not called. The solution is to design the code so that in all cases it runs -to completion. The error appears to be in [PEP492](https://www.python.org/dev/peps/pep-0492/). -See [this issue](https://github.com/micropython/micropython/issues/3153). +There was a bug in the implementation whereby if an explicit `return` was issued +within an `async with` block, the `__aexit__` method was not called. This was +fixed as of 27th June 2018 [ref](https://github.com/micropython/micropython/pull/3890). ###### [Contents](./TUTORIAL.md#contents) @@ -1269,11 +1267,14 @@ 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: From bfdb2e24498885bd63c3ea80da71013a7215347a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 5 Jul 2018 11:07:39 +0100 Subject: [PATCH 037/472] Major update: asyncio_priority is replaced by a modified version of uasyncio. This is because of the advent of io.IOBase which enables stream I/O drivers written in Python. The modified uasyncio version includes an option to schedule stream I/O at high priority which allows faster, more efficient, high priority device drivers to be written. --- FASTPOLL.md | 454 +++++++---------------- README.md | 40 +- TUTORIAL.md | 151 ++++++-- UNDER_THE_HOOD.md | 240 ++++++++++++ astests.py | 6 +- aswitch.py | 5 +- asyn.py | 22 +- asyncio_priority.py | 244 ------------ asyntest.py | 10 +- awaitable.py | 7 +- benchmarks/call_lp.py | 39 -- benchmarks/latency.py | 122 ------ benchmarks/overdue.py | 37 -- benchmarks/priority.py | 111 ------ benchmarks/{rate_p.py => rate_fastio.py} | 18 +- benchmarks/timing.py | 111 ------ fast_io/__init__.py | 284 ++++++++++++++ fast_io/core.py | 347 +++++++++++++++++ fast_io/ms_timer.py | 33 ++ fast_io/ms_timer_test.py | 28 ++ fast_io/pin_cb.py | 47 +++ fast_io/pin_cb_test.py | 53 +++ iorw.py | 101 +++++ priority_test.py | 79 ---- roundrobin.py | 5 +- 25 files changed, 1434 insertions(+), 1160 deletions(-) create mode 100644 UNDER_THE_HOOD.md delete mode 100644 asyncio_priority.py delete mode 100644 benchmarks/call_lp.py delete mode 100644 benchmarks/latency.py delete mode 100644 benchmarks/overdue.py delete mode 100644 benchmarks/priority.py rename benchmarks/{rate_p.py => rate_fastio.py} (66%) delete mode 100644 benchmarks/timing.py create mode 100644 fast_io/__init__.py create mode 100644 fast_io/core.py create mode 100644 fast_io/ms_timer.py create mode 100644 fast_io/ms_timer_test.py create mode 100644 fast_io/pin_cb.py create mode 100644 fast_io/pin_cb_test.py create mode 100644 iorw.py delete mode 100644 priority_test.py diff --git a/FASTPOLL.md b/FASTPOLL.md index 675b19a..e509ff9 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -1,22 +1,34 @@ -# An experimental modified version of uasyncio - -This document describes a "priority" version of uasyncio. Its purpose is to -provide a simple priority mechanism to facilitate the design of applications -with improved millisecond-level timing accuracy and reduced scheduling latency. - -I am hopeful that uasyncio will support fast I/O polling and have a -[PR](https://github.com/micropython/micropython-lib/pull/287) in place to -implement this. If this (or other solution) is implemented I will deprecate -this module as the I/O mechanism is inherently more efficient with polling -implemented in C. - -V0.3 Feb 2018. A single module designed to work with the official `uasyncio` -library. This requires `uasyncio` V2.0 which requires firmware dated -22nd Feb 2018 or later. - -**API CHANGES** -V2.0 of `uasyncio` changed the arguments to `get_event_loop` so this version -has corresponding changes. See [section 3](./FASTPOLL.md#3-a-solution). +# fast_io: An experimental modified version of uasyncio + +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 aysnchronously 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 current `uasyncio` polls I/O with a relatively high degree of +latency. It also has a bug whereby bidirectional devices such as UARTS could +fail to handle concurrent input and output. + +This version has the following changes: + * I/O can optionally be handled at a higher priority than other coroutines + [PR287](https://github.com/micropython/micropython-lib/pull/287). + * The bug with read/write device drivers is fixed (forthcoming PR). + * 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) + +A key advantage of this version is that priority device drivers are written +entirely by using the officially-supported technique for writing stream I/O +drivers. If official `uasyncio` acquires a means of prioritising I/O by other +means than by these proposals, application code changes are likely to be +minimal. Using the priority mechanism in this version requires a change to just +one line of code compared to an application running under the official version. + +The high priority mechanism in `asyncio_priority.py` is replaced with a faster +and more efficient way of handling asynchronous events with minimum latency. +Consequently `asyncio_priority.py` is obsolete and should be deleted from your +system. The facility for low priority coros is currently unavailable. ###### [Main README](./README.md) @@ -34,34 +46,20 @@ has corresponding changes. See [section 3](./FASTPOLL.md#3-a-solution). 2.3 [Polling in uasyncio](./FASTPOLL.md#23-polling-in-usayncio) - 2.4 [Background](./FASTPOLL.md#24-background) - - 3. [A solution](./FASTPOLL.md#3-a-solution) - - 3.1 [Low priority yield](./FASTPOLL.md#31-low-priority-yield) + 3. [The modified version](./FASTPOLL.md#3-the-modified-version) - 3.1.1 [Task Cancellation and Timeouts](./FASTPOLL.md#311-task-cancellation-and-timeouts) + 4. [ESP Platforms](./FASTPOLL.md#4-esp-platforms) - 3.2 [Low priority callbacks](./FASTPOLL.md#32-low-priority-callbacks) - - 3.3 [High priority tasks](./FASTPOLL.md#33-high-priority-tasks) - - 4. [The asyn library](./FASTPOLL.md#4-the-asyn-library) - - 5. [Heartbeat](./FASTPOLL.md#5-heartbeat) - - 6. [ESP Platforms](./FASTPOLL.md#6-esp-platforms) + 5. [Background](./FASTPOLL.md#4-background) # 1. Installation -Install and test uasyncio on the target hardware. Copy `asyncio_priority.py` -to the target. Users of previous versions should update any of the benchmark -programs which are to be run. +Install and test uasyncio on the target hardware. Replace `core.py` and +`__init__.py` with the files in the `fast_io` directory. In MicroPython 1.9 `uasyncio` was implemented as a frozen module on the -ESP8266. This version is not compatible with `asyncio_priority.py`. Given the -limited resources of the ESP8266 `uasyncio` and `uasyncio_priority` should be -implemented as frozen bytecode. See +ESP8266. To install this version it is necessary to build the firmware with the +above two files implemented as frozen bytecode. See [ESP Platforms](./FASTPOLL.md#6-esp-platforms) for general comments on the suitability of ESP platforms for systems requiring fast response. @@ -71,41 +69,37 @@ The benchmarks directory contains files demonstrating the performance gains offered by prioritisation. They also offer illustrations of the use of these features. Documentation is in the code. - * `benchmarks/latency.py` Shows the effect on latency with and without low - priority usage. - * `benchmarks/timing.py` Shows the effect on timing with and without low - priority usage. - * ``benchmarks/rate.py` Shows the frequency with which the official uasyncio - schedules minimal coroutines (coros). - * `benchmarks/rate_p.py` As above, but measures the overhead of the priority - extension. - * `benchmarks/call_lp.py` Demos low priority callbacks. - * `benchmarks/overdue.py` Demo of maximum overdue feature. - * `benchmarks/priority.py` Demo of high priority coro. - * `priority_test.py` Cancellation of low priority coros. - -With the exceptions of call_lp and priority.py, benchmarks can be run against -the official and priority versions of usayncio. + * ``benchmarks/rate.py` Shows the frequency with which uasyncio schedules + minimal coroutines (coros). + * `benchmarks/rate_esp.py` As above for ESP32 and ESP8266. + * `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. + * `fast_io/ms_timer.py` Provides higher precision timing than `wait_ms()`. + * `fast_io/ms_timer.py` Test/demo program for above. + * `fast_io/pin_cb.py` Demo of an I/O device driver which causes a pin state + change to trigger a callback. + * `fast_io/pin_cb_test.py` Demo of above. + +With the exception of `rate_fastio`, benchmarks can be run against the official +and priority versions of usayncio. # 2. Rationale 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. +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. - -This variant mitigates this by enabling 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 can exceed two orders of magnitude. +execution. Delays can easily exceed the nominal value by an order of magnitude. -It also provides for fast scheduling where a user supplied callback is tested -on every iteration of the scheduler. This minimises latency at some cost to -overall performance. +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. ## 2.1 Latency @@ -132,24 +126,18 @@ 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 -adequate to avoid overruns. +suficient to avoid overruns. -This version provides a mechanism for reducing this latency 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 -250μs - a figure close to the inherent latency of uasyncio. +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. -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 250μs plus the worst-case delay between yields of -any one competing task. +### 2.1.1 I/O latency -Where a coro must respond rapidly to an event, the scheduler can test a user -supplied callback on every iteration. See -[section 3.3](./FASTPOLL.md#33-high-priority-tasks). +The current 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. -###### [Jump to Contents](./FASTPOLL.md#contents) +###### [Contents](./FASTPOLL.md#contents) ## 2.2 Timing accuracy @@ -173,280 +161,88 @@ 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). The -priority version can improve this substantially. The degree of improvement -is dependent on other coros regularly yielding with low priority: if any coro -hogs execution for a substantial period that will inevitably contribute to -latency in a cooperative system. +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: -In the somewhat contrived example of 200 tasks each issuing a low priority -yield every 2ms, a 10ms nominal delay produced times in the range 9.8 to 10.8ms -contrasing to 407.9 to 410.9ms using normal scheduling. +```python +async def timer_test(n): + timer = ms_timer.MillisecTimer() + while True: + await timer(30) # More precise timing + # Code +``` -The benchmark timing.py demonstrates this. Documentation is in the code. It can -be run against the official and priority versions. +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. -###### [Jump to Contents](./FASTPOLL.md#contents) +###### [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. It is important to appreciate that these mechanisms have the same -drawback as the polling loop: uasyncio schedules tasks by placing them on a -`utimeq` queue. This is a queue sorted by time-to-run. Tasks which are ready -to run are scheduled in "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). - -A partial solution is to design the competing `foo()` tasks to minimise the -delay between yields to the scheduler. This can be difficult or impossible. -Further it is inefficient to reduce the delay much below 2ms as the scheduler -takes ~200μs to schedule a task. - -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. - -###### [Jump to Contents](./FASTPOLL.md#contents) - -## 2.4 Background +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 has been discussed in detail -[in issue 2989](https://github.com/micropython/micropython/issues/2989). +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). -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). The final -comment to this issue suggests that it may never be done for drivers written in -Python. While a driver written in C is an undoubted solution, the purpose of -MicroPython is arguably to facilitate coding in Python where possible. - -It seems there is no plan to incorporate a priority mechanism in the official -verion of uasyncio but I believe it confers significant advantages for the -reasons discussed above. Hence this variant. +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. -###### [Jump to Contents](./FASTPOLL.md#contents) +###### [Contents](./FASTPOLL.md#contents) -# 3. A solution +# 3. The modified version -The module enables coroutines to yield to the scheduler with three levels of -priority, one with higher and one with lower priority than standard. It -provides a replacement for `uasyncio.get_event_loop()` enabling the queue -sizes to be set. +The `fast_io` version adds an `ioq_len=0` argument to `get_event_loop`. The +zero default causes the scheduler to operate as per the official version. If an +I/O queue length > 0 is provided, I/O performed by `StreamReader` and +`StreamWriter` objects will be prioritised over other coros. -`aysncio_priority.get_event_loop(runq_len, waitq_len, lpqlen)` -Arguments: +Arguments to `get_event_loop()`: 1. `runq_len` Length of normal queue. Default 16 tasks. 2. `waitq_len` Length of wait queue. Default 16. - 3. `lpqlen` Length of low priority queue. Default 16. - -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 optional high priority mechanism adds "when" implying scheduling when a -condition is met. The library adds the following awaitable instances: - - * `after(t)` Low priority version of `sleep(t)`. - * `after_ms(t)` LP version of `sleep_ms(t)`. - * `when(callback)` Re-schedules when the callback returns True. - -It adds the following event loop methods: + 3. `ioq_len` Length of I/O queue. Default 0. - * `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. +Device drivers which are to be capable of running at high priority should be +written to use stream I/O: see +[Writing IORead device drivers](./TUTORIAL.md#54-writing-ioread-device-drivers). -See [Low priority callbacks](./FASTPOLL.md#32-low-priority-callbacks) +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. -## 3.1 Low priority yield +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. -Consider this code fragment: - -```python -import asyncio_priority as asyncio -loop = asyncio.get_event_loop() - -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 asyncio_priority as asyncio - -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(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 become overdue by -more than 1s. +###### [Contents](./FASTPOLL.md#contents) -### 3.1.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#36-task-cancellation) -and [Coroutines with timeouts](./TUTORIAL.md#44-coroutines-with-timeouts). - -###### [Jump to Contents](./FASTPOLL.md#contents) - -## 3.2 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` 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. - -###### [Jump to Contents](./FASTPOLL.md#contents) - -## 3.3 High priority tasks - -Where latency must be reduced to the absolute minimum, a condition may be -tested on every iteration of the scheduler. This involves yielding a callback -function which returns a boolean. When a coro yields to the scheduler, each -pending callback is run until one returns `True` when that task is run. If -there are no pending callbacks which return `True` it will schedule other -tasks. - -This machanism should be used only if the application demands it. Caution is -required since running the callbacks inevitably impacts the performance of -the scheduler. To minimise this callbacks should be short (typically returning -a boolean flag set by a hard interrupt handler) and the number of high priority -tasks should be small. - -The benchmark priority.py demonstrates and tests this mechanism. - -To yield at high priority issue - -```python -import asyncio_priority as asyncio - -async def foo(): - while True: - await asyncio.when(callback) # Pauses until callback returns True - # Code omitted - typically queue received data for processing - # by another coro -``` - -Pending callbacks are stored in a list which grows dynamically. An application -will typically have only one or two coroutines which wait on callbacks so the -list will never grow beyond this length. - -In the current implementation the callback takes no arguments. However it can -be a bound method, enabling it to access class and instance variables. - -No means of scheduling a high priority callback analogous to `call_soon` is -provided. If such a mechanism existed, the cb would run immediately the coro -yielded, with the coro being rescheduled once the cb returned `True`. This -behaviour can be achieved more efficiently by simply calling the function. - -###### [Jump to Contents](./FASTPOLL.md#contents) - -## 4. The asyn library - -This now uses the low priority (LP) mechanism if available and where -appropriate. It is employed as follows: - - * `Lock` class. Uses normal scheduling on the basis that locks should be - held for brief periods only. - * `Event` class. An optional boolean constructor arg, defaulting `False`, - specifies LP scheduling (if available). A `True` value provides for cases - where response to an event is not time-critical. - * `Barrier`, `Semaphore` and `BoundedSemaphore` classes use LP - scheduling if available. This is on the basis that typical code may wait on - these objects for some time. - -A coro waiting on a `Lock` or an `Event` which uses normal scheduling will -therefore prevent the execution of LP tasks for the duration. - -###### [Jump to Contents](./FASTPOLL.md#contents) - -# 5. Heartbeat - -I find it useful to run a "heartbeat" coro in development as a simple check -for code which has failed to yield. If the low priority mechanism is used this -can be extended to check that no coro loops indefinitely on a zero delay. - -```python -async def heartbeat(led): - while True: - led.toggle() - await after_ms(500) # Will hang while a coro loops on a zero delay -``` - -###### [Jump to Contents](./FASTPOLL.md#contents) - -# 6. ESP Platforms +# 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). diff --git a/README.md b/README.md index 6d0e31c..c32d01f 100644 --- a/README.md +++ b/README.md @@ -18,28 +18,36 @@ This GitHub repository consists of the following parts: * [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. - * [A modified uasyncio](./FASTPOLL.md) This incorporates a simple priority - mechanism. With suitable application design this improves the rate at which - devices can be polled and improves the accuracy of time delays. Also provides - for low priority tasks which are only scheduled when normal tasks are paused. - NOTE1: this requires uasyncio V2.0. - NOTE2: I have a PR in place which, if accepted, will largely supersede this - with a faster and more efficient way of handling fast I/O. This modified - version should be regarded as "experimental". It may stop being supported. * [Communication between devices](./syncom_as/README.md) Enables MicroPython boards to communicate without using a UART. Primarily intended to enable a a Pyboard-like device to achieve bidirectional communication with an ESP8266. + * [Under the hood](./UNDER_THE_HOOD.md) A guide to help understand the + `uasyncio` code. Strictly for scheduler geeks... + +## 1.1 A new "priority" version. + +This repo included `asyncio_priority.py` which is now deprecated. Its primary +purpose was to provide a means of servicing fast hardware devices by means of +coroutines running at a high priority. The official firmware now includes +[this major improvement](https://github.com/micropython/micropython/pull/3836) +which offers a much more efficient way of achieving this end. The tutorial has +details of how to use this. + +The current `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. + +A modified version of `uasyncio` is described [here](./FASTPOLL.md) which +provides an option for I/O scheduling with much reduced latency. It also fixes +the bug. It is hoped that these changes will be accepted into mainstream in due +course. # 2. Version and installation of uasyncio The documentation and code in this repository are based on `uasyncio` version -2.0, which is the version on PyPi. This requires firmware dated 22nd Feb 2018 -or later. - -Version 2.0 brings only one API change over V1.7.1, namely the arguments to -`get_event_loop()`. Unless using the priority version all test programs and -code samples use default args so will work under either version. The priority -version requires the later version and firmware. +2.0, which is the version on PyPi and in the official micropython-lib. This +requires firmware dated 22nd Feb 2018 or later. Use of the IORead mechanism +requires firmware after 17th June 2018. See [tutorial](./TUTORIAL.md#installing-uasyncio-on-bare-metal) for installation instructions. @@ -50,7 +58,7 @@ 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 nonstandard extensions to optimise services +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. diff --git a/TUTORIAL.md b/TUTORIAL.md index 9520680..00406a9 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -192,9 +192,6 @@ The following modules are provided which may be copied to the target hardware. 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. `asyncio_priority.py` An experimental version of uasyncio with a simple - priority mechanism. See [this doc](./FASTPOLL.md). Note that this does not yet - support `uasyncio` V2.0. **Demo Programs** @@ -639,8 +636,6 @@ controlled. Documentation of this is in the code. ## 3.6 Task cancellation -This requires `uasyncio` V1.7.1 or later, with suitably recent firmware. - `uasyncio` now provides 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. However there is a @@ -649,7 +644,7 @@ limitation. If a coro issues `await uasyncio.sleep(secs)` or This introduces latency into cancellation which matters in some use-cases. Other potential sources of latency take the form of slow code. `uasyncio` has no mechanism for verifying when cancellation has actually occurred. The `asyn` -library provides solutions via the following classes: +library provides verification via the following classes: 1. `Cancellable` This allows one or more tasks to be assigned to a group. A coro can cancel all tasks in the group, pausing until this has been acheived. @@ -678,7 +673,7 @@ illustration of the mechanism a cancellable task is defined as below: ```python @asyn.cancellable -async def print_nums(_, num): +async def print_nums(num): while True: print(num) num += 1 @@ -696,6 +691,10 @@ async def foo(): print('Done') ``` +**Note** It is bad practice to issue the close() method to 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) ## 3.7 Other synchronisation primitives @@ -1235,11 +1234,9 @@ 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. - -It should be noted that currently the task polling I/O devices effectively runs -in round-robin fashion along with other coroutines. Arguably this could be -improved: [see this GitHub RFC](https://github.com/micropython/micropython/issues/2664). +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 IORead mechanism may support `StreamReader`, `StreamWriter` instances or both. A readable device must @@ -1288,11 +1285,117 @@ class MyIO(io.IOBase): 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()`. +With the [priority version](./FASTPOLL.md) it offers much more precise delays +under a common usage scenario. + +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 [priority version](./FASTPOLL.md) 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 woking. See [this GitHub thread](https://github.com/micropython/micropython/pull/3836#issuecomment-397317408). -The workround is to write two separate drivers, one read-only and the other -write-only. +There are two solutions. A workround is to write two separate drivers, one +read-only and the other write-only. Alternatively an experimental version +of `uasyncio` is [documented here](./FASTPOLL.md) which addresses this and +also enables the priority of I/O to be substantially raised. + +In the official `uasyncio` is scheduled quite infrequently. See +[see this GitHub RFC](https://github.com/micropython/micropython/issues/2664). +The experimental version addresses this issue. ###### [Contents](./TUTORIAL.md#contents) @@ -1436,6 +1539,10 @@ 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. +I have [a PR](https://github.com/micropython/micropython-lib/pull/292) which +proposes a fix for this. The [experimental fast_io](./FASTPOLL.md) version +implements this fix. + The script `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 @@ -1443,8 +1550,8 @@ 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). +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. @@ -1480,6 +1587,9 @@ issues which require rather unpleasant hacks for error-free operation. The file `sock_nonblock.py` illustrates the sort of techniques required. It is not a working demo, and solutions are likely to be application dependent. +An alternative approach is to use blocking sockets with `StreamReader` and +`StreamWriter` instances to control polling. + ###### [Contents](./TUTORIAL.md#contents) # 7 Notes for beginners @@ -1774,11 +1884,10 @@ handles the data and clears the flag. A better approach is to use an `Event`. # 8 Modifying uasyncio -The library is designed to be extensible, an example being the -`asyncio_priority` module. By following the following 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. +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. Assume that the aim is to alter the event loop. The module should issue diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md new file mode 100644 index 0000000..0abe99a --- /dev/null +++ b/UNDER_THE_HOOD.md @@ -0,0 +1,240 @@ +# 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, +also studying the code, experiment and inference. There may be errors, in which +case please raise an issue. None of the information here is required to use the +library. + +It assumes a good appreciation of the use of `uasyncio`. Familiarity with +Python generators is also recommended, in particular the use of `yield from` +and appreciating 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 `uasyncio` may be found in micropython-lib in the following +directories: + +``` +uasyncio/uasyncio/__init__.py +uasyncio.core/uasyncio/core.py +``` + +# 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 (see below). + +# 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. + +## 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. + * `IOWriteDone` + +The `IO*` classes are for the exclusive use of `StreamReader` and `StreamWriter` +objects. + +# 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 `EventLoop` maintains two queues, `.runq` and `.waitq`. 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. Tasks on the wait queue 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. + +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. + +## 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. + +# Stream I/O + +This description of stream I/O is based on my code rather than the official +version. + +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`). + +## 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. + +## 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. + +## PollEventLoop.wait() + +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. diff --git a/astests.py b/astests.py index 94396a7..6846061 100644 --- a/astests.py +++ b/astests.py @@ -7,11 +7,7 @@ from machine import Pin from pyb import LED from aswitch import Switch, Pushbutton -# Verify it works under standard and priority version -try: - import asyncio_priority as asyncio -except ImportError: - import uasyncio as asyncio +import uasyncio as asyncio helptext = ''' Test using switch or pushbutton between X1 and gnd. diff --git a/aswitch.py b/aswitch.py index b086633..94cc059 100644 --- a/aswitch.py +++ b/aswitch.py @@ -28,10 +28,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -try: - import asyncio_priority as asyncio -except ImportError: - import uasyncio as asyncio +import uasyncio as asyncio import utime as time from asyn import launch # launch: run a callback or initiate a coroutine depending on which is passed. diff --git a/asyn.py b/asyn.py index 68c1d10..df7762d 100644 --- a/asyn.py +++ b/asyn.py @@ -30,18 +30,11 @@ # CPython 3.5 compatibility # (ignore RuntimeWarning: coroutine '_g' was never awaited) -# Check availability of 'priority' version try: - import asyncio_priority as asyncio - p_version = True + import uasyncio as asyncio except ImportError: - p_version = False - try: - import uasyncio as asyncio - except ImportError: - import asyncio + import asyncio -after = asyncio.after if p_version else asyncio.sleep async def _g(): pass @@ -102,11 +95,8 @@ def release(self): # A coro rasing the event issues event.set() # When all waiting coros have run # event.clear() should be issued -# Use of low_priority may be specified in the constructor -# when it will be used if available. class Event(): - def __init__(self, lp=False): - self.after = after if (p_version and lp) else asyncio.sleep + def __init__(self, lp=False): # Redundant arg retained for compatibility self.clear() def clear(self): @@ -115,7 +105,7 @@ def clear(self): def __await__(self): while not self._flag: - yield from self.after(0) + yield __iter__ = __await__ @@ -162,7 +152,7 @@ def __await__(self): while True: # Wait until last waiting thread changes the direction if direction != self._down: return - yield from after(0) + yield __iter__ = __await__ @@ -209,7 +199,7 @@ async def __aexit__(self, *args): async def acquire(self): while self._count == 0: - await after(0) + yield self._count -= 1 def release(self): diff --git a/asyncio_priority.py b/asyncio_priority.py deleted file mode 100644 index 08d6939..0000000 --- a/asyncio_priority.py +++ /dev/null @@ -1,244 +0,0 @@ -# asyncio_priority.py Modified version of uasyncio with priority mechanism. - -# Updated 18th Dec 2017 for uasyncio.core V1.6 -# New low priority algorithm reduces differences in run_forever compared to -# standard 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 utime as time -import utimeq -import ucollections -from uasyncio import * - -class PriorityEventLoop(PollEventLoop): - def __init__(self, runq_len=16, waitq_len=16, lpqlen=42): - super().__init__(runq_len, waitq_len) - self._max_overdue_ms = 0 - self.lpq = utimeq.utimeq(lpqlen) - self.hp_tasks = [] - - def max_overdue_ms(self, t=None): - if t is not None: - self._max_overdue_ms = int(t) - return self._max_overdue_ms - - # 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): - self.lpq.push(time, callback, args) - - def _schedule_hp(self, func, callback, *args): - # If there's an empty slot, assign without allocation - for entry in self.hp_tasks: # O(N) search - but N is typically 1 or 2... - if not entry[0]: - entry[0] = func - entry[1] = callback - entry[2] = args - break - else: - self.hp_tasks.append([func, callback, args]) - -# Low priority (LP) scheduling. -# Schedule a single low priority task if one is ready or overdue. -# The most overdue task is scheduled even if normal tasks are pending. -# The most due task is scheduled only if no normal tasks are pending. - - def run_forever(self): - cur_task = [0, 0, 0] - while True: - 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_overdue_ms > 0 and tim < -self._max_overdue_ms - 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) - self.call_soon(cur_task[1], *cur_task[2]) - - # Expire entries in waitq and move them to runq - 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]) - self.call_soon(cur_task[1], *cur_task[2]) - - # Process runq - l = len(self.runq) - if __debug__ and DEBUG: - log.debug("Entries in runq: %d", l) - while l: - # Check list of high priority tasks - cb = None - for entry in self.hp_tasks: - if entry[0] and entry[0](): # Ready to run - entry[0] = 0 - cb = entry[1] - args = entry[2] - break - - if cb is None: - cb = self.runq.popleft() - l -= 1 - args = () - if not isinstance(cb, type_gen): - args = self.runq.popleft() - l -= 1 - if __debug__ and DEBUG: - log.info("Next callback to run: %s", (cb, args)) - cb(*args) - continue - - if __debug__ and DEBUG: - log.info("Next coroutine to run: %s", (cb, args)) - self.cur_task = cb - delay = 0 - func = None - low_priority = False # Assume normal priority - try: - if args is (): - ret = next(cb) - else: - ret = cb.send(*args) - if __debug__ and DEBUG: - log.info("Coroutine %s yield result: %s", cb, ret) - if isinstance(ret, SysCall1): - 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, When): - if callable(arg): - func = arg - else: - assert False, "Argument to 'when' must be a function or method." - elif isinstance(ret, IORead): - cb.pend_throw(False) - self.add_reader(arg, cb) - continue - elif isinstance(ret, IOWrite): - cb.pend_throw(False) - self.add_writer(arg, cb) - continue - elif isinstance(ret, IOReadDone): - self.remove_reader(arg) - elif isinstance(ret, IOWriteDone): - self.remove_writer(arg) - elif isinstance(ret, StopLoop): - return arg - else: - assert False, "Unknown syscall yielded: %r (of type %r)" % (ret, type(ret)) - elif isinstance(ret, type_gen): - self.call_soon(ret) - elif isinstance(ret, int): - # Delay - delay = ret - elif ret is None: - # Just reschedule - pass - elif ret is False: - # Don't reschedule - 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 - if func is not None: - self._schedule_hp(func, 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) # waitq - else: - self.call_soon(cb) # runq - - # Wait until next waitq task or I/O availability - delay = 0 - if not self.runq: - delay = -1 - tnow = self.time() - if self.waitq: - 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 - self.wait(delay) - -# Low priority -class AfterMs(SleepMs): - pass - -class After(AfterMs): - pass - -# High Priority -class When(SleepMs): - pass - -after_ms = AfterMs() -after = After() -when = When() - -import uasyncio.core -uasyncio.core._event_loop_class = PriorityEventLoop -def get_event_loop(runq_len=16, waitq_len=16, lpqlen=16): - if uasyncio.core._event_loop is None: # Add a q entry for lp_monitor() - uasyncio.core._event_loop = uasyncio.core._event_loop_class(runq_len, waitq_len, lpqlen) - return uasyncio.core._event_loop diff --git a/asyntest.py b/asyntest.py index 3d98bcb..2c7e7c2 100644 --- a/asyntest.py +++ b/asyntest.py @@ -37,7 +37,7 @@ def print_tests(): st = '''Available functions: print_tests() Print this list. ack_test() Test event acknowledge. -event_test(lp=True) Test Event and Lock objects. If lp use low priority mechanism. +event_test() Test Event and Lock objects. barrier_test() Test the Barrier class. semaphore_test(bounded=False) Test Semaphore or BoundedSemaphore. condition_test() Test the Condition class. @@ -143,7 +143,7 @@ async def eventwait(event): print('got event') event.clear() -async def run_event_test(lp): +async def run_event_test(): print('Test Lock class') loop = asyncio.get_event_loop() lock = asyn.Lock() @@ -151,13 +151,13 @@ async def run_event_test(lp): loop.create_task(run_lock(2, lock)) loop.create_task(run_lock(3, lock)) print('Test Event class') - event = asyn.Event(lp) + 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(lp=True): # Option to use low priority scheduling +def event_test(): printexp('''Test Lock class Test Event class waiting for event @@ -177,7 +177,7 @@ def event_test(lp=True): # Option to use low priority scheduling Tasks complete ''', 5) loop = asyncio.get_event_loop() - loop.run_until_complete(run_event_test(lp)) + loop.run_until_complete(run_event_test()) # ************ Barrier test ************ diff --git a/awaitable.py b/awaitable.py index c0fe9d6..a9087f6 100644 --- a/awaitable.py +++ b/awaitable.py @@ -5,12 +5,9 @@ # Trivial fix for MicroPython issue #2678 try: - import asyncio_priority as asyncio + import uasyncio as asyncio except ImportError: - try: - import uasyncio as asyncio - except ImportError: - import asyncio + import asyncio class Hardware(object): def __init__(self, count): diff --git a/benchmarks/call_lp.py b/benchmarks/call_lp.py deleted file mode 100644 index 57110b5..0000000 --- a/benchmarks/call_lp.py +++ /dev/null @@ -1,39 +0,0 @@ -# call_lp.py Demo of low priority callback. Author Peter Hinch April 2017. -# Requires experimental version of core.py - -try: - import asyncio_priority as asyncio -except ImportError: - print('This demo requires asyncio_priority.py') -import pyb - -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/benchmarks/latency.py b/benchmarks/latency.py deleted file mode 100644 index 5fec4eb..0000000 --- a/benchmarks/latency.py +++ /dev/null @@ -1,122 +0,0 @@ -# latency.py Benchmark for uasyncio. Author Peter Hinch May 2017. - -# 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. - -try: - import asyncio_priority as asyncio - lp_version = True -except ImportError: - import uasyncio as asyncio - 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 asyncio_priority.py') - else: - ntasks = max(num_coros) + 10 #4 - if use_priority: - loop = asyncio.get_event_loop(ntasks, ntasks, 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/benchmarks/overdue.py b/benchmarks/overdue.py deleted file mode 100644 index 068f785..0000000 --- a/benchmarks/overdue.py +++ /dev/null @@ -1,37 +0,0 @@ -# overdue.py Test for "low priority" uasyncio. Author Peter Hinch April 2017. -try: - import asyncio_priority as asyncio -except ImportError: - print('This demo requires asyncio_priority.py') - -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/benchmarks/priority.py b/benchmarks/priority.py deleted file mode 100644 index 57b3da3..0000000 --- a/benchmarks/priority.py +++ /dev/null @@ -1,111 +0,0 @@ -# priority.py Demonstrate high priority scheduling in modified uasyncio. -# Author Peter Hinch May 2017. - -# Measures the maximum latency of a high priority task. This tests a flag set -# by a timer interrupt to ensure a realistic measurement. The "obvious" way, -# using a coro to set the flag, produces unrealistically optimistic results -# because the scheduler is started immediately after the flag is set. - -try: - import asyncio_priority as asyncio -except ImportError: - print('This demo requires asyncio_priority.py') -import pyb -import utime as time -import gc -import micropython -micropython.alloc_emergency_exception_buf(100) - -n_hp_tasks = 2 # Number of high priority tasks -n_tasks = 4 # Number of normal priority tasks - -max_latency = 0 # Results: max latency of priority task -tmax = 0 # Latency of normal task -tmin = 1000000 - -class DummyDeviceDriver(): - def __iter__(self): - yield - -# boolean flag records time between setting and clearing it. -class Flag(): - def __init__(self): - self.flag = False - self.time_us = 0 - - def __call__(self): - return self.flag - - def set_(self): - self.flag = True - self.time_us = time.ticks_us() - - def clear(self): - self.flag = False - return time.ticks_diff(time.ticks_us(), self.time_us) - -# Instantiate a flag for each priority task -flags = [Flag() for _ in range(n_hp_tasks)] - -# Wait for a flag then clear it, updating global max_latency. -async def urgent(n): - global max_latency - flag = flags[n] - while True: - # Pause until flag is set. The callback is the bound method flag.__call__() - await asyncio.when(flag) # callback is passed not using function call syntax - latency = flag.clear() # Timer ISR has set the flag. Clear it. - max_latency = max(max_latency, latency) - -# Timer callback: hard IRQ which sets a flag to be tested by a priority coro, -# set each flag in turn -nflag = 0 -def trig(t): - global nflag - flags[nflag].set_() - nflag += 1 - nflag %= n_hp_tasks - -tim = pyb.Timer(4) - - -# Have a number of normal tasks each using some CPU time -async def normal_task(delay): - while True: - time.sleep_ms(delay) # Simulate processing - await asyncio.sleep_ms(0) - -# Measure the scheduling latency of a normal task which waits on an event. -# In this instance the driver returns immediately emulating an event which has -# already occurred - so we measure the scheduling latency. -async def norm_latency(): - global tmax, tmin - device = DummyDeviceDriver() - while True: - await asyncio.sleep_ms(100) - gc.collect() # For precise timing - 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) - -# Ensure coros are running before we start the timer and measurement. -async def report(): - await asyncio.sleep_ms(100) - tim.init(freq=10) - tim.callback(trig) - await asyncio.sleep(2) - print('Max latency of urgent tasks: {}us'.format(max_latency)) - print('Latency of normal tasks: {:6.2f}ms max {:6.2f}ms min.'.format(tmax / 1000, tmin / 1000)) - tim.deinit() - -print('Test runs for two seconds.') -loop = asyncio.get_event_loop() -#loop.allocate_hpq(n_hp_tasks) # Allocate a (small) high priority queue -loop.create_task(norm_latency()) # Measure latency of a normal task -for _ in range(n_tasks): - loop.create_task(normal_task(1)) # Hog CPU for 1ms -for n in range(n_hp_tasks): - loop.create_task(urgent(n)) -loop.run_until_complete(report()) diff --git a/benchmarks/rate_p.py b/benchmarks/rate_fastio.py similarity index 66% rename from benchmarks/rate_p.py rename to benchmarks/rate_fastio.py index ba5e31e..3e957f2 100644 --- a/benchmarks/rate_p.py +++ b/benchmarks/rate_fastio.py @@ -1,18 +1,12 @@ -# rate_p.py Benchmark for asyncio_priority.py aiming to measure overhead of -# this version. Compare results with those from rate.py which uses the official -# version. -# Author Peter Hinch Feb 2018. +# 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. -# Outcome: minimal coros are scheduled at an interval of ~190us, independent of -# the number of instances. Overhead relative to official version ~25%. +# Outcome: minimal coros are scheduled at an interval of ~200us -try: - import asyncio_priority as asyncio -except ImportError: - print('This demo requires asyncio_priority.py') +import uasyncio as asyncio num_coros = (100, 200, 500, 1000) iterations = [0, 0, 0, 0] @@ -46,8 +40,8 @@ async def test(): iterations[n] = count done = True -ntasks = max(num_coros) + 3 -loop = asyncio.get_event_loop(ntasks, ntasks) +ntasks = max(num_coros) + 2 +loop = asyncio.get_event_loop(ntasks, ntasks, 6) loop.create_task(test()) loop.run_until_complete(report()) diff --git a/benchmarks/timing.py b/benchmarks/timing.py deleted file mode 100644 index 8578bc3..0000000 --- a/benchmarks/timing.py +++ /dev/null @@ -1,111 +0,0 @@ -# timing.py Benchmark for uasyncio. Author Peter Hinch May 2017. - -# This measures the accuracy of uasyncio.sleep_ms() in the presence of a number of -# other coros. This can test asyncio_priority.py which incorporates the priority -# mechanism. (In the home directory of this repo). - -# Outcome: when the priority mechanism is used the worst-case 10ms delay was 11.0ms -# With the normal algorithm the 10ms delay takes ~N*Dms where N is the number of -# lp_task() instances and D is the lp_task() processing delay (2ms). -# So for 200 coros the 10ms delay takes up to 411ms. - - -try: - import asyncio_priority as asyncio - lp_version = True -except ImportError: - import uasyncio as asyncio - 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) -lst_tmin = [tmin] * len(num_coros) -lst_sd = [0] * len(num_coros) - -async def report(target_delay): - # 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) - print('Nominal delay of priority task was {}ms.'.format(target_delay)) - s = 'Coros {:4d} Actual delay = {: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) # LP yield - -async def priority(ms): - global tmax, tmin, dtotal, count - while True: - gc.collect() # GC was affecting result - tstart = time.ticks_us() - await asyncio.sleep_ms(ms) # Measure the actual delay - 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, ms_delay): - global done, tmax, tmin, dtotal, count - loop.create_task(priority(ms_delay)) - 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 - 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, lp_version, loop - target_delay = 10 # Nominal delay in priority task (ms) - 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 asyncio_priority.py') - else: - ntasks = max(num_coros) + 4 - if use_priority: - loop = asyncio.get_event_loop(ntasks, ntasks, 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 accuracy of {}ms nominal delay with coros blocking for {}ms.' - print(s.format(target_delay, processing_delay)) - if lp_version: - print('Using priority mechanism.') - else: - print('Not using priority mechanism.') - loop.create_task(run_test(processing_delay, target_delay)) - loop.run_until_complete(report(target_delay)) - -print('Issue timing.test() to test priority mechanism, timing.test(False) to test standard algo.') diff --git a/fast_io/__init__.py b/fast_io/__init__.py new file mode 100644 index 0000000..825cef6 --- /dev/null +++ b/fast_io/__init__.py @@ -0,0 +1,284 @@ +import uerrno +import uselect as select +import usocket as _socket +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") + +# add_writer causes read failure if passed the same sock instance as was passed +# to add_reader. Cand we fix this by maintaining two object maps? +class PollEventLoop(EventLoop): + + def __init__(self, runq_len=16, waitq_len=16, fast_io=0): + EventLoop.__init__(self, runq_len, waitq_len, fast_io) + 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 + if flags: + self.flags[id(sock)] = flags + self.poller.register(sock, flags) + else: + del self.flags[id(sock)] + self.poller.unregister(sock) + del objmap[id(sock)] + + # 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)) + self._register(sock, select.POLLIN) + 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)) + self._register(sock, select.POLLOUT) + 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)] + # Test code. Invalidate objmap: this ensures an exception is thrown + # rather than exhibiting weird behaviour when testing. + self.wrobjmap[id(sock)] = None # TEST + if DEBUG and __debug__: + log.debug("Calling IO callback: %r", cb) + if isinstance(cb, tuple): + cb[0](*cb[1]) + else: + cb.pend_throw(None) + self._call_io(cb) + if ev & select.POLLIN: + cb = self.rdobjmap[id(sock)] + self.rdobjmap[id(sock)] = None # TEST + 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: + cb.pend_throw(None) + 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) + 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) + return res + + def readexactly(self, n): + buf = b"" + while n: + yield IORead(self.polls) + res = self.ios.read(n) + assert res is not None + if not res: + 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) + res = self.ios.readline() + assert res is not None + 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: + res = self.s.write(buf, off, sz) + # If we spooled everything, return immediately + if res == sz: + if DEBUG and __debug__: + log.debug("StreamWriter.awrite(): completed spooling %d bytes", res) + yield IOWriteDone(self.s) + return + if res is None: + res = 0 + if DEBUG and __debug__: + log.debug("StreamWriter.awrite(): spooled partial %d bytes", res) + assert res < sz + off += res + sz -= res + yield IOWrite(self.s) + #assert s2.fileno() == self.s.fileno() + if DEBUG and __debug__: + log.debug("StreamWriter.awrite(): can write more") + + # 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) + while True: + 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} + yield client_coro(StreamReader(s2), StreamWriter(s2, extra)) + + +import uasyncio.core +uasyncio.core._event_loop_class = PollEventLoop diff --git a/fast_io/core.py b/fast_io/core.py new file mode 100644 index 0000000..28f6afb --- /dev/null +++ b/fast_io/core.py @@ -0,0 +1,347 @@ +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): + self.runq = ucollections.deque((), runq_len, True) + self.ioq_len = ioq_len + 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), 'Generator function is not iterable.' # upy issue #3241 + self.call_later_ms(0, coro) + # CPython asyncio incompatibility: we don't return Task object + + def _call_now(self, callback, *args): + 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 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) + + 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] + while True: + # Expire entries in waitq and move them to runq + tnow = self.time() + 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]) + self.call_soon(cur_task[1], *cur_task[2]) + + # Process runq + 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 + while l or self.ioq_len: + if self.ioq_len: + self.wait(0) # Schedule I/O. Can append to ioq. + if self.ioq: + cur_q = self.ioq + dl = 0 + elif l == 0: + break + else: + cur_q = self.runq + dl = 1 + l -= dl + cb = cur_q.popleft() + args = () + if not isinstance(cb, type_gen): + args = cur_q.popleft() + l -= dl + if __debug__ and DEBUG: + log.info("Next callback to run: %s", (cb, args)) + cb(*args) + continue + + if __debug__ and DEBUG: + log.info("Next coroutine to run: %s", (cb, args)) + self.cur_task = cb + delay = 0 + try: + if args is (): + ret = next(cb) + else: + ret = cb.send(*args) + if __debug__ and DEBUG: + log.info("Coroutine %s yield result: %s", cb, ret) + if isinstance(ret, SysCall1): + arg = ret.arg + if isinstance(ret, SleepMs): + delay = arg + elif isinstance(ret, IORead): + cb.pend_throw(False) + self.add_reader(arg, cb) + continue + elif isinstance(ret, IOWrite): + cb.pend_throw(False) + self.add_writer(arg, cb) + continue + elif isinstance(ret, IOReadDone): + self.remove_reader(arg) + self._call_io(cb, args) # Next call produces StopIteration + continue + elif isinstance(ret, IOWriteDone): + self.remove_writer(arg) + self._call_io(cb, args) + continue + elif isinstance(ret, StopLoop): + return arg + else: + assert False, "Unknown syscall yielded: %r (of type %r)" % (ret, type(ret)) + elif isinstance(ret, type_gen): + self.call_soon(ret) + elif isinstance(ret, int): + # Delay + delay = ret + elif ret is None: + # Just reschedule + pass + elif ret is False: + # Don't reschedule + 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 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 + self.wait(delay) + + def run_until_complete(self, coro): + assert not isinstance(coro, type_genf), 'Generator function is not iterable.' # upy issue #3241 + def _run_and_stop(): + yield from coro + yield StopLoop(0) + self.call_soon(_run_and_stop()) + 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): + global _event_loop + if _event_loop is None: + _event_loop = _event_loop_class(runq_len, waitq_len, ioq_len) + return _event_loop + +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: + _event_loop.call_soon(coro) + + +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()) + #print("prev pend", prev) + if prev is False: + _event_loop.call_soon(timeout_obj.coro) + + 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 + +# +# 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/fast_io/ms_timer.py b/fast_io/ms_timer.py new file mode 100644 index 0000000..f539289 --- /dev/null +++ b/fast_io/ms_timer.py @@ -0,0 +1,33 @@ +# 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/fast_io/ms_timer_test.py b/fast_io/ms_timer_test.py new file mode 100644 index 0000000..4c6e999 --- /dev/null +++ b/fast_io/ms_timer_test.py @@ -0,0 +1,28 @@ +# 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 + +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_forever() + +print('Run test() to test fast I/O, test(False) to test normal I/O.') diff --git a/fast_io/pin_cb.py b/fast_io/pin_cb.py new file mode 100644 index 0000000..91e69e2 --- /dev/null +++ b/fast_io/pin_cb.py @@ -0,0 +1,47 @@ +# 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/fast_io/pin_cb_test.py b/fast_io/pin_cb_test.py new file mode 100644 index 0000000..69cdc9f --- /dev/null +++ b/fast_io/pin_cb_test.py @@ -0,0 +1,53 @@ +# ********* 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 + +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_forever() + +print('''Link Pyboard pins X1 and X2. +Issue ctrl-D between runs. +test() args: +fast_io=True test fast I/O mechanism. +latency=False test latency (delay between X1 and X3 leading edge)''') diff --git a/iorw.py b/iorw.py new file mode 100644 index 0000000..f3a8502 --- /dev/null +++ b/iorw.py @@ -0,0 +1,101 @@ +# 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/priority_test.py b/priority_test.py deleted file mode 100644 index eb580eb..0000000 --- a/priority_test.py +++ /dev/null @@ -1,79 +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 -try: - import asyncio_priority as asyncio - p_version = True -except ImportError: - p_version = False - -if not p_version: - print('This program tests and therefore requires asyncio_priority.') - -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/roundrobin.py b/roundrobin.py index 096b397..5aefae1 100644 --- a/roundrobin.py +++ b/roundrobin.py @@ -9,10 +9,7 @@ # Note using yield in a coro is "unofficial" and may not # work in future uasyncio revisions. -try: - import asyncio_priority as asyncio -except ImportError: - import uasyncio as asyncio +import uasyncio as asyncio count = 0 period = 5 From f69b615429469c860cfa2df9a12fa381f31f8c03 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 5 Jul 2018 12:02:51 +0100 Subject: [PATCH 038/472] UNDER_THE_HOOD.md reviewed and updated. --- UNDER_THE_HOOD.md | 48 +++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index 0abe99a..b32555f 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -2,11 +2,15 @@ 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, -also studying the code, experiment and inference. There may be errors, in which -case please raise an issue. None of the information here is required to use the -library. +studying the code, experiment and inference. There may be errors, in which case +please raise an issue. None of the information here is required to use the +library: it is intended to satisfy the curiosity of scheduler geeks. -It assumes a good appreciation of the use of `uasyncio`. Familiarity with +Where the versions differ, the explanation relates to the `fast_io` version. +Differences are largely in `__init__.py`: the scheduling algorithm in `core.py` +is little changed. + +This doc assumes a good appreciation of the use of `uasyncio`. Familiarity with Python generators is also recommended, in particular the use of `yield from` and appreciating the difference between a generator and a generator function: @@ -19,12 +23,11 @@ def gen_func(n): # gen_func is a generator function my_gen = gen_func(7) # my_gen is a generator ``` -The code for `uasyncio` may be found in micropython-lib in the following -directories: +The code for the `fast_io` variant of `uasyncio` may be found in: ``` -uasyncio/uasyncio/__init__.py -uasyncio.core/uasyncio/core.py +fast_io/__init__.py +fast_io/core.py ``` # Generators and coroutines @@ -101,7 +104,7 @@ The following subclasses exist: interface. * `IOWrite` Causes an interface to be polled for ready to accept data. `.arg` is the interface. - * `IOReadDone` These stop polling of an interface. + * `IOReadDone` These stop polling of an interface (in `.arg`). * `IOWriteDone` The `IO*` classes are for the exclusive use of `StreamReader` and `StreamWriter` @@ -113,11 +116,14 @@ 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 `EventLoop` maintains two queues, `.runq` and `.waitq`. 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. Tasks on the wait queue are sorted in order -of the time when they are to run, the task having the soonest time to run at -the top. +The `fast_io` `EventLoop` maintains three queues, `.runq`, `.waitq` and `.ioq`, +although `.ioq` is only instantiated if it is specified. Official `uasyncio` +does not have `.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 the wait queue 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 @@ -140,7 +146,8 @@ 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. +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. @@ -164,10 +171,15 @@ 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. -# Stream I/O +## Task Cancellation -This description of stream I/O is based on my code rather than the official -version. +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. + +# Stream I/O 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 From 3941ce53cb199d3a2c9940442ba8411f61cba683 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 5 Jul 2018 15:36:38 +0100 Subject: [PATCH 039/472] Tutorial improvements. --- README.md | 2 +- TUTORIAL.md | 67 ++++++++++++++++++++++++++++------------------------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index c32d01f..2840d9a 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ course. The documentation and code in this repository are based on `uasyncio` version 2.0, which is the version on PyPi and in the official micropython-lib. This -requires firmware dated 22nd Feb 2018 or later. Use of the IORead mechanism +requires firmware dated 22nd Feb 2018 or later. Use of the stream I/O mechanism requires firmware after 17th June 2018. See [tutorial](./TUTORIAL.md#installing-uasyncio-on-bare-metal) for diff --git a/TUTORIAL.md b/TUTORIAL.md index 00406a9..d45b27e 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -125,11 +125,11 @@ and rebuilding. 5.2 [Polling hardware with a coroutine](./TUTORIAL.md#52-polling-hardware-with-a-coroutine) - 5.3 [Using the IORead mechnanism](./TUTORIAL.md#53-using-the-ioread-mechanism) + 5.3 [Using the stream mechnanism](./TUTORIAL.md#53-using-the-stream-mechanism) 5.3.1 [A UART driver example](./TUTORIAL.md#531-a-uart-driver-example) - 5.4 [Writing IORead device drivers](./TUTORIAL.md#54-writing-ioread-device-drivers) + 5.4 [Writing streaming device drivers](./TUTORIAL.md#54-writing-streaming-device-drivers) 5.5 [A complete example: aremote.py](./TUTORIAL.md#55-a-complete-example-aremotepy) A driver for an IR remote control receiver. @@ -186,8 +186,8 @@ The following modules are provided which may be copied to the target hardware. **Libraries** 1. `asyn.py` Provides synchronisation primitives `Lock`, `Event`, `Barrier`, - `Semaphore` and `BoundedSemaphore`. Provides support for task cancellation via - `NamedTask` and `Cancellable` classes. + `Semaphore`, `BoundedSemaphore`, `Condition` and `gather`. Provides support + for task cancellation via `NamedTask` and `Cancellable` classes. 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 @@ -215,7 +215,7 @@ results by accessing Pyboard hardware. 11. `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` Demo of a read/write device driver using the IORead mechanism. + 12. `iorw.py` Demo of a read/write device driver using the stream I/O mechanism. **Test Programs** @@ -247,7 +247,6 @@ Consider the following example: ```python import uasyncio as asyncio -loop = asyncio.get_event_loop() async def bar(): count = 0 while True: @@ -255,6 +254,7 @@ async def bar(): print(count) await asyncio.sleep(1) # Pause 1s +loop = asyncio.get_event_loop() loop.create_task(bar()) # Schedule ASAP loop.run_forever() ``` @@ -272,11 +272,12 @@ loop's `run_until_complete` method. Examples of this may be found in the `astests.py` module. The event loop instance is a singleton, instantiated by a program's first call -to `asyncio.get_event_loop()`. This takes an optional integer arg being the -length of the coro queue - i.e. the maximum number of concurrent coros allowed. -The default of 42 is likely to be adequate for most purposes. If a coro needs -to call an event loop method, calling `asyncio.get_event_loop()` (without -args) will efficiently return it. +to `asyncio.get_event_loop()`. This takes two optional integer args being the +lengths of the two coro queues - i.e. the maximum number of concurrent coros +allowed. The default of 16 is likely to be adequate for most purposes. + +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) @@ -317,7 +318,8 @@ the `roundrobin.py` example. 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 and blocks until it has run to completion. + 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. @@ -395,14 +397,15 @@ compete to access a single resource. An example is provided in the `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. A more -elegant approach is to use synchronisation primitives. The module +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 partially superseded by an official implementation. +is an alterantive 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` @@ -460,9 +463,9 @@ ineffective. It will not receive the `TimeoutError` until it has acquired the lock. The same observation applies to task cancellation. The module `asyn.py` offers a `Lock` class which works in these situations -[full details](./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. +[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) @@ -691,9 +694,9 @@ async def foo(): print('Done') ``` -**Note** It is bad practice to issue the close() method to 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. +**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) @@ -1015,7 +1018,7 @@ 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 `IORead` mechanism which is a system designed for stream +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: @@ -1034,10 +1037,10 @@ an awaitable class might be used. Explicit polling is discussed further [below](./TUTORIAL.md#52-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 `IORead`. This polls devices using +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 `IORead` is discussed -[here](./TUTORIAL.md#53-using-the-ioread-mechanism). +and more efficient than explicit polling. The use of `stream I/O` is discussed +[here](./TUTORIAL.md#53-using-the-stream-mechanism). ###### [Contents](./TUTORIAL.md#contents) @@ -1099,7 +1102,7 @@ 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 -IORead. +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 @@ -1159,7 +1162,7 @@ loop.run_until_complete(run()) ###### [Contents](./TUTORIAL.md#contents) -## 5.3 Using the IORead Mechanism +## 5.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 @@ -1191,10 +1194,10 @@ 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 IORead device drivers](./TUTORIAL.md#54-writing-ioread-device-drivers) +[Writing streaming device drivers](./TUTORIAL.md#54-writing-streaming-device-drivers) for details on how such drivers may be written in Python. -A UART can receive data at any time. The IORead mechanism checks for pending +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 @@ -1227,9 +1230,9 @@ returned. See the code comments for more details. ###### [Contents](./TUTORIAL.md#contents) -## 5.4 Writing IORead device drivers +## 5.4 Writing streaming device drivers -The `IORead` mechanism is provided to support I/O to stream devices. Its +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 @@ -1238,7 +1241,7 @@ 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 IORead mechanism may support +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 From 78bb13338644586c6b880fe931d5db17282c210b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 6 Jul 2018 08:42:21 +0100 Subject: [PATCH 040/472] Tutorial improvements. --- TUTORIAL.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index d45b27e..47efa32 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -266,15 +266,16 @@ 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. -Many embedded applications have an event loop which runs continuously. The event +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. Examples of this may be found in the -`astests.py` module. +loop's `run_until_complete` method; this is mainly of use in testing. Examples +may be found in the `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 - i.e. the maximum number of concurrent coros -allowed. The default of 16 is likely to be adequate for most purposes. +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 a coro needs to call an event loop method (usually `create_task`), calling `asyncio.get_event_loop()` (without args) will efficiently return it. @@ -754,7 +755,8 @@ loop = asyncio.get_event_loop() loop.run_until_complete(bar()) ``` -Currently MicroPython doesn't support `__await__` (issue #2678) and +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. @@ -912,19 +914,17 @@ value returned by `__aenter__`. There was a bug in the implementation whereby if an explicit `return` was issued within an `async with` block, the `__aexit__` method was not called. This was -fixed as of 27th June 2018 [ref](https://github.com/micropython/micropython/pull/3890). +fixed as of 27th June 2018 [PR 3890](https://github.com/micropython/micropython/pull/3890). ###### [Contents](./TUTORIAL.md#contents) ## 4.4 Coroutines with timeouts -This requires uasyncio.core V1.7 which was released on 16th Dec 2017, with -firmware of that date or later. - Timeouts are implemented by means of `uasyncio.wait_for()`. This takes as arguments a coroutine and a timeout in seconds. If the timeout expires a -`TimeoutError` will be thrown to the coro. The next time the coro is scheduled -for execution the exception will be raised: the coro should trap this and quit. +`TimeoutError` will be thrown to the coro in such a way that the next time the +coro is scheduled for execution the exception will be raised. The coro should +trap this and quit. ```python import uasyncio as asyncio @@ -946,10 +946,11 @@ loop = asyncio.get_event_loop() loop.run_until_complete(foo()) ``` -Note that if the coro awaits a long delay, it will not be rescheduled until the -time has elapsed. The `TimeoutError` will occur as soon as the coro is -scheduled. But in real time and from the point of view of the calling coro, its -response to the `TimeoutError` will correspondingly be delayed. +Note that if the coro issues `await asyncio.sleep(t)` where `t` is a long delay +it 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). @@ -961,10 +962,9 @@ or in a coro which is awaiting its completion. This ensures that the exception is not propagated to the scheduler. If this occurred it would stop running, passing the exception to the code which started the scheduler. -Using `throw` to throw an exception to a coro is unwise. It subverts the design -of `uasyncio` by forcing the coro to run, and possibly terminate, when it is -still queued for execution. I haven't entirely thought through the implications -of this, but it's a thoroughly bad idea. +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. From fa38336f111b1c692e7d54e389f78bbedee6b4f4 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 6 Jul 2018 10:33:10 +0100 Subject: [PATCH 041/472] Improvements to docs. --- README.md | 2 +- TUTORIAL.md | 126 +++++++++++++++------------------------------- UNDER_THE_HOOD.md | 36 +++++++++++++ 3 files changed, 77 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 2840d9a..ea3c840 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ This GitHub repository consists of the following parts: boards to communicate without using a UART. Primarily intended to enable a a Pyboard-like device to achieve bidirectional communication with an ESP8266. * [Under the hood](./UNDER_THE_HOOD.md) A guide to help understand the - `uasyncio` code. Strictly for scheduler geeks... + `uasyncio` code. For scheduler geeks and those wishing to modify `uasyncio`. ## 1.1 A new "priority" version. diff --git a/TUTORIAL.md b/TUTORIAL.md index 47efa32..2638e24 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -139,19 +139,17 @@ and rebuilding. 6. [Hints and tips](./TUTORIAL.md#6-hints-and-tips) - 6.1 [Coroutines are generators](./TUTORIAL.md#61-coroutines-are-generators) + 6.1 [Program hangs](./TUTORIAL.md#61-program-hangs) - 6.2 [Program hangs](./TUTORIAL.md#62-program-hangs) + 6.2 [uasyncio retains state](./TUTORIAL.md#62-uasyncio-retains-state) - 6.3 [uasyncio retains state](./TUTORIAL.md#63-uasyncio-retains-state) + 6.3 [Garbage Collection](./TUTORIAL.md#63-garbage-collection) - 6.4 [Garbage Collection](./TUTORIAL.md#64-garbage-collection) + 6.4 [Testing](./TUTORIAL.md#64-testing) - 6.5 [Testing](./TUTORIAL.md#65-testing) + 6.5 [A common error](./TUTORIAL.md#65-a-common-error) This can be hard to find. - 6.6 [A common hard to find error](./TUTORIAL.md#66-a-common-error) - - 6.7 [Socket programming](./TUTORIAL.md#67-socket-programming) + 6.6 [Socket programming](./TUTORIAL.md#66-socket-programming) 7. [Notes for beginners](./TUTORIAL.md#7-notes-for-beginners) @@ -169,8 +167,6 @@ and rebuilding. 7.7 [Polling](./TUTORIAL.md#77-polling) - 8. [Modifying uasyncio](./TUTORIAL.md#8-modifying-uasyncio) - # 1. Cooperative scheduling The technique of cooperative multi-tasking is widely used in embedded systems. @@ -1042,6 +1038,10 @@ 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#53-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 5.4](./TUTORIAL.md#54-writing-streaming-device-drivers). + ###### [Contents](./TUTORIAL.md#contents) ## 5.1 Timing issues @@ -1075,10 +1075,9 @@ 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. -There is an experimental version of uasyncio presented [here](./FASTPOLL.md). -This provides for callbacks which run on every iteration of the scheduler -enabling a coro to wait on an event with much reduced latency. It is hoped -that improvements to `uasyncio` will remove the need for this in future. +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) @@ -1121,7 +1120,7 @@ class RecordOrientedUart(): self.uart = UART(4, 9600) self.data = b'' - def __await__(self): + def __iter__(self): # Not __await__ issue #2678 data = b'' while not data.endswith(self.DELIMITER): yield from asyncio.sleep(0) # Neccessary because: @@ -1130,8 +1129,6 @@ class RecordOrientedUart(): data = b''.join((data, self.uart.read(self.uart.any()))) self.data = data - __iter__ = __await__ # workround for issue #2678 - async def send_record(self, data): data = b''.join((data, self.DELIMITER)) self.uart.write(data) @@ -1251,8 +1248,7 @@ 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 if you plan to use `StreamReader.read()` or -`StreamReader.readexactly()` +Required to use `StreamReader.read()` or `StreamReader.readexactly()` A writeable driver must provide this synchronous method: `write` Args `buf`, `off`, `sz`. Arguments: @@ -1332,8 +1328,8 @@ async def timer_test(n): ``` With official `uasyncio` this confers no benefit over `await asyncio.sleep_ms()`. -With the [priority version](./FASTPOLL.md) it offers much more precise delays -under a common usage scenario. +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 @@ -1385,20 +1381,19 @@ class PinCall(io.IOBase): ``` Once again with official `uasyncio` latency can be high. Depending on -application design the [priority version](./FASTPOLL.md) can greatly reduce +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 woking. 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 an experimental version -of `uasyncio` is [documented here](./FASTPOLL.md) which addresses this and -also enables the priority of I/O to be substantially raised. +read-only and the other write-only. Alternatively the +[fast_io](./FASTPOLL.md) addresses this. -In the official `uasyncio` is scheduled quite infrequently. See +In the official `uasyncio` I/O is scheduled quite infrequently. See [see this GitHub RFC](https://github.com/micropython/micropython/issues/2664). -The experimental version addresses this issue. +The `fast_io` version addresses this issue. ###### [Contents](./TUTORIAL.md#contents) @@ -1407,7 +1402,7 @@ The experimental version addresses this issue. This may be found in the `nec_ir` directory. Its use is 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. +salient points regarding its `asyncio` usage. A pin interrupt records the time of a state change (in us) and sets an event, passing the time when the first state change occurred. A coro waits on the @@ -1415,7 +1410,7 @@ 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. +any `asyncio` latency when setting its delay period. ###### [Contents](./TUTORIAL.md#contents) @@ -1433,26 +1428,6 @@ run while acquisition is in progress. # 6 Hints and tips -## 6.1 Coroutines are generators - -In MicroPython coroutines are generators. This is not the case in CPython. -Issuing `yield` in a coro will provoke a syntax error in CPython, whereas in -MicroPython it has the same effect as `await asyncio.sleep(0)`. The surest way -to write error free code is to use CPython conventions and assume that coros -are not generators. - -The following will work. If you use them, be prepared to test your code against -each uasyncio release because the behaviour is not necessarily guaranteed. - -```python -yield from coro # Equivalent to await coro: continue when coro terminates. -yield # Reschedule current coro in round-robin fashion. -yield 100 # Pause 100ms - equivalent to above -``` - -Issuing `yield` or `yield 100` is slightly faster than the equivalent `await` -statements. - ###### [Contents](./TUTORIAL.md#contents) ## 6.1 Program hangs @@ -1532,6 +1507,7 @@ the outer loop: 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) @@ -1540,11 +1516,18 @@ data been sent to the UART at a slow rate rather than via a loopback test. 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. +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 this. The [experimental fast_io](./FASTPOLL.md) version -implements this fix. +proposes a fix for case 1. The [fast_io](./FASTPOLL.md) version implements +this. The script `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 @@ -1569,11 +1552,14 @@ 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. -## 6.7 Socket programming +###### [Contents](./TUTORIAL.md#contents) + +## 6.6 Socket programming The use of nonblocking sockets requires some attention to detail. If a nonblocking read is performed, because of server latency, there is no guarantee @@ -1587,7 +1573,7 @@ practice a timeout is likely to be required to cope with server outages. A further complication is that, at the time of writing, the ESP32 port has issues which require rather unpleasant hacks for error-free operation. -The file `sock_nonblock.py` illustrates the sort of techniques required. It is +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. An alternative approach is to use blocking sockets with `StreamReader` and @@ -1884,35 +1870,3 @@ 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) - -# 8 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. - -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 -``` diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index b32555f..28dc36d 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -30,6 +30,8 @@ fast_io/__init__.py fast_io/core.py ``` +###### [Main README](./README.md) + # Generators and coroutines In MicroPython coroutines and generators are identical: this differs from @@ -250,3 +252,37 @@ retrieved from `.rdobjmap` and queued for scheduling. This is done via I/O queue has been instantiated. Writing is handled similarly. + +# 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 +``` + +###### [Main README](./README.md) From 621b5db76bff39b43cffd9775f6b9c592fd02a36 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 6 Jul 2018 10:47:38 +0100 Subject: [PATCH 042/472] Improvements to docs. --- TUTORIAL.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 2638e24..b282ca5 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -64,9 +64,7 @@ and rebuilding. # Contents 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) @@ -1529,11 +1527,11 @@ 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` 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`. +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 @@ -1573,8 +1571,9 @@ practice a timeout is likely to be required to cope with server outages. A further complication is that, at the time of writing, the ESP32 port has issues which require rather unpleasant hacks for error-free operation. -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. +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. An alternative approach is to use blocking sockets with `StreamReader` and `StreamWriter` instances to control polling. From 043640e115990838b873c072dca2347f89835a77 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 6 Jul 2018 10:48:34 +0100 Subject: [PATCH 043/472] Improvements to docs. --- TUTORIAL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index b282ca5..d1cdd61 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -63,8 +63,8 @@ and rebuilding. # Contents - 1. [Cooperative scheduling](./TUTORIAL.md#1-cooperative-scheduling) - 1.1 [Modules](./TUTORIAL.md#11-modules) + 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) From 2feddfa0d8d6ef9b220d58ea1f9fee0bbdeb0268 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 6 Jul 2018 10:52:40 +0100 Subject: [PATCH 044/472] Improvements to docs. --- TUTORIAL.md | 146 ++++++++++++++++++---------------------------------- 1 file changed, 49 insertions(+), 97 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index d1cdd61..9eccbbd 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -65,105 +65,57 @@ and rebuilding. 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 [Task cancellation](./TUTORIAL.md#36-task-cancellation) - - 3.7 [Other synchronisation primitives](./TUTORIAL.md#37-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) - - 4.4 [Coroutines with timeouts](./TUTORIAL.md#44-coroutines-with-timeouts) - - 4.5 [Exceptions](./TUTORIAL.md#45-exceptions) - - 5. [Interfacing hardware](./TUTORIAL.md#5-interfacing-hardware) - - 5.1 [Timing issues](./TUTORIAL.md#51-timing-issues) - - 5.2 [Polling hardware with a coroutine](./TUTORIAL.md#52-polling-hardware-with-a-coroutine) - - 5.3 [Using the stream mechnanism](./TUTORIAL.md#53-using-the-stream-mechanism) - - 5.3.1 [A UART driver example](./TUTORIAL.md#531-a-uart-driver-example) - - 5.4 [Writing streaming device drivers](./TUTORIAL.md#54-writing-streaming-device-drivers) - + 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 [Task cancellation](./TUTORIAL.md#36-task-cancellation) + 3.7 [Other synchronisation primitives](./TUTORIAL.md#37-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) + 4.4 [Coroutines with timeouts](./TUTORIAL.md#44-coroutines-with-timeouts) + 4.5 [Exceptions](./TUTORIAL.md#45-exceptions) + 5. [Interfacing hardware](./TUTORIAL.md#5-interfacing-hardware) + 5.1 [Timing issues](./TUTORIAL.md#51-timing-issues) + 5.2 [Polling hardware with a coroutine](./TUTORIAL.md#52-polling-hardware-with-a-coroutine) + 5.3 [Using the stream mechnanism](./TUTORIAL.md#53-using-the-stream-mechanism) + 5.3.1 [A UART driver example](./TUTORIAL.md#531-a-uart-driver-example) + 5.4 [Writing streaming device drivers](./TUTORIAL.md#54-writing-streaming-device-drivers) 5.5 [A complete example: aremote.py](./TUTORIAL.md#55-a-complete-example-aremotepy) - A driver for an IR remote control receiver. - + A driver for an IR remote control receiver. 5.6 [Driver for HTU21D](./TUTORIAL.md#56-htu21d-environment-sensor) A - temperature and humidity sensor. - - 6. [Hints and tips](./TUTORIAL.md#6-hints-and-tips) - - 6.1 [Program hangs](./TUTORIAL.md#61-program-hangs) - - 6.2 [uasyncio retains state](./TUTORIAL.md#62-uasyncio-retains-state) - - 6.3 [Garbage Collection](./TUTORIAL.md#63-garbage-collection) - - 6.4 [Testing](./TUTORIAL.md#64-testing) - - 6.5 [A common error](./TUTORIAL.md#65-a-common-error) This can be hard to find. - - 6.6 [Socket programming](./TUTORIAL.md#66-socket-programming) - - 7. [Notes for beginners](./TUTORIAL.md#7-notes-for-beginners) - - 7.1 [Problem 1: event loops](./TUTORIAL.md#71-problem-1:-event-loops) - - 7.2 [Problem 2: blocking methods](./TUTORIAL.md#7-problem-2:-blocking-methods) - - 7.3 [The uasyncio approach](./TUTORIAL.md#73-the-uasyncio-approach) - - 7.4 [Scheduling in uasyncio](./TUTORIAL.md#74-scheduling-in-uasyncio) - - 7.5 [Why cooperative rather than pre-emptive?](./TUTORIAL.md#75-why-cooperative-rather-than-pre-emptive) - - 7.6 [Communication](./TUTORIAL.md#76-communication) - - 7.7 [Polling](./TUTORIAL.md#77-polling) + temperature and humidity sensor. + 6. [Hints and tips](./TUTORIAL.md#6-hints-and-tips) + 6.1 [Program hangs](./TUTORIAL.md#61-program-hangs) + 6.2 [uasyncio retains state](./TUTORIAL.md#62-uasyncio-retains-state) + 6.3 [Garbage Collection](./TUTORIAL.md#63-garbage-collection) + 6.4 [Testing](./TUTORIAL.md#64-testing) + 6.5 [A common error](./TUTORIAL.md#65-a-common-error) This can be hard to find. + 6.6 [Socket programming](./TUTORIAL.md#66-socket-programming) + 7. [Notes for beginners](./TUTORIAL.md#7-notes-for-beginners) + 7.1 [Problem 1: event loops](./TUTORIAL.md#71-problem-1:-event-loops) + 7.2 [Problem 2: blocking methods](./TUTORIAL.md#7-problem-2:-blocking-methods) + 7.3 [The uasyncio approach](./TUTORIAL.md#73-the-uasyncio-approach) + 7.4 [Scheduling in uasyncio](./TUTORIAL.md#74-scheduling-in-uasyncio) + 7.5 [Why cooperative rather than pre-emptive?](./TUTORIAL.md#75-why-cooperative-rather-than-pre-emptive) + 7.6 [Communication](./TUTORIAL.md#76-communication) + 7.7 [Polling](./TUTORIAL.md#77-polling) # 1. Cooperative scheduling From 86d85f3b04f567b0e75b4d7c36922e1fc743ddf7 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 6 Jul 2018 11:28:28 +0100 Subject: [PATCH 045/472] Improvements to docs. --- TUTORIAL.md | 100 ++++++++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 46 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 9eccbbd..fcdff64 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -131,10 +131,10 @@ The following modules are provided which may be copied to the target hardware. **Libraries** - 1. `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` This provides classes for interfacing switches and + 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. @@ -144,34 +144,41 @@ The following modules are provided which may be copied to the target hardware. The first two are the most immediately rewarding as they produce visible results by accessing Pyboard hardware. - 1. `aledflash.py` Flashes the four Pyboard LED's asynchronously for 10s. The - simplest uasyncio demo. Import it to run. - 2. `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. - 3. `astests.py` Test/demonstration programs for the `aswitch` module. - 4. `asyn_demos.py` Simple task cancellation demos. - 5. `roundrobin.py` Demo of round-robin scheduling. Also a benchmark of - scheduling performance. - 6. `awaitable.py` Demo of an awaitable class. One way of implementing a - device driver which polls an interface. - 7. `chain.py` Copied from the Python docs. Demo of chaining coroutines. - 8. `aqtest.py` Demo of uasyncio `Queue` class. - 9. `aremote.py` Example device driver for NEC protocol IR remote control. - 10. `auart.py` Demo of streaming I/O via a Pyboard UART. - 11. `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` Demo of a read/write device driver using the stream I/O mechanism. + 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. + 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` Tests for the synchronisation primitives in `asyn.py`. - 2. `cantest.py` Task cancellation tests. + 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` A Python3 utility to locate a particular coding - error which can be hard to find. See [this para](./TUTORIAL.md#65-a-common-error). + 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 6.5](./TUTORIAL.md#65-a-common-error). **Benchmarks** @@ -215,7 +222,7 @@ 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` module. +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 @@ -340,9 +347,10 @@ be unable to schedule other coros while the delay is in progress. 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` -program and discussed in [the docs](./DRIVERS.md). Another hazard is the "deadly -embrace" where two coros each wait on the other's completion. +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 @@ -409,10 +417,10 @@ 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` 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. +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) @@ -649,9 +657,9 @@ code even though descheduled. This is likely to have unwanted consequences. ## 3.7 Other synchronisation primitives -The `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 [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 @@ -705,7 +713,7 @@ 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. +`Barrier`, `Cancellable` and `Condition` classes in [asyn.py](./asyn.py). ### 4.1.1 Use in context managers @@ -1155,10 +1163,10 @@ provide a solution if the data source supports it. ### 5.3.1 A UART driver example -The program `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 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. @@ -1349,10 +1357,10 @@ The `fast_io` version addresses this issue. ## 5.5 A complete example: aremote.py -This may be found in the `nec_ir` directory. Its use is 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. +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 us) and sets an event, passing the time when the first state change occurred. A coro waits on the From ae0663485dc32cfd53ef8879726d541b239e2038 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 11:52:36 +0100 Subject: [PATCH 046/472] fast_io code has extra comments. --- UNDER_THE_HOOD.md | 16 +++++++----- fast_io/__init__.py | 22 +++++++++-------- fast_io/core.py | 59 +++++++++++++++++++++++---------------------- 3 files changed, 52 insertions(+), 45 deletions(-) diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index 28dc36d..ad9b3b3 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -3,16 +3,18 @@ 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 the information here is required to use the -library: it is intended to satisfy the curiosity of scheduler geeks. +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. -Where the versions differ, the explanation relates to the `fast_io` version. +Where the versions differ, this explanation relates to the `fast_io` version. Differences are largely in `__init__.py`: the scheduling algorithm in `core.py` is little changed. -This doc assumes a good appreciation of the use of `uasyncio`. Familiarity with -Python generators is also recommended, in particular the use of `yield from` -and appreciating the difference between a generator and a generator function: +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 appreciatiion of the difference between a generator and a generator +function: ```python def gen_func(n): # gen_func is a generator function @@ -30,6 +32,8 @@ fast_io/__init__.py fast_io/core.py ``` +This has additional comments to aid in its understanding. + ###### [Main README](./README.md) # Generators and coroutines diff --git a/fast_io/__init__.py b/fast_io/__init__.py index 825cef6..1202ae3 100644 --- a/fast_io/__init__.py +++ b/fast_io/__init__.py @@ -32,14 +32,14 @@ def _unregister(self, sock, objmap, flag): if id(sock) in self.flags: flags = self.flags[id(sock)] if flags & flag: # flag is currently registered - flags &= ~flag - if flags: + 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)] + del self.flags[id(sock)] # Clear all flags self.poller.unregister(sock) - del objmap[id(sock)] + del objmap[id(sock)] # Remove coro from appropriate dict # Additively register sock for reading or writing def _register(self, sock, flag): @@ -94,8 +94,8 @@ def wait(self, delay): if isinstance(cb, tuple): cb[0](*cb[1]) else: - cb.pend_throw(None) - self._call_io(cb) + cb.pend_throw(None) # Clears the pend_throw(False) executed when IOWrite was yielded + self._call_io(cb) # Put coro onto runq (or ioq if one exists) if ev & select.POLLIN: cb = self.rdobjmap[id(sock)] self.rdobjmap[id(sock)] = None # TEST @@ -126,14 +126,16 @@ def __init__(self, polls, ios=None): def read(self, n=-1): while True: yield IORead(self.polls) - res = self.ios.read(n) + 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) - return res + 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 readexactly(self, n): buf = b"" @@ -196,7 +198,7 @@ def awrite(self, buf, off=0, sz=-1): if res == sz: if DEBUG and __debug__: log.debug("StreamWriter.awrite(): completed spooling %d bytes", res) - yield IOWriteDone(self.s) + yield IOWriteDone(self.s) # remove_writer de-registers device as a writer return if res is None: res = 0 diff --git a/fast_io/core.py b/fast_io/core.py index 28f6afb..41252a5 100644 --- a/fast_io/core.py +++ b/fast_io/core.py @@ -50,7 +50,7 @@ def create_task(self, coro): self.call_later_ms(0, coro) # CPython asyncio incompatibility: we don't return Task object - def _call_now(self, callback, *args): + 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) @@ -99,79 +99,80 @@ def run_forever(self): log.debug("Moving from waitq to runq: %s", cur_task[1]) self.call_soon(cur_task[1], *cur_task[2]) - # Process runq + # 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 + dl = 1 # Subtract this from entry count l while l or self.ioq_len: - if 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 + dl = 0 # No effect on l elif l == 0: - break + break # Both queues are empty else: cur_q = self.runq dl = 1 l -= dl - cb = cur_q.popleft() + cb = cur_q.popleft() # Remove most current task args = () - if not isinstance(cb, type_gen): + 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) - continue + 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 + self.cur_task = cb # Stored in a bound variable for TimeoutObj delay = 0 try: if args is (): - ret = next(cb) + 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): + 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 - elif isinstance(ret, IORead): - cb.pend_throw(False) - self.add_reader(arg, cb) - continue - elif isinstance(ret, IOWrite): + elif isinstance(ret, IORead): # coro was a StreamReader read method + cb.pend_throw(False) # Why? I think this is for debugging. If it 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): + 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 + 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) - continue - elif isinstance(ret, StopLoop): + self._call_io(cb, args) # Next call produces StopIteration. Arguably this is not required + continue # as awrite produces no return value. + 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): - self.call_soon(ret) - elif isinstance(ret, int): - # Delay + 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: + elif ret is None: # coro issued yield. delay == 0 so line 195 will put the current task back on runq # Just reschedule pass elif ret is False: - # Don't reschedule + # 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)) From 2ec7ef7b1d8da66dad19696f984582e0e4335f48 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 15:19:15 +0100 Subject: [PATCH 047/472] Improvements to tutorial. --- TUTORIAL.md | 122 +++++++++++++++++++++++++++------------------------- 1 file changed, 64 insertions(+), 58 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index fcdff64..fc1ab62 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,8 +1,70 @@ # Application of uasyncio to hardware interfaces +# 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 [Task cancellation](./TUTORIAL.md#36-task-cancellation) + 3.7 [Other synchronisation primitives](./TUTORIAL.md#37-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) + 4.4 [Coroutines with timeouts](./TUTORIAL.md#44-coroutines-with-timeouts) + 4.5 [Exceptions](./TUTORIAL.md#45-exceptions) + 5. [Interfacing hardware](./TUTORIAL.md#5-interfacing-hardware) + 5.1 [Timing issues](./TUTORIAL.md#51-timing-issues) + 5.2 [Polling hardware with a coroutine](./TUTORIAL.md#52-polling-hardware-with-a-coroutine) + 5.3 [Using the stream mechnanism](./TUTORIAL.md#53-using-the-stream-mechanism) + 5.3.1 [A UART driver example](./TUTORIAL.md#531-a-uart-driver-example) + 5.4 [Writing streaming device drivers](./TUTORIAL.md#54-writing-streaming-device-drivers) + 5.5 [A complete example: aremote.py](./TUTORIAL.md#55-a-complete-example-aremotepy) + A driver for an IR remote control receiver. + 5.6 [Driver for HTU21D](./TUTORIAL.md#56-htu21d-environment-sensor) A + temperature and humidity sensor. + 6. [Hints and tips](./TUTORIAL.md#6-hints-and-tips) + 6.1 [Program hangs](./TUTORIAL.md#61-program-hangs) + 6.2 [uasyncio retains state](./TUTORIAL.md#62-uasyncio-retains-state) + 6.3 [Garbage Collection](./TUTORIAL.md#63-garbage-collection) + 6.4 [Testing](./TUTORIAL.md#64-testing) + 6.5 [A common error](./TUTORIAL.md#65-a-common-error) This can be hard to find. + 6.6 [Socket programming](./TUTORIAL.md#66-socket-programming) + 7. [Notes for beginners](./TUTORIAL.md#7-notes-for-beginners) + 7.1 [Problem 1: event loops](./TUTORIAL.md#71-problem-1:-event-loops) + 7.2 [Problem 2: blocking methods](./TUTORIAL.md#7-problem-2:-blocking-methods) + 7.3 [The uasyncio approach](./TUTORIAL.md#73-the-uasyncio-approach) + 7.4 [Scheduling in uasyncio](./TUTORIAL.md#74-scheduling-in-uasyncio) + 7.5 [Why cooperative rather than pre-emptive?](./TUTORIAL.md#75-why-cooperative-rather-than-pre-emptive) + 7.6 [Communication](./TUTORIAL.md#76-communication) + 7.7 [Polling](./TUTORIAL.md#77-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 -[here](./TUTORIAL.md#7-notes-for-beginners). +[in section 7](./TUTORIAL.md#7-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 @@ -20,7 +82,7 @@ Except where detailed below, `asyncio` features of versions >3.4 are unsupported. As stated above it is a subset; this document identifies supported features. -# Installing uasyncio on bare metal +## 0.1 Installing uasyncio on bare metal MicroPython libraries are located on [PyPi](https://pypi.python.org/pypi). Libraries to be installed are: @@ -61,62 +123,6 @@ and rebuilding. ###### [Main README](./README.md) -# Contents - - 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 [Task cancellation](./TUTORIAL.md#36-task-cancellation) - 3.7 [Other synchronisation primitives](./TUTORIAL.md#37-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) - 4.4 [Coroutines with timeouts](./TUTORIAL.md#44-coroutines-with-timeouts) - 4.5 [Exceptions](./TUTORIAL.md#45-exceptions) - 5. [Interfacing hardware](./TUTORIAL.md#5-interfacing-hardware) - 5.1 [Timing issues](./TUTORIAL.md#51-timing-issues) - 5.2 [Polling hardware with a coroutine](./TUTORIAL.md#52-polling-hardware-with-a-coroutine) - 5.3 [Using the stream mechnanism](./TUTORIAL.md#53-using-the-stream-mechanism) - 5.3.1 [A UART driver example](./TUTORIAL.md#531-a-uart-driver-example) - 5.4 [Writing streaming device drivers](./TUTORIAL.md#54-writing-streaming-device-drivers) - 5.5 [A complete example: aremote.py](./TUTORIAL.md#55-a-complete-example-aremotepy) - A driver for an IR remote control receiver. - 5.6 [Driver for HTU21D](./TUTORIAL.md#56-htu21d-environment-sensor) A - temperature and humidity sensor. - 6. [Hints and tips](./TUTORIAL.md#6-hints-and-tips) - 6.1 [Program hangs](./TUTORIAL.md#61-program-hangs) - 6.2 [uasyncio retains state](./TUTORIAL.md#62-uasyncio-retains-state) - 6.3 [Garbage Collection](./TUTORIAL.md#63-garbage-collection) - 6.4 [Testing](./TUTORIAL.md#64-testing) - 6.5 [A common error](./TUTORIAL.md#65-a-common-error) This can be hard to find. - 6.6 [Socket programming](./TUTORIAL.md#66-socket-programming) - 7. [Notes for beginners](./TUTORIAL.md#7-notes-for-beginners) - 7.1 [Problem 1: event loops](./TUTORIAL.md#71-problem-1:-event-loops) - 7.2 [Problem 2: blocking methods](./TUTORIAL.md#7-problem-2:-blocking-methods) - 7.3 [The uasyncio approach](./TUTORIAL.md#73-the-uasyncio-approach) - 7.4 [Scheduling in uasyncio](./TUTORIAL.md#74-scheduling-in-uasyncio) - 7.5 [Why cooperative rather than pre-emptive?](./TUTORIAL.md#75-why-cooperative-rather-than-pre-emptive) - 7.6 [Communication](./TUTORIAL.md#76-communication) - 7.7 [Polling](./TUTORIAL.md#77-polling) - # 1. Cooperative scheduling The technique of cooperative multi-tasking is widely used in embedded systems. From 476f3c3652dc277b51505eadaba0237456052189 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 15:51:05 +0100 Subject: [PATCH 048/472] UNDER_THE_HOOD.md: add section on debug code. --- TUTORIAL.md | 6 ++++++ UNDER_THE_HOOD.md | 24 +++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index fc1ab62..4629027 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,5 +1,8 @@ # 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) @@ -82,6 +85,9 @@ 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 tutoial advocates a consistent programming style with good compatibility +with CPython V3.5 and above. + ## 0.1 Installing uasyncio on bare metal MicroPython libraries are located on [PyPi](https://pypi.python.org/pypi). diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index ad9b3b3..4c421ff 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -9,7 +9,8 @@ wishing to modify it. Where the versions differ, this explanation relates to the `fast_io` version. Differences are largely in `__init__.py`: the scheduling algorithm in `core.py` -is little changed. +is little changed. Note that the code in `fast_io` contains additional comments +to explain its operation. 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` @@ -257,6 +258,27 @@ I/O queue has been instantiated. Writing is handled similarly. +# Debug code + +The official `uasyncio` contains considerable explicit debug code: schedulers +are hard to debug. There is code which I believe is for debugging purposes and +some I have added myself for this purpose. The aim is to ensure that, if an +error causes a coro to be scheduled when it shouldn't be, an exception is +thrown. The alternative is weird, hard to diagnose, behaviour. +These are the instances. + +[pend_throw(false)](https://github.com/peterhinch/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/uasyncio.core/uasyncio/core.py#L119) +[also here](https://github.com/peterhinch/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/uasyncio.core/uasyncio/core.py#L123) +I think the intention here is to throw an exception (exception doesn't inherit +from Exception) if it is scheduled incorrectly. Correct scheduling coutermands +this: +[here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L97) +[and here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L114) +The `rdobjmap` and `wrobjmap` dictionary entries are invalidated +[here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L91) +[and here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L101) +This has the same aim: if an attempt is made incorrectly to reschedule them, an + # Modifying uasyncio The library is designed to be extensible. By following these guidelines a From d104b14eeb78e924221cf567b03580214654b0f2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 15:56:49 +0100 Subject: [PATCH 049/472] UNDER_THE_HOOD.md: add section on debug code. --- UNDER_THE_HOOD.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index 4c421ff..0a49c88 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -265,19 +265,20 @@ are hard to debug. There is code which I believe is for debugging purposes and some I have added myself for this purpose. The aim is to ensure that, if an error causes a coro to be scheduled when it shouldn't be, an exception is thrown. The alternative is weird, hard to diagnose, behaviour. -These are the instances. - -[pend_throw(false)](https://github.com/peterhinch/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/uasyncio.core/uasyncio/core.py#L119) -[also here](https://github.com/peterhinch/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/uasyncio.core/uasyncio/core.py#L123) +These are the instances: +[pend_throw(false)](https://github.com/peterhinch/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/uasyncio.core/uasyncio/core.py#L119) +also [here](https://github.com/peterhinch/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/uasyncio.core/uasyncio/core.py#L123). I think the intention here is to throw an exception (exception doesn't inherit from Exception) if it is scheduled incorrectly. Correct scheduling coutermands -this: -[here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L97) -[and here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L114) -The `rdobjmap` and `wrobjmap` dictionary entries are invalidated +this +[here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L97) +and [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L114): +these lines ensures that the exception will not be thrown. +The `rdobjmap` and `wrobjmap` dictionary entries are invalidated [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L91) -[and here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L101) +and [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L101). This has the same aim: if an attempt is made incorrectly to reschedule them, an +exception is thrown. # Modifying uasyncio From 8cd56e059e75cefe298135310e0d8abf514faa97 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 15:58:02 +0100 Subject: [PATCH 050/472] UNDER_THE_HOOD.md: add section on debug code. --- UNDER_THE_HOOD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index 0a49c88..6f023c7 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -275,7 +275,7 @@ this and [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L114): these lines ensures that the exception will not be thrown. The `rdobjmap` and `wrobjmap` dictionary entries are invalidated -[here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L91) +[here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L91) and [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L101). This has the same aim: if an attempt is made incorrectly to reschedule them, an exception is thrown. From 4a2286b4fe04d1c83985a6c315fc1682ce77567e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 16:20:54 +0100 Subject: [PATCH 051/472] UNDER_THE_HOOD.md: add TOC. --- UNDER_THE_HOOD.md | 72 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index 6f023c7..0b912a9 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -7,6 +7,24 @@ 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) + 3. [Coroutine yield types](./UNDER_THE_HOOD.md#3-coroutine-yield-types) + 3.1 [SysCall1 classes](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](./UNDER_THE_HOOD.md#42-task-cancellation) + 5. [Stream I/O](./UNDER_THE_HOOD.md#5-stream-i/o) + 5.1 [StreamReader](./UNDER_THE_HOOD.md#51-streamreader) + 5.2 [StreamWriter](./UNDER_THE_HOOD.md#52-streamwriter) + 5.3 [PollEventLoop.wait](./UNDER_THE_HOOD.md#53-polleventloop.wait) + 6. [Debug code](./UNDER_THE_HOOD.md#6-debug-code) + 7. [Modifying uasyncio](./UNDER_THE_HOOD.md#7-modifying-uasyncio) + +# 1. Introduction + Where the versions differ, this explanation relates to the `fast_io` version. Differences are largely in `__init__.py`: the scheduling algorithm in `core.py` is little changed. Note that the code in `fast_io` contains additional comments @@ -37,7 +55,7 @@ This has additional comments to aid in its understanding. ###### [Main README](./README.md) -# Generators and coroutines +# 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 @@ -62,7 +80,9 @@ 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 (see below). -# Coroutine yield types +###### [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 @@ -95,7 +115,7 @@ the type of that object. The following object types are handled: `yield` is rescheduled. * A `SysCall1` instance. See below. -## SysCall1 classes +## 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 @@ -117,7 +137,9 @@ The following subclasses exist: The `IO*` classes are for the exclusive use of `StreamReader` and `StreamWriter` objects. -# The EventLoop +###### [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 @@ -168,7 +190,9 @@ 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. -## Exceptions +###### [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 @@ -178,7 +202,7 @@ 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. -## Task Cancellation +## 4.2 Task Cancellation 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 @@ -186,7 +210,9 @@ 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. -# Stream I/O +###### [Contents](./UNDER_THE_HOOD.md#0-contents) + +# 5. Stream I/O 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 @@ -194,7 +220,7 @@ 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`). -## StreamReader +## 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 @@ -230,7 +256,7 @@ 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. -## StreamWriter +## 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 @@ -240,7 +266,7 @@ 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. -## PollEventLoop.wait() +## 5.3 PollEventLoop.wait 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 @@ -258,14 +284,19 @@ I/O queue has been instantiated. Writing is handled similarly. -# Debug code +###### [Contents](./UNDER_THE_HOOD.md#0-contents) + +# 6. Debug code The official `uasyncio` contains considerable explicit debug code: schedulers -are hard to debug. There is code which I believe is for debugging purposes and -some I have added myself for this purpose. The aim is to ensure that, if an -error causes a coro to be scheduled when it shouldn't be, an exception is -thrown. The alternative is weird, hard to diagnose, behaviour. -These are the instances: +are hard to debug. + +There is also code which I believe is for debugging purposes including some I +have added myself for this purpose. The aim is to ensure that, if an error +causes a coro to be scheduled when it shouldn't be, an exception is thrown. The +alternative is weird, hard to diagnose, behaviour. + +Consider these instances: [pend_throw(false)](https://github.com/peterhinch/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/uasyncio.core/uasyncio/core.py#L119) also [here](https://github.com/peterhinch/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/uasyncio.core/uasyncio/core.py#L123). I think the intention here is to throw an exception (exception doesn't inherit @@ -273,14 +304,17 @@ from Exception) if it is scheduled incorrectly. Correct scheduling coutermands this [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L97) and [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L114): -these lines ensures that the exception will not be thrown. +these lines ensures that the exception will not be thrown. + The `rdobjmap` and `wrobjmap` dictionary entries are invalidated [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L91) and [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L101). This has the same aim: if an attempt is made incorrectly to reschedule them, an exception is thrown. -# Modifying uasyncio +###### [Contents](./UNDER_THE_HOOD.md#0-contents) + +# 7. 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 @@ -312,4 +346,6 @@ def get_event_loop(args): return _event_loop ``` +###### [Contents](./UNDER_THE_HOOD.md#0-contents) + ###### [Main README](./README.md) From 3cbda5d2db622a5a4de53cbb7bc1880a945557bb Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 16:24:11 +0100 Subject: [PATCH 052/472] UNDER_THE_HOOD.md: add TOC. --- UNDER_THE_HOOD.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index 0b912a9..3c5f147 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -12,11 +12,11 @@ wishing to modify it. 1. [Introduction](./UNDER_THE_HOOD.md#1-introduction) 2. [Generators and coroutines](./UNDER_THE_HOOD.md#2-generators-and-coroutines) 3. [Coroutine yield types](./UNDER_THE_HOOD.md#3-coroutine-yield-types) - 3.1 [SysCall1 classes](SysCall1 classes) + 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](./UNDER_THE_HOOD.md#42-task-cancellation) - 5. [Stream I/O](./UNDER_THE_HOOD.md#5-stream-i/o) + 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](./UNDER_THE_HOOD.md#53-polleventloop.wait) @@ -212,7 +212,7 @@ time the coro is scheduled. ###### [Contents](./UNDER_THE_HOOD.md#0-contents) -# 5. Stream I/O +# 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 From 3feec88af712b87c1c2270554de73696d65eeb45 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 16:27:00 +0100 Subject: [PATCH 053/472] UNDER_THE_HOOD.md: add TOC. --- UNDER_THE_HOOD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index 3c5f147..dc4ddc5 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -19,7 +19,7 @@ wishing to modify it. 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](./UNDER_THE_HOOD.md#53-polleventloop.wait) + 5.3 [PollEventLoop wait method](./UNDER_THE_HOOD.md#53-polleventloop-wait-method) 6. [Debug code](./UNDER_THE_HOOD.md#6-debug-code) 7. [Modifying uasyncio](./UNDER_THE_HOOD.md#7-modifying-uasyncio) @@ -266,7 +266,7 @@ 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 +## 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 From 444cec4369c2b5928106861a3bcfe8861f3aa827 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 16:45:04 +0100 Subject: [PATCH 054/472] UNDER_THE_HOOD.md: add links section. --- UNDER_THE_HOOD.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index dc4ddc5..e83024d 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -22,6 +22,7 @@ wishing to modify it. 5.3 [PollEventLoop wait method](./UNDER_THE_HOOD.md#53-polleventloop-wait-method) 6. [Debug code](./UNDER_THE_HOOD.md#6-debug-code) 7. [Modifying uasyncio](./UNDER_THE_HOOD.md#7-modifying-uasyncio) + 8. [Links](./UNDER_THE_HOOD.md#8-links) # 1. Introduction @@ -348,4 +349,19 @@ def get_event_loop(args): ###### [Contents](./UNDER_THE_HOOD.md#0-contents) +# 8. Links + +[Initial discussion of priority I/O scheduling](https://github.com/micropython/micropython/issues/2664) + +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: +[PR #287: fast I/O](https://github.com/micropython/micropython-lib/pull/287) +[PR #292: error reporting](https://github.com/micropython/micropython-lib/pull/292) + +This caught my attention as worthwhile: +[PR #270](https://github.com/micropython/micropython-lib/pull/270). + ###### [Main README](./README.md) From 2b96274fd6bed916c700d72224dbce1f0aa00b1a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 16:47:37 +0100 Subject: [PATCH 055/472] UNDER_THE_HOOD.md: add links section. --- UNDER_THE_HOOD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index e83024d..65c60a1 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -351,14 +351,14 @@ def get_event_loop(args): # 8. Links -[Initial discussion of priority I/O scheduling](https://github.com/micropython/micropython/issues/2664) +Initial discussion of priority I/O scheduling [here](https://github.com/micropython/micropython/issues/2664) 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: -[PR #287: fast I/O](https://github.com/micropython/micropython-lib/pull/287) +[PR #287: fast I/O](https://github.com/micropython/micropython-lib/pull/287) and [PR #292: error reporting](https://github.com/micropython/micropython-lib/pull/292) This caught my attention as worthwhile: From dfcb68e8c2da0a971d100f8099a6ff271fc0578e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 16:51:23 +0100 Subject: [PATCH 056/472] UNDER_THE_HOOD.md: add links section. --- UNDER_THE_HOOD.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index 65c60a1..ddb0760 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -351,17 +351,18 @@ def get_event_loop(args): # 8. Links -Initial discussion of priority I/O scheduling [here](https://github.com/micropython/micropython/issues/2664) +Initial discussion of priority I/O scheduling [here](https://github.com/micropython/micropython/issues/2664). -PR enabling stream device drivers to be written in Python +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: -[PR #287: fast I/O](https://github.com/micropython/micropython-lib/pull/287) and -[PR #292: error reporting](https://github.com/micropython/micropython-lib/pull/292) +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 as worthwhile: +This caught my attention for usefulness and compliance with CPython: [PR #270](https://github.com/micropython/micropython-lib/pull/270). ###### [Main README](./README.md) From a57f7f43aa2e8d1c016308d6463ac37e71efa3c8 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 17:00:22 +0100 Subject: [PATCH 057/472] UNDER_THE_HOOD.md: minor additions. --- UNDER_THE_HOOD.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index ddb0760..5f15437 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -29,7 +29,9 @@ wishing to modify it. Where the versions differ, this explanation relates to the `fast_io` version. Differences are largely in `__init__.py`: the scheduling algorithm in `core.py` is little changed. Note that the code in `fast_io` contains additional comments -to explain its operation. +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` @@ -305,7 +307,8 @@ from Exception) if it is scheduled incorrectly. Correct scheduling coutermands this [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L97) and [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L114): -these lines ensures that the exception will not be thrown. +these lines ensures that the exception will not be thrown. If my interpretation +of this is wrong I'd be very glad to be enlightened. The `rdobjmap` and `wrobjmap` dictionary entries are invalidated [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L91) From 366f16f5314b15f104618822755ac2f2052e5e0a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Jul 2018 17:03:47 +0100 Subject: [PATCH 058/472] UNDER_THE_HOOD.md: ran the spoiling chuckler. --- UNDER_THE_HOOD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index 5f15437..0c9c858 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -35,7 +35,7 @@ to explain its operation. The code the `fast_io` directory is also in 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 appreciatiion of the difference between a generator and a generator +and an appreciation of the difference between a generator and a generator function: ```python @@ -303,7 +303,7 @@ Consider these instances: [pend_throw(false)](https://github.com/peterhinch/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/uasyncio.core/uasyncio/core.py#L119) also [here](https://github.com/peterhinch/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/uasyncio.core/uasyncio/core.py#L123). I think the intention here is to throw an exception (exception doesn't inherit -from Exception) if it is scheduled incorrectly. Correct scheduling coutermands +from Exception) if it is scheduled incorrectly. Correct scheduling countermands this [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L97) and [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L114): From 23a60e2a5a399d7d9e0d95f6e63214877d7ad06f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 15 Jul 2018 18:58:56 +0100 Subject: [PATCH 059/472] Low power version added. --- FASTPOLL.md | 8 ++ README.md | 16 +++- fast_io/__init__.py | 3 +- fast_io/core.py | 17 ++-- lowpower/README.md | 190 +++++++++++++++++++++++++++++++++++++++++++ lowpower/lowpower.py | 56 +++++++++++++ lowpower/rtc_time.py | 74 +++++++++++++++++ 7 files changed, 356 insertions(+), 8 deletions(-) create mode 100644 lowpower/README.md create mode 100644 lowpower/lowpower.py create mode 100644 lowpower/rtc_time.py diff --git a/FASTPOLL.md b/FASTPOLL.md index e509ff9..4f467f6 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -225,6 +225,14 @@ 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. +The version also supports an `implementation` namedtuple with the following +fields: + * `name` 'fast_io' + * `variant` `standard` + * `major` 0 Major version no. + * `minor` 100 Minor version no. i.e. version = 0.100 + +The `variant` field can also contain `lowpower` if running that version. ###### [Contents](./FASTPOLL.md#contents) diff --git a/README.md b/README.md index ea3c840..78aacdb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # 1. The MicroPython uasyncio library -This GitHub repository consists of the following parts: +This GitHub repository consists of the following parts. Firstly a modified +`fast_io` version of `uasyncio` offering some benefits over the official +version. + +Secondly the following resources relevant to users of official or `fast_io` +versions: * [A tutorial](./TUTORIAL.md) An introductory tutorial on asynchronous programming and the use of the uasyncio library (asyncio subset). * [Asynchronous device drivers](./DRIVERS.md). A module providing drivers for @@ -24,7 +29,7 @@ This GitHub repository consists of the following parts: * [Under the hood](./UNDER_THE_HOOD.md) A guide to help understand the `uasyncio` code. For scheduler geeks and those wishing to modify `uasyncio`. -## 1.1 A new "priority" version. +## 1.1 The "fast_io" version. This repo included `asyncio_priority.py` which is now deprecated. Its primary purpose was to provide a means of servicing fast hardware devices by means of @@ -42,6 +47,13 @@ provides an option for I/O scheduling with much reduced latency. It also fixes the bug. It is hoped that these changes will be accepted into mainstream in due course. +### 1.1.1 A Pyboard-only low power version + +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 the library on battery powered projects. + # 2. Version and installation of uasyncio The documentation and code in this repository are based on `uasyncio` version diff --git a/fast_io/__init__.py b/fast_io/__init__.py index 1202ae3..c4e7dc9 100644 --- a/fast_io/__init__.py +++ b/fast_io/__init__.py @@ -1,9 +1,10 @@ +# uasyncio.__init__ fast_io +# fork: peterhinch/micropython-lib branch: uasyncio-io-fast-and-rw import uerrno import uselect as select import usocket as _socket from uasyncio.core import * - DEBUG = 0 log = None diff --git a/fast_io/core.py b/fast_io/core.py index 41252a5..6a6f143 100644 --- a/fast_io/core.py +++ b/fast_io/core.py @@ -1,4 +1,10 @@ -import utime as time +# uasyncio.core fast_io +# fork: peterhinch/micropython-lib branch: uasyncio-io-fast-and-rw +version = 'fast_io' +try: + import rtc_time as time # Low power timebase using RTC +except ImportError: + import utime as time import utimeq import ucollections @@ -46,7 +52,8 @@ def time(self): def create_task(self, coro): # CPython 3.4.2 - assert not isinstance(coro, type_genf), 'Generator function is not iterable.' # upy issue #3241 + 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 @@ -158,8 +165,8 @@ def run_forever(self): continue elif isinstance(ret, IOWriteDone): self.remove_writer(arg) - self._call_io(cb, args) # Next call produces StopIteration. Arguably this is not required - continue # as awrite produces no return value. + 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: @@ -205,7 +212,7 @@ def run_forever(self): self.wait(delay) def run_until_complete(self, coro): - assert not isinstance(coro, type_genf), 'Generator function is not iterable.' # upy issue #3241 + assert not isinstance(coro, type_genf), 'Coroutine arg expected.' # upy issue #3241 def _run_and_stop(): yield from coro yield StopLoop(0) diff --git a/lowpower/README.md b/lowpower/README.md new file mode 100644 index 0000000..33f4794 --- /dev/null +++ b/lowpower/README.md @@ -0,0 +1,190 @@ +# An experimental low power usayncio adaptation + +# 1. Introduction + +This adaptation is specific to the Pyboard and compatible platforms, namely +those capable of running the `pyb` module. This module 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 as 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, reducing power consumption. 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. + +Some general notes on low power Pyboard applications may be found +[here](https://github.com/peterhinch/micropython-micropower). + +# 2. Installation + +Ensure that the version of `uasyncio` in this repository is installed and +tested. Copy the file `rtc_time.py` to the device so that it is on `sys.path`. + +The test program `lowpower.py` requires a link between pins X1 and X2 to enable +UART 4 to operate via a loopback. + +# 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 coro +is ready for execution, it determines the time when the most current coro 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()`. This would cause all `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`. + +To avoid the power drain caused by `select.poll` the user code must issue the +following: + +```python + loop = asyncio.get_event_loop() + loop.create_task(rtc_time.lo_power(t)) +``` + +This coro has a continuously running loop that executes `pyb.stop` before +yielding with a zero delay: + +```python + def lo_power(t_ms): + rtc.wakeup(t_ms) + while True: + pyb.stop() + yield +``` + +The design of the scheduler is such that, if at least one coro is pending with +a zero delay, polling will occur with a zero delay. This minimises power draw. +The significance of the `t` argument is detailed below. + +### 3.2.1 Consequences of pyb.stop + +#### 3.2.1.1 Timing Accuracy + +A minor limitation is that the Pyboard RTC cannot resolve times of less than +4ms so there is a theoretical reduction in the accuracy of delays. In practice, +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 coroutines. The latency implicit in the `lo_power` coro +(see section 5.2) makes this issue largely academic. + +#### 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. + +Applications can detect which timebase is in use by issuing: + +```python +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 +``` + +# 4. rtc_time.py + +This provides the following. + +Variable: + * `use_utime` `True` if the `uasyncio` timebase is `utime`, `False` if it is + the RTC. + +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 for these. + * `ticks_ms` + * `ticks_add` + * `ticks_diff` + * `sleep_ms` This should not be used if the RTC timebase is in use as its + usage of the RTC will conflict with the `lo_power` coro. + +Coroutine: + * `lo_power` Argument: `t_ms`. This coro repeatedly issues `pyb.stop`, waking + after `t_ms` ms. The higher `t_ms` is, the greater the latency experienced by + other coros and by I/O. Smaller values may result in higher power consumption + with other coros being scheduled more frequently. + +# 5. Application design + +Attention to detail is required to minimise power consumption, both in terms of +hardware and code. + +## 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. + +## 5.2 Application Code + +Issuing `pyb.stop` directly in code is unwise; also, when using the RTC +timebase, calling `rtc_time.sleep_ms`. This is because there is only one RTC, +and hence there is potential conflict with different routines issuing +`rtc.wakeup`. The coro `rtc_time.lo_power` should be the only one issuing this +call. + +The implications of the `t_ms` argument to `rtc_time.lo_power` should be +considered. During periods when the Pyboard is in a `stop` state, other coros +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 with this latency +period. + +Long values of `t_ms` will 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 coro will acquire. If `t_ms == 200` the coro + +```python +async def foo(): + while True: + # Do some processing + await asyncio.sleep(0) +``` + +will execute (at best) at a rate of 5Hz. And possibly considerably less +frequently depending on the behaviour of competing coros. Likewise + +```python +async def bar(): + while True: + # Do some processing + await asyncio.sleep_ms(10) +``` + +the 10ms sleep may be 200ms or longer, again dependent on other application +code. diff --git a/lowpower/lowpower.py b/lowpower/lowpower.py new file mode 100644 index 0000000..9dfa18b --- /dev/null +++ b/lowpower/lowpower.py @@ -0,0 +1,56 @@ +# lowpower.py Demo of using uasyncio to reduce Pyboard power consumption +# Author: Peter Hinch +# Copyright Peter Hinch 2018 Released under the MIT license + +# The file rtc_time.py must be on the path for this to work at low power. + +import pyb +import uasyncio as asyncio +import rtc_time + +# Stop the test after a period +async def killer(duration): + await asyncio.sleep(duration) + +# Briefly pulse an LED to save power +async def pulse(led): + led.on() + await asyncio.sleep_ms(200) + led.off() + +# Flash an LED forever +async def flash(led, ms): + while True: + await pulse(led) + await asyncio.sleep_ms(ms) + +# Periodically send text through UART +async def sender(uart): + swriter = asyncio.StreamWriter(uart, {}) + while True: + await swriter.awrite('Hello uart\n') + await asyncio.sleep(1.3) + +# Each time a message is received pulse the LED +async def receiver(uart, led): + sreader = asyncio.StreamReader(uart) + while True: + await sreader.readline() + await pulse(led) + +def test(duration): + # For lowest power consumption set unused pins as inputs with pullups. + # Note the 4K7 I2C pullups on X9 X10 Y9 Y10. + 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) + if rtc_time.use_utime: # Not running in low power mode + pyb.LED(4).on() + uart = pyb.UART(4, 115200) + loop = asyncio.get_event_loop() + loop.create_task(rtc_time.lo_power(100)) + loop.create_task(flash(pyb.LED(1), 4000)) + loop.create_task(sender(uart)) + loop.create_task(receiver(uart, pyb.LED(2))) + loop.run_until_complete(killer(duration)) + +test(60) diff --git a/lowpower/rtc_time.py b/lowpower/rtc_time.py new file mode 100644 index 0000000..26adea0 --- /dev/null +++ b/lowpower/rtc_time.py @@ -0,0 +1,74 @@ +# rtc_time.py Pyboard-only RTC based timing for low power uasyncio +# Author: Peter Hinch +# Copyright Peter Hinch 2018 Released under the MIT license + +# Code based on extmod/utime_mphal.c +# millisecs roll over on 7 days rather than 12.42757 days + +# If not running on a Pyboard the normal utime timebase is used. This is also +# used on a USB connected Pyboard to keep the USB connection open. +# On an externally powered Pyboard an RTC timebase is substituted. + +import sys + +_PERIOD = const(604800000) # ms in 7 days +_PERIOD_2 = const(302400000) # half period +_SS_TO_MS = 1000/256 # Subsecs to ms + +use_utime = True # Assume the normal utime timebase +if sys.platform == 'pyboard': + import pyb + mode = pyb.usb_mode() + if mode is None: # USB is disabled + use_utime = False # use RTC timebase + elif 'VCP' in mode: # User has enabled VCP in boot.py + if pyb.Pin.board.USB_VBUS.value() == 1: # USB physically connected + print('USB connection: rtc_time disabled.') + else: + pyb.usb_mode(None) # Save power + use_utime = False # use RTC timebase +else: + print('rtc_time.py is Pyboard-specific.') + +if use_utime: # Run utime: Pyboard connected to PC via USB or alien platform + import utime + ticks_ms = utime.ticks_ms + ticks_add = utime.ticks_add + ticks_diff = utime.ticks_diff + sleep_ms = utime.sleep_ms + lo_power = lambda _ : (yield) +else: + rtc = pyb.RTC() + # dt: (year, month, day, weekday, hours, minutes, seconds, subseconds) + # weekday is 1-7 for Monday through Sunday. + # subseconds counts down from 255 to 0 + def ticks_ms(): + dt = rtc.datetime() + return ((dt[3] - 1)*86400000 + dt[4]*3600000 + dt[5]*60000 + dt[6]*1000 + + int(_SS_TO_MS * (255 - dt[7]))) + + def ticks_add(a, b): + return (a + b) % _PERIOD + + def ticks_diff(end, start): + return ((end - start + _PERIOD_2) % _PERIOD) - _PERIOD_2 + + # This function is unused by uasyncio as its only call is in core.wait which + # is overridden in __init__.py. This means that the lo_power coro can rely + # on rtc.wakeup() + def sleep_ms(t): + end = ticks_add(ticks_ms(), t) + while t > 0: + if t < 9: # <= 2 RTC increments + pyb.delay(t) # Just wait and quit + break + rtc.wakeup(t) + pyb.stop() # Note some interrupt might end this prematurely + rtc.wakeup(None) + t = ticks_diff(end, ticks_ms()) + + def lo_power(t_ms): + rtc.wakeup(t_ms) + while True: + pyb.stop() + yield From 829c4df3240bdb0f1f34516e71a1b6153180b712 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 25 Jul 2018 13:29:05 +0100 Subject: [PATCH 060/472] Low power version release 0.1 --- fast_io/core.py | 4 + lowpower/README.md | 256 ++++++++++++++++++++++++++++++++----------- lowpower/lowpower.py | 50 ++++----- lowpower/rtc_time.py | 82 ++++++++++---- 4 files changed, 279 insertions(+), 113 deletions(-) diff --git a/fast_io/core.py b/fast_io/core.py index 6a6f143..49892df 100644 --- a/fast_io/core.py +++ b/fast_io/core.py @@ -264,6 +264,10 @@ def get_event_loop(runq_len=16, waitq_len=16, ioq_len=0): _event_loop = _event_loop_class(runq_len, waitq_len, ioq_len) return _event_loop +# Allow user classes to determine prior event loop instantiation. +def got_event_loop(): + return _event_loop is not None + def sleep(secs): yield int(secs * 1000) diff --git a/lowpower/README.md b/lowpower/README.md index 33f4794..f74ce4e 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -1,26 +1,34 @@ -# An experimental low power usayncio adaptation +# A low power usayncio adaptation + +Release 0.1 25th July 2018 # 1. Introduction This adaptation is specific to the Pyboard and compatible platforms, namely -those capable of running the `pyb` module. This module supports two low power -modes `standby` and `stop` +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 as after a hard +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, reducing power consumption. The two approaches can be combined, -with a device waking from `shutdown` to run a low power `uasyncio` application -before again entering `shutdown`. +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. +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). @@ -30,70 +38,83 @@ Some general notes on low power Pyboard applications may be found Ensure that the version of `uasyncio` in this repository is installed and tested. Copy the file `rtc_time.py` to the device so that it is on `sys.path`. +## 2.1 Files + + * `rtc_time.py` Low power library. + * `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. + * `lowpower.py` Send and receive messages on UART4, echoing received messages + to UART2 at a different baudrate. This consumes about 1.4mA and serves to + demonstrate that interrupt-driven devices operate correctly. + The test program `lowpower.py` requires a link between pins X1 and X2 to enable -UART 4 to operate via a loopback. +UART 4 to receive data via a loopback. # 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 coro -is ready for execution, it determines the time when the most current coro will +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()`. This would cause all `uasyncio` timing to become highly -inaccurate. +`pyb.stop()`. An application-level scheme using `pyb.stop` to conserve power +would cause all `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`. +run through `stop`. Libraries using `uasyncio` will run unmodified, barring any +timing issues if user code increases scheduler latency. To avoid the power drain caused by `select.poll` the user code must issue the following: ```python - loop = asyncio.get_event_loop() - loop.create_task(rtc_time.lo_power(t)) -``` - -This coro has a continuously running loop that executes `pyb.stop` before -yielding with a zero delay: - -```python - def lo_power(t_ms): - rtc.wakeup(t_ms) - while True: - pyb.stop() - yield +try: + if asyncio.version != 'fast_io': + raise AttributeError +except AttributeError: + raise OSError('This requires fast_io fork of uasyncio.') +import rtc_time + # Instantiate event loop with any args before running code that uses it +loop = asyncio.get_event_loop() +rtc_time.Latency(100) # Define latency in ms ``` -The design of the scheduler is such that, if at least one coro is pending with -a zero delay, polling will occur with a zero delay. This minimises power draw. -The significance of the `t` argument is detailed below. +The `Latency` class has a continuously running loop that executes `pyb.stop` +before yielding with a zero delay. The duration of the `stop` condition +(`latency`) can be dynamically varied. If zeroed the scheduler will run at +full speed. The `yield` allows each pending task to run once before the +scheduler is again paused (if `latency` > 0). ### 3.2.1 Consequences of pyb.stop -#### 3.2.1.1 Timing Accuracy +#### 3.2.1.1 Timing Accuracy and rollover A minor limitation is that the Pyboard RTC cannot resolve times of less than 4ms so there is a theoretical reduction in the accuracy of delays. In practice, as explained in the [tutorial](../TUTORIAL.md), issuing ```python - await asyncio.sleep_ms(t) +await asyncio.sleep_ms(t) ``` specifies a minimum delay: the maximum may be substantially higher depending on -the behaviour of other coroutines. The latency implicit in the `lo_power` coro -(see section 5.2) makes this issue largely academic. +the behaviour of other tasks. + +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 @@ -106,6 +127,11 @@ from a separate power source for power measurements. Applications can detect which timebase is in use by issuing: ```python +try: + if asyncio.version != '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 @@ -113,34 +139,97 @@ else: # Running on RTC timebase with no USB connection ``` -# 4. rtc_time.py +Debugging at low power is facilitated by using `pyb.repl_uart` with an FTDI +adaptor. + +### 3.2.2 Measured results + +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` where `ib` is the stopped +current (in my case 380μA) and `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. + +# 4. The rtc_time module This provides the following. Variable: * `use_utime` `True` if the `uasyncio` timebase is `utime`, `False` if it is - the RTC. + the RTC. Treat as read-only. 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 for these. +timebase. See the `utime` official +[documentation](http://docs.micropython.org/en/latest/pyboard/library/utime.html) +for these. * `ticks_ms` * `ticks_add` * `ticks_diff` - * `sleep_ms` This should not be used if the RTC timebase is in use as its - usage of the RTC will conflict with the `lo_power` coro. -Coroutine: - * `lo_power` Argument: `t_ms`. This coro repeatedly issues `pyb.stop`, waking - after `t_ms` ms. The higher `t_ms` is, the greater the latency experienced by - other coros and by I/O. Smaller values may result in higher power consumption - with other coros being scheduled more frequently. +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 arg `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. 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 +rtc_time.Latency().value(t) +``` # 5. Application design Attention to detail is required to minimise power consumption, both in terms of -hardware and code. +hardware and code. The only *required* change to application code is to add + +```python +try: + if asyncio.version != '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: +import rtc_time + # Instantiate event loop with any args before running code that uses it: +loop = asyncio.get_event_loop() +lp = rtc_time.Latency(100) # Define latency in ms + # Run application code +``` + +However optimising the power draw/performance tradeoff benefits from further +optimisations. ## 5.1 Hardware @@ -150,24 +239,38 @@ 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 then reconfigure any pins which are to be used. +If I2C is to be used there are further implications: see the above reference. + ## 5.2 Application Code -Issuing `pyb.stop` directly in code is unwise; also, when using the RTC -timebase, calling `rtc_time.sleep_ms`. This is because there is only one RTC, -and hence there is potential conflict with different routines issuing -`rtc.wakeup`. The coro `rtc_time.lo_power` should be the only one issuing this -call. +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 `t_ms` argument to `rtc_time.lo_power` should be -considered. During periods when the Pyboard is in a `stop` state, other coros +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 with this latency -period. +needs to be determined in conjunction with data rates and the latency period. -Long values of `t_ms` will 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 coro will acquire. If `t_ms == 200` the coro +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(): @@ -176,8 +279,8 @@ async def foo(): await asyncio.sleep(0) ``` -will execute (at best) at a rate of 5Hz. And possibly considerably less -frequently depending on the behaviour of competing coros. Likewise +will execute (at best) at a rate of 5Hz; possibly considerably less frequently +depending on the behaviour of competing tasks. Likewise ```python async def bar(): @@ -186,5 +289,36 @@ async def bar(): await asyncio.sleep_ms(10) ``` -the 10ms sleep may be 200ms or longer, again dependent on other application -code. +the 10ms sleep will be >=200ms dependent on other application tasks. + +Latency may be changed dynamically by using the `value` method of the `Latency` +instance. A typical application (as in `lpdemo.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. + +# 6. Note on the design + +The `rtc_time` module represents a compromise designed to minimise changes to +`uasyncio`. The aim is to have zero effect on the performance of normal +applications or code 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. + +The `rtc_time` module ensures that `uasyncio` uses `utime` for timing if the +module is present in the path but is unused. This can occur because of an +active USB connection or if running on an an incompatible platform. This +ensures that under such conditions performance is unaffected. diff --git a/lowpower/lowpower.py b/lowpower/lowpower.py index 9dfa18b..2fa02f9 100644 --- a/lowpower/lowpower.py +++ b/lowpower/lowpower.py @@ -2,28 +2,24 @@ # Author: Peter Hinch # Copyright Peter Hinch 2018 Released under the MIT license -# The file rtc_time.py must be on the path for this to work at low power. +# The file rtc_time.py must be on the path. +# Requires a link between X1 and X2. +# Periodically sends a line on UART4 at 115200 baud. +# This is received on UART4 and re-sent on UART2 (pin X3) at 9600 baud. import pyb import uasyncio as asyncio +try: + if asyncio.version != 'fast_io': + raise AttributeError +except AttributeError: + raise OSError('This requires fast_io fork of uasyncio.') import rtc_time # Stop the test after a period async def killer(duration): await asyncio.sleep(duration) -# Briefly pulse an LED to save power -async def pulse(led): - led.on() - await asyncio.sleep_ms(200) - led.off() - -# Flash an LED forever -async def flash(led, ms): - while True: - await pulse(led) - await asyncio.sleep_ms(ms) - # Periodically send text through UART async def sender(uart): swriter = asyncio.StreamWriter(uart, {}) @@ -31,26 +27,24 @@ async def sender(uart): await swriter.awrite('Hello uart\n') await asyncio.sleep(1.3) -# Each time a message is received pulse the LED -async def receiver(uart, led): - sreader = asyncio.StreamReader(uart) +# Each time a message is received echo it on uart 4 +async def receiver(uart_in, uart_out): + sreader = asyncio.StreamReader(uart_in) + swriter = asyncio.StreamWriter(uart_out, {}) while True: - await sreader.readline() - await pulse(led) + res = await sreader.readline() + await swriter.awrite(res) def test(duration): - # For lowest power consumption set unused pins as inputs with pullups. - # Note the 4K7 I2C pullups on X9 X10 Y9 Y10. - 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) if rtc_time.use_utime: # Not running in low power mode - pyb.LED(4).on() - uart = pyb.UART(4, 115200) + pyb.LED(3).on() + uart2 = pyb.UART(2, 9600) + uart4 = pyb.UART(4, 115200) + # Instantiate event loop before using it in Latency class loop = asyncio.get_event_loop() - loop.create_task(rtc_time.lo_power(100)) - loop.create_task(flash(pyb.LED(1), 4000)) - loop.create_task(sender(uart)) - loop.create_task(receiver(uart, pyb.LED(2))) + lp = rtc_time.Latency(50) # ms + loop.create_task(sender(uart4)) + loop.create_task(receiver(uart4, uart2)) loop.run_until_complete(killer(duration)) test(60) diff --git a/lowpower/rtc_time.py b/lowpower/rtc_time.py index 26adea0..1da484b 100644 --- a/lowpower/rtc_time.py +++ b/lowpower/rtc_time.py @@ -10,6 +10,7 @@ # On an externally powered Pyboard an RTC timebase is substituted. import sys +import utime _PERIOD = const(604800000) # ms in 7 days _PERIOD_2 = const(302400000) # half period @@ -28,15 +29,68 @@ pyb.usb_mode(None) # Save power use_utime = False # use RTC timebase else: - print('rtc_time.py is Pyboard-specific.') + raise OSError('rtc_time.py is Pyboard-specific.') +# For lowest power consumption set unused pins as inputs with pullups. +# Note the 4K7 I2C pullups on X9 X10 Y9 Y10. +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) +# User code redefines any pins in use + +import uasyncio as asyncio + +# Common version has a needless dict: https://www.python.org/dev/peps/pep-0318/#examples +# https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python +# Resolved: https://forum.micropython.org/viewtopic.php?f=2&t=5033&p=28824#p28824 +def singleton(cls): + instance = None + def getinstance(*args, **kwargs): + nonlocal instance + if instance is None: + instance = cls(*args, **kwargs) + return instance + return getinstance + +@singleton +class Latency(): + def __init__(self, t_ms=100): + if use_utime: # Not in low power mode: t_ms stays zero + self._t_ms = 0 + else: + if asyncio.got_event_loop(): + self._t_ms = t_ms + loop = asyncio.get_event_loop() + loop.create_task(self._run()) + else: + raise OSError('Event loop not instantiated.') + + def _run(self): + rtc = pyb.RTC() + rtc.wakeup(self._t_ms) + t_ms = self._t_ms + while True: + if t_ms > 0: + pyb.stop() + yield + if t_ms != self._t_ms: + t_ms = self._t_ms + if t_ms > 0: + rtc.wakeup(t_ms) + else: + rtc.wakeup(None) + + def value(self, val=None): + if val is not None and not use_utime: + self._t_ms = max(val, 0) + return self._t_ms + +# sleep_ms is defined to stop things breaking if someone imports uasyncio.core +# Power won't be saved if this is done. +sleep_ms = utime.sleep_ms if use_utime: # Run utime: Pyboard connected to PC via USB or alien platform - import utime ticks_ms = utime.ticks_ms ticks_add = utime.ticks_add ticks_diff = utime.ticks_diff - sleep_ms = utime.sleep_ms - lo_power = lambda _ : (yield) else: rtc = pyb.RTC() # dt: (year, month, day, weekday, hours, minutes, seconds, subseconds) @@ -52,23 +106,3 @@ def ticks_add(a, b): def ticks_diff(end, start): return ((end - start + _PERIOD_2) % _PERIOD) - _PERIOD_2 - - # This function is unused by uasyncio as its only call is in core.wait which - # is overridden in __init__.py. This means that the lo_power coro can rely - # on rtc.wakeup() - def sleep_ms(t): - end = ticks_add(ticks_ms(), t) - while t > 0: - if t < 9: # <= 2 RTC increments - pyb.delay(t) # Just wait and quit - break - rtc.wakeup(t) - pyb.stop() # Note some interrupt might end this prematurely - rtc.wakeup(None) - t = ticks_diff(end, ticks_ms()) - - def lo_power(t_ms): - rtc.wakeup(t_ms) - while True: - pyb.stop() - yield From 9101ea37d540ec0e4ea2a46c3677829593f6c21a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 25 Jul 2018 13:38:31 +0100 Subject: [PATCH 061/472] Tidy up low power doc. --- lowpower/README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lowpower/README.md b/lowpower/README.md index f74ce4e..169360d 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -161,12 +161,15 @@ while True: ``` This accords with the 500μA maximum specification for `stop`. So current -consumption can be estimated by `i = ib + n/latency` where `ib` is the stopped -current (in my case 380μA) and `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. +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. # 4. The rtc_time module @@ -300,8 +303,8 @@ with high latency to conserve battery. # 6. Note on the design The `rtc_time` module represents a compromise designed to minimise changes to -`uasyncio`. The aim is to have zero effect on the performance of normal -applications or code running on non-Pyboard hardware. +`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 From b08ab3c51d3fc029a19a5403c9496f8538d59c2b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 25 Jul 2018 17:13:45 +0100 Subject: [PATCH 062/472] Update FASTPOLL.md --- FASTPOLL.md | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 4f467f6..096a597 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -225,14 +225,29 @@ 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. -The version also supports an `implementation` namedtuple with the following -fields: - * `name` 'fast_io' - * `variant` `standard` - * `major` 0 Major version no. - * `minor` 100 Minor version no. i.e. version = 0.100 - -The `variant` field can also contain `lowpower` if running that version. +The version also supports an `version` variable containing 'fast_io'. This +enables the presence of this version to be determined at runtime. + +It also supports a `got_event_loop()` function returning a `bool`: `True` if +the event loop has been instantiated. The purpose is to enable code which uses +the event loop to raise an exception if the event loop was not instantiated. + +```python +class Foo(): + def __init__(self): + if asyncio.got_event_loop(): + loop = asyncio.get_event_loop() + loop.create_task(self._run()) + else: + raise OSError('Foo class requires an event loop instance') +``` +This avoids subtle errors: +```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) +``` ###### [Contents](./FASTPOLL.md#contents) From 71ebf31339f64284506a05d8158e9b72b34f378e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 26 Jul 2018 07:35:19 +0100 Subject: [PATCH 063/472] Tutorial: add warning on get_event_loop() args. --- TUTORIAL.md | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 4629027..18d01af 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -52,6 +52,7 @@ asyncio and includes a section for complete beginners. 6.4 [Testing](./TUTORIAL.md#64-testing) 6.5 [A common error](./TUTORIAL.md#65-a-common-error) This can be hard to find. 6.6 [Socket programming](./TUTORIAL.md#66-socket-programming) + 6.7 [Event loop constructor args](./TUTORIAL.md#67-event-loop-constructor-args) 7. [Notes for beginners](./TUTORIAL.md#7-notes-for-beginners) 7.1 [Problem 1: event loops](./TUTORIAL.md#71-problem-1:-event-loops) 7.2 [Problem 2: blocking methods](./TUTORIAL.md#7-problem-2:-blocking-methods) @@ -240,7 +241,8 @@ 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. +is usually sufficient. If using non-default values see +[Event loop constructor args](./TUTORIAL.md#67-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. @@ -984,9 +986,9 @@ 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: +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(): @@ -1359,11 +1361,11 @@ time of writing there is a bug in `uasyncio` which prevents this from woking. 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) addresses this. +[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` version addresses this issue. +The [fast_io](./FASTPOLL.md) version addresses this issue. ###### [Contents](./TUTORIAL.md#contents) @@ -1374,7 +1376,7 @@ 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 us) and sets an event, +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. @@ -1552,6 +1554,35 @@ An alternative approach is to use blocking sockets with `StreamReader` and ###### [Contents](./TUTORIAL.md#contents) +## 6.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 only safe 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 +``` + +Ref [this issue](https://github.com/micropython/micropython-lib/issues/295). + +###### [Contents](./TUTORIAL.md#contents) + # 7 Notes for beginners These notes are intended for those new to asynchronous code. They start by From 2709c56bd18da46bbc46ba7ec088f54f85042e6d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 26 Jul 2018 08:23:16 +0100 Subject: [PATCH 064/472] Docs: minor fixes. --- FASTPOLL.md | 50 +++++++++++++++++++++++--------------------------- TUTORIAL.md | 32 ++++++++++++++++---------------- 2 files changed, 39 insertions(+), 43 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 096a597..2198c02 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -1,8 +1,8 @@ -# fast_io: An experimental modified version of uasyncio +# fast_io: A modified version of uasyncio 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 aysnchronously set flag needs to be +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. @@ -16,7 +16,7 @@ This version has the following changes: [PR287](https://github.com/micropython/micropython-lib/pull/287). * The bug with read/write device drivers is fixed (forthcoming PR). * 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) + is called with a generator function [PR292](https://github.com/micropython/micropython-lib/pull/292). A key advantage of this version is that priority device drivers are written entirely by using the officially-supported technique for writing stream I/O @@ -25,32 +25,28 @@ means than by these proposals, application code changes are likely to be minimal. Using the priority mechanism in this version requires a change to just one line of code compared to an application running under the official version. -The high priority mechanism in `asyncio_priority.py` is replaced with a faster -and more efficient way of handling asynchronous events with minimum latency. -Consequently `asyncio_priority.py` is obsolete and should be deleted from your -system. The facility for low priority coros is currently unavailable. +The high priority mechanism formerly provided in `asyncio_priority.py` is +replaced with a faster and more efficient way of handling asynchronous events +with minimum latency. Consequently `asyncio_priority.py` is obsolete and should +be deleted from your system. The facility for low priority coros is currently +unavailable but will be reinstated. + +This modified version also provides for ultra low power consumption using a +module documented [here](./lowpower/README.md). ###### [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) - - 4. [ESP Platforms](./FASTPOLL.md#4-esp-platforms) - - 5. [Background](./FASTPOLL.md#4-background) + 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) + 4. [ESP Platforms](./FASTPOLL.md#4-esp-platforms) + 5. [Background](./FASTPOLL.md#4-background) # 1. Installation @@ -69,7 +65,7 @@ The benchmarks directory contains files demonstrating the performance gains offered by prioritisation. They also offer illustrations of the use of these features. Documentation is in the code. - * ``benchmarks/rate.py` Shows the frequency with which uasyncio schedules + * `benchmarks/rate.py` Shows the frequency with which uasyncio schedules minimal coroutines (coros). * `benchmarks/rate_esp.py` As above for ESP32 and ESP8266. * `benchmarks/rate_fastio.py` Measures the rate at which coros can be scheduled @@ -126,7 +122,7 @@ 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 -suficient to avoid overruns. +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. @@ -225,7 +221,7 @@ 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. -The version also supports an `version` variable containing 'fast_io'. This +The version also supports a `version` variable containing 'fast_io'. This enables the presence of this version to be determined at runtime. It also supports a `got_event_loop()` function returning a `bool`: `True` if diff --git a/TUTORIAL.md b/TUTORIAL.md index 18d01af..bd33b57 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -38,7 +38,7 @@ asyncio and includes a section for complete beginners. 5. [Interfacing hardware](./TUTORIAL.md#5-interfacing-hardware) 5.1 [Timing issues](./TUTORIAL.md#51-timing-issues) 5.2 [Polling hardware with a coroutine](./TUTORIAL.md#52-polling-hardware-with-a-coroutine) - 5.3 [Using the stream mechnanism](./TUTORIAL.md#53-using-the-stream-mechanism) + 5.3 [Using the stream mechanism](./TUTORIAL.md#53-using-the-stream-mechanism) 5.3.1 [A UART driver example](./TUTORIAL.md#531-a-uart-driver-example) 5.4 [Writing streaming device drivers](./TUTORIAL.md#54-writing-streaming-device-drivers) 5.5 [A complete example: aremote.py](./TUTORIAL.md#55-a-complete-example-aremotepy) @@ -86,8 +86,8 @@ 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 tutoial advocates a consistent programming style with good compatibility -with CPython V3.5 and above. +This tutorial aims to present a consistent programming style compatible with +CPython V3.5 and above. ## 0.1 Installing uasyncio on bare metal @@ -101,7 +101,7 @@ Libraries to be installed are: The `queues` and `synchro` modules are optional, but are required to run all the examples below. -The oficial approach is to use the `upip` utility as described +The official approach is to use the `upip` utility as described [here](https://github.com/micropython/micropython-lib). Network enabled hardware has this included in the firmware so it can be run locally. This is the preferred approach. @@ -114,7 +114,7 @@ then copying the library to the target. The need for Linux and the Unix build may be avoided by using [micropip.py](https://github.com/peterhinch/micropython-samples/tree/master/micropip). This runs under Python 3.2 or above. Create a temporary directory on your PC -and install to that. Then copy the contents of the temporary direcory to the +and install to that. Then copy the contents of the temporary directory to the device. The following assume Linux and a temporary directory named `~/syn` - adapt to suit your OS. The first option requires that `micropip.py` has executable permission. @@ -374,7 +374,7 @@ 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 alterantive to the official implementation. +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` @@ -474,7 +474,7 @@ async def foo(event): event.set() ``` -Where multiple coros wait on a single event synchronisationcan be achieved by +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 @@ -619,7 +619,7 @@ no mechanism for verifying when cancellation has actually occurred. The `asyn` library provides verification via the following classes: 1. `Cancellable` This allows one or more tasks to be assigned to a group. A - coro can cancel all tasks in the group, pausing until this has been acheived. + coro can cancel all tasks in the group, pausing until this has been achieved. Documentation may be found [here](./PRIMITIVES.md#42-class-cancellable). 2. `NamedTask` This enables a coro to be associated with a user-defined name. The running status of named coros may be checked. For advanced usage more @@ -1095,7 +1095,7 @@ class RecordOrientedUart(): def __iter__(self): # Not __await__ issue #2678 data = b'' while not data.endswith(self.DELIMITER): - yield from asyncio.sleep(0) # Neccessary because: + 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()))) @@ -1152,7 +1152,7 @@ async def receiver(): sreader = asyncio.StreamReader(uart) while True: res = await sreader.readline() - print('Recieved', res) + print('Received', res) loop = asyncio.get_event_loop() loop.create_task(sender()) @@ -1357,7 +1357,7 @@ 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 woking. +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 @@ -1406,7 +1406,7 @@ run while acquisition is in progress. 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 confirmtion that the +periodically toggles an onboard LED. This provides confirmation that the scheduler is running. ###### [Contents](./TUTORIAL.md#contents) @@ -1470,7 +1470,7 @@ the outer loop: def __await__(self): data = b'' while not data.endswith(self.DELIMITER): - yield from asyncio.sleep(0) # Neccessary because: + 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()))) @@ -1630,7 +1630,7 @@ def event_loop(): # handle UART input ``` -This works for simple examples but event loops rapidly become unweildy as the +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 @@ -1800,7 +1800,7 @@ 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 neccessary to use +required, especially one below a few ms, it may be necessary to use `utime.sleep_us(us)`. ###### [Contents](./TUTORIAL.md#contents) @@ -1812,7 +1812,7 @@ 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. -Fistly, it is lightweight. It is possible to have large numbers of coroutines +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 From ac44723c19236eeec44265bdc323a296e3a90193 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 26 Jul 2018 10:55:05 +0100 Subject: [PATCH 065/472] Reinstate low priority coros. Not yet documented. --- FASTPOLL.md | 6 ++-- fast_io/__init__.py | 4 +-- fast_io/core.py | 68 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 2198c02..c26ed32 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -28,8 +28,10 @@ one line of code compared to an application running under the official version. The high priority mechanism formerly provided in `asyncio_priority.py` is replaced with a faster and more efficient way of handling asynchronous events with minimum latency. Consequently `asyncio_priority.py` is obsolete and should -be deleted from your system. The facility for low priority coros is currently -unavailable but will be reinstated. +be deleted from your system. + +The facility for low priority coros formerly provided by `asyncio_priority.py` +exists but is not yet documented. This modified version also provides for ultra low power consumption using a module documented [here](./lowpower/README.md). diff --git a/fast_io/__init__.py b/fast_io/__init__.py index c4e7dc9..d588a80 100644 --- a/fast_io/__init__.py +++ b/fast_io/__init__.py @@ -19,8 +19,8 @@ def set_debug(val): # to add_reader. Cand we fix this by maintaining two object maps? class PollEventLoop(EventLoop): - def __init__(self, runq_len=16, waitq_len=16, fast_io=0): - EventLoop.__init__(self, runq_len, waitq_len, fast_io) + 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 = {} diff --git a/fast_io/core.py b/fast_io/core.py index 49892df..be90279 100644 --- a/fast_io/core.py +++ b/fast_io/core.py @@ -33,8 +33,10 @@ class TimeoutError(CancelledError): class EventLoop: - def __init__(self, runq_len=16, waitq_len=16, ioq_len=0): + def __init__(self, runq_len=16, waitq_len=16, ioq_len=0, lp_len=0): self.runq = ucollections.deque((), runq_len, True) + self._max_overdue_ms = 0 + self.lpq = utimeq.utimeq(lp_len) if lp_len else None self.ioq_len = ioq_len if ioq_len: self.ioq = ucollections.deque((), ioq_len, True) @@ -64,6 +66,24 @@ def _call_now(self, callback, *args): # For stream I/O only if not isinstance(callback, type_gen): self.ioq.append(args) + def max_overdue_ms(self, t=None): + if t is not None: + self._max_overdue_ms = int(t) + return self._max_overdue_ms + + # 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) + 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)) @@ -96,6 +116,22 @@ def run_forever(self): 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_overdue_ms > 0 and tim < -self._max_overdue_ms + 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) + self.call_soon(cur_task[1], *cur_task[2]) + while self.waitq: t = self.waitq.peektime() delay = time.ticks_diff(t, tnow) @@ -139,6 +175,7 @@ def run_forever(self): 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 @@ -150,6 +187,10 @@ def run_forever(self): 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) # Why? I think this is for debugging. If it is scheduled other than by wait # (which does pend_throw(None) an exception (exception doesn't inherit from Exception) is thrown @@ -194,7 +235,9 @@ def run_forever(self): # 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 delay: + 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) @@ -209,6 +252,13 @@ def run_forever(self): 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): @@ -258,10 +308,10 @@ class IOWriteDone(SysCall1): _event_loop = None _event_loop_class = EventLoop -def get_event_loop(runq_len=16, waitq_len=16, ioq_len=0): +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) + _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. @@ -342,6 +392,16 @@ def wait_for(coro, timeout): 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 From 3677fb4b145b3c4c821777533f2f16caa8763ec9 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 26 Jul 2018 12:31:35 +0100 Subject: [PATCH 066/472] Reinstate low priority benchmarks and docs. --- FASTPOLL.md | 192 +++++++++++++++++++++++++++++++++--- benchmarks/call_lp.py | 43 ++++++++ benchmarks/latency.py | 123 +++++++++++++++++++++++ benchmarks/overdue.py | 44 +++++++++ benchmarks/priority_test.py | 82 +++++++++++++++ benchmarks/rate_esp.py | 50 ++++++++++ 6 files changed, 521 insertions(+), 13 deletions(-) create mode 100644 benchmarks/call_lp.py create mode 100644 benchmarks/latency.py create mode 100644 benchmarks/overdue.py create mode 100644 benchmarks/priority_test.py create mode 100644 benchmarks/rate_esp.py diff --git a/FASTPOLL.md b/FASTPOLL.md index c26ed32..c52f67c 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -14,6 +14,8 @@ fail to handle concurrent input and output. This version has the following changes: * 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. * The bug with read/write device drivers is fixed (forthcoming PR). * 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). @@ -31,7 +33,7 @@ with minimum latency. Consequently `asyncio_priority.py` is obsolete and should be deleted from your system. The facility for low priority coros formerly provided by `asyncio_priority.py` -exists but is not yet documented. +is now implemented. This modified version also provides for ultra low power consumption using a module documented [here](./lowpower/README.md). @@ -47,6 +49,12 @@ module documented [here](./lowpower/README.md). 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 I/O](./FASTPOLL.md#31-fast-I/O) + 3.2 [Low Priority](./FASTPOLL.md#32-low-priority) + 3.3 [Other Features](./FASTPOLL.md#33-other-features) + 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) @@ -67,6 +75,8 @@ The benchmarks directory contains files demonstrating the performance gains offered by prioritisation. They also offer illustrations of the use of these features. Documentation is in the code. + * `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. @@ -77,9 +87,12 @@ features. Documentation is in the code. * `fast_io/pin_cb.py` Demo of an I/O device driver which causes a pin state change to trigger a callback. * `fast_io/pin_cb_test.py` Demo of above. + * `benchmarks/call_lp.py` Demos low priority callbacks. + * `benchmarks/overdue.py` Demo of maximum overdue feature. + * `priority_test.py` Cancellation of low priority coros. -With the exception of `rate_fastio`, benchmarks can be run against the official -and priority versions of usayncio. +With the exceptions of `call_lp`, `priority` and `rate_fastio`, benchmarks can +be run against the official and priority versions of usayncio. # 2. Rationale @@ -99,6 +112,12 @@ 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. +It 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 can +exceed two orders of magnitude. + ## 2.1 Latency Coroutines in uasyncio which are pending execution are scheduled in a "fair" @@ -129,9 +148,20 @@ 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 current version of `uasyncio` has even higher levels of latency for I/O +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. @@ -196,19 +226,41 @@ 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 an `ioq_len=0` argument to `get_event_loop`. The -zero default causes the scheduler to operate as per the official version. If an -I/O queue length > 0 is provided, I/O performed by `StreamReader` and -`StreamWriter` objects will be prioritised over other coros. +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. 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 will be 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 competition with other tasks. Arguments to `get_event_loop()`: - 1. `runq_len` Length of normal queue. Default 16 tasks. - 2. `waitq_len` Length of wait queue. Default 16. - 3. `ioq_len` Length of I/O queue. Default 0. + 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. + +## 3.1 Fast I/O Device drivers which are to be capable of running at high priority should be written to use stream I/O: see @@ -223,8 +275,31 @@ 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. -The version also supports a `version` variable containing 'fast_io'. This -enables the presence of this version to be determined at runtime. +## 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) + +## 3.3 Other Features + +The version has a `version` variable containing 'fast_io'. This enables the +presence of this version to be determined at runtime. It also supports a `got_event_loop()` function returning a `bool`: `True` if the event loop has been instantiated. The purpose is to enable code which uses @@ -246,6 +321,97 @@ bar = Bar() # Constructor calls get_event_loop() # and renders these args inoperative loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) ``` +## 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 + +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 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#36-task-cancellation) +and [Coroutines with timeouts](./TUTORIAL.md#44-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` 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) diff --git a/benchmarks/call_lp.py b/benchmarks/call_lp.py new file mode 100644 index 0000000..becc1a0 --- /dev/null +++ b/benchmarks/call_lp.py @@ -0,0 +1,43 @@ +# 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 asyncio.version != 'fast_io': + raise AttributeError +except AttributeError: + raise OSError('This program requires uasyncio fast_io version.') + +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/benchmarks/latency.py b/benchmarks/latency.py new file mode 100644 index 0000000..0d6985a --- /dev/null +++ b/benchmarks/latency.py @@ -0,0 +1,123 @@ +# 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 asyncio.version != 'fast_io': + 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/benchmarks/overdue.py b/benchmarks/overdue.py new file mode 100644 index 0000000..85f5b9c --- /dev/null +++ b/benchmarks/overdue.py @@ -0,0 +1,44 @@ +# overdue.py Test for "low priority" uasyncio. Author Peter Hinch April 2017. +import uasyncio as asyncio +p_version = True +try: + if asyncio.version != 'fast_io': + raise AttributeError +except AttributeError: + p_version = False + +if not p_version: + raise OSError('This program requires uasyncio fast_io version.') + +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/benchmarks/priority_test.py b/benchmarks/priority_test.py new file mode 100644 index 0000000..cb8892c --- /dev/null +++ b/benchmarks/priority_test.py @@ -0,0 +1,82 @@ +# 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 +p_version = True +try: + if asyncio.version != 'fast_io': + raise AttributeError +except AttributeError: + p_version = False + +if not p_version: + raise OSError('This program requires uasyncio fast_io version.') + +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/benchmarks/rate_esp.py b/benchmarks/rate_esp.py new file mode 100644 index 0000000..a2a54e4 --- /dev/null +++ b/benchmarks/rate_esp.py @@ -0,0 +1,50 @@ +# 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()) + From 76743bcf7e5c0baac41977901bac09b6d8611d10 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 26 Jul 2018 12:34:41 +0100 Subject: [PATCH 067/472] Reinstate low priority benchmarks and docs. --- FASTPOLL.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index c52f67c..588f149 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -49,7 +49,7 @@ module documented [here](./lowpower/README.md). 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 I/O](./FASTPOLL.md#31-fast-I/O) + 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.4 [Low priority yield](./FASTPOLL.md#34-low-priority-yield) @@ -260,7 +260,9 @@ Arguments to `get_event_loop()`: 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. -## 3.1 Fast I/O +###### [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 @@ -275,6 +277,8 @@ 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 @@ -296,6 +300,8 @@ It adds the following event loop methods: See [Low priority callbacks](./FASTPOLL.md#35-low-priority-callbacks) +###### [Contents](./FASTPOLL.md#contents) + ## 3.3 Other Features The version has a `version` variable containing 'fast_io'. This enables the @@ -321,6 +327,9 @@ bar = Bar() # Constructor calls get_event_loop() # and renders these args inoperative loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) ``` + +###### [Contents](./FASTPOLL.md#contents) + ## 3.4 Low priority yield Consider this code fragment: From 14bcf95443130d50463de7ffb9e4447c6fd3f8c3 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 26 Jul 2018 12:35:58 +0100 Subject: [PATCH 068/472] Reinstate low priority benchmarks and docs. --- FASTPOLL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 588f149..98539df 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -49,7 +49,7 @@ module documented [here](./lowpower/README.md). 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.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.4 [Low priority yield](./FASTPOLL.md#34-low-priority-yield) From c57486bc8a168ad704e9fa4e5306a223b635ea12 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 26 Jul 2018 13:58:38 +0100 Subject: [PATCH 069/472] Fixs to FASTPOLL.md. --- FASTPOLL.md | 58 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 98539df..90262ba 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -7,8 +7,8 @@ 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 current `uasyncio` polls I/O with a relatively high degree of -latency. It also has a bug whereby bidirectional devices such as UARTS could +Unfortunately official `uasyncio` polls I/O with a relatively high degree of +latency. It also has a bug whereby bidirectional devices such as UARTS can fail to handle concurrent input and output. This version has the following changes: @@ -18,7 +18,9 @@ This version has the following changes: * Callbacks can similarly be scheduled with low priority. * The bug with read/write device drivers is fixed (forthcoming PR). * 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). + 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. A key advantage of this version is that priority device drivers are written entirely by using the officially-supported technique for writing stream I/O @@ -35,8 +37,8 @@ be deleted from your system. The facility for low priority coros formerly provided by `asyncio_priority.py` is now implemented. -This modified version also provides for ultra low power consumption using a -module documented [here](./lowpower/README.md). +This version also provides for ultra low power consumption using a module +documented [here](./lowpower/README.md). ###### [Main README](./README.md) @@ -69,6 +71,10 @@ above two files implemented as frozen bytecode. See [ESP Platforms](./FASTPOLL.md#6-esp-platforms) for general comments on the suitability of ESP platforms for systems requiring fast response. +It is possible to load modules in the filesystem in preference to frozen ones +by modifying `sys.path`. However the ESP8266 probably has too little RAM for +this to be useful. + ## 1.1 Benchmarks The benchmarks directory contains files demonstrating the performance gains @@ -82,14 +88,14 @@ features. Documentation is in the code. * `benchmarks/rate_esp.py` As above for ESP32 and ESP8266. * `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` Demos low priority callbacks. + * `benchmarks/overdue.py` Demo of maximum overdue feature. + * `benchmarks/priority_test.py` Cancellation of low priority coros. * `fast_io/ms_timer.py` Provides higher precision timing than `wait_ms()`. - * `fast_io/ms_timer.py` Test/demo program for above. + * `fast_io/ms_timer_test.py` Test/demo program for above. * `fast_io/pin_cb.py` Demo of an I/O device driver which causes a pin state change to trigger a callback. * `fast_io/pin_cb_test.py` Demo of above. - * `benchmarks/call_lp.py` Demos low priority callbacks. - * `benchmarks/overdue.py` Demo of maximum overdue feature. - * `priority_test.py` Cancellation of low priority coros. With the exceptions of `call_lp`, `priority` and `rate_fastio`, benchmarks can be run against the official and priority versions of usayncio. @@ -115,8 +121,8 @@ realised. It 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 can -exceed two orders of magnitude. +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 @@ -248,11 +254,12 @@ Examples are: 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. 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 will be 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 competition with other tasks. +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. @@ -266,7 +273,7 @@ Arguments to `get_event_loop()`: Device drivers which are to be capable of running at high priority should be written to use stream I/O: see -[Writing IORead device drivers](./TUTORIAL.md#54-writing-ioread-device-drivers). +[Writing streaming device drivers](./TUTORIAL.md#54-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 @@ -304,13 +311,14 @@ See [Low priority callbacks](./FASTPOLL.md#35-low-priority-callbacks) ## 3.3 Other Features -The version has a `version` variable containing 'fast_io'. This enables the -presence of this version to be determined at runtime. - -It also supports a `got_event_loop()` function returning a `bool`: `True` if -the event loop has been instantiated. The purpose is to enable code which uses -the event loop to raise an exception if the event loop was not instantiated. +Variable: + * `version` Contains 'fast_io'. Enables the presence of this version to be + determined at runtime. +Function: + * `got_event_loop()` No arg. Returns a `bool`: `True` if the event loop has + been instantiated. Enables code using the event loop to raise an exception if + the event loop was not instantiated: ```python class Foo(): def __init__(self): @@ -357,6 +365,7 @@ 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: @@ -404,7 +413,8 @@ 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` Schedule a callback with low priority. Positional args: +`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. From bc278d25e8239977097d10d476c034188b065cf0 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 26 Jul 2018 14:09:24 +0100 Subject: [PATCH 070/472] Fixs to FASTPOLL.md. --- FASTPOLL.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 90262ba..0403acb 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -16,23 +16,25 @@ This version has the following changes: [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. - * The bug with read/write device drivers is fixed (forthcoming PR). + * The bug with read/write device drivers is fixed. * 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. - -A key advantage of this version is that priority device drivers are written -entirely by using the officially-supported technique for writing stream I/O -drivers. If official `uasyncio` acquires a means of prioritising I/O by other -means than by these proposals, application code changes are likely to be -minimal. Using the priority mechanism in this version requires a change to just -one line of code compared to an application running under the official version. - -The high priority mechanism formerly provided in `asyncio_priority.py` is -replaced with a faster and more efficient way of handling asynchronous events -with minimum latency. Consequently `asyncio_priority.py` is obsolete and should -be deleted from your system. + * The version and the presence of an event loop instance can be tested at + runtime. + +Note that priority device drivers are written by using the officially supported +technique for writing stream I/O drivers. If official `uasyncio` acquires a +means of prioritising I/O other than that in this version, application code +changes should be minimal. Using the fast I/O mechanism in this version +requires changing just one line of code compared to running under the official +version. + +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. From f82cbaa261bacbd307cf570446c836b9b4b69b8b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 27 Jul 2018 11:38:09 +0100 Subject: [PATCH 071/472] Doc improvements. --- FASTPOLL.md | 63 ++++++++++++++++++++++++++----------- README.md | 66 +++++++++++++++++++-------------------- benchmarks/rate.py | 4 ++- benchmarks/rate_fastio.py | 5 +-- 4 files changed, 83 insertions(+), 55 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 0403acb..44b44d0 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -1,22 +1,19 @@ # fast_io: A modified version of uasyncio -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. It also has a bug whereby bidirectional devices such as UARTS can -fail to handle concurrent input and output. +This version is a "drop in" replacement for official `uasyncio`. Existing +applications should run under it unchanged and with essentially identical +performance. -This version has the following changes: +This version has the following features: * 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. - * The bug with read/write device drivers is fixed. + * 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). * 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 @@ -34,13 +31,8 @@ version. 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. - -This version also provides for ultra low power consumption using a module -documented [here](./lowpower/README.md). +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) @@ -61,6 +53,7 @@ documented [here](./lowpower/README.md). 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 @@ -104,6 +97,16 @@ be run against the official and priority versions of usayncio. # 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 @@ -454,3 +457,25 @@ 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 + +This version is designed to enable existing applications to run without change +to code and to minimise the effect on raw scheduler performance in the case +where the added functionality is unused. + +The benchmark `rate.py` measures the rate at which tasks can be scheduled. It +was run (on a Pyboard V1.1) under official `uasyncio` V2, then under this +version. The benchmark `rate_fastio` is identical except it instantiates an I/O +queue and a low priority queue. Results were as follows. + +| Script | Uasyncio version | Period (100 coros) | Overhead | +| --- | --- | --- | +| rate | Official V2 | 156μs | 0% | +| rate | fast_io | 162μs | 3.4% | +| rate_fastio | fast_io | 206μs | 32% | + +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. diff --git a/README.md b/README.md index 78aacdb..13bec4d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # 1. The MicroPython uasyncio library -This GitHub repository consists of the following parts. Firstly a modified -`fast_io` version of `uasyncio` offering some benefits over the official -version. +This repository comprises the following parts. + 1. A modified [fast_io](./FASTPOLL.md) version of `uasyncio`. This is a "drop + in" replacement for the official version providing additional functionality. + 2. A module enabling the `fast_io` version to run with very low power draw. + 3. Resources for users of official or `fast_io` versions: -Secondly the following resources relevant to users of official or `fast_io` -versions: * [A tutorial](./TUTORIAL.md) An introductory tutorial on asynchronous - programming and the use of the uasyncio library (asyncio subset). + programming and the use of the `uasyncio` library (asyncio subset). * [Asynchronous device drivers](./DRIVERS.md). A module providing drivers for devices such as switches and pushbuttons. * [Synchronisation primitives](./PRIMITIVES.md). Provides commonly used @@ -29,34 +29,9 @@ versions: * [Under the hood](./UNDER_THE_HOOD.md) A guide to help understand the `uasyncio` code. For scheduler geeks and those wishing to modify `uasyncio`. -## 1.1 The "fast_io" version. - -This repo included `asyncio_priority.py` which is now deprecated. Its primary -purpose was to provide a means of servicing fast hardware devices by means of -coroutines running at a high priority. The official firmware now includes -[this major improvement](https://github.com/micropython/micropython/pull/3836) -which offers a much more efficient way of achieving this end. The tutorial has -details of how to use this. - -The current `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. - -A modified version of `uasyncio` is described [here](./FASTPOLL.md) which -provides an option for I/O scheduling with much reduced latency. It also fixes -the bug. It is hoped that these changes will be accepted into mainstream in due -course. - -### 1.1.1 A Pyboard-only low power version - -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 the library on battery powered projects. - # 2. Version and installation of uasyncio -The documentation and code in this repository are based on `uasyncio` version +The documentation and code in this repository assume `uasyncio` version 2.0, which is the version on PyPi and in the official micropython-lib. This requires firmware dated 22nd Feb 2018 or later. Use of the stream I/O mechanism requires firmware after 17th June 2018. @@ -114,7 +89,32 @@ 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. -# 4. The asyn.py library +# 4. The "fast_io" version. + +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. + +## 4.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. + +## 4.2 Historical note + +This repo formerly included `asyncio_priority.py` which is replaced. Its main +purpose was to provide a means of servicing fast hardware devices by means of +coroutines running at a high priority. The official firmware now includes +[this major improvement](https://github.com/micropython/micropython/pull/3836) +which offers a much more efficient way of achieving this end. The tutorial has +details of how to use this. + +# 5. The asyn.py library This library ([docs](./PRIMITIVES.md)) provides 'micro' implementations of the `asyncio` synchronisation primitives. diff --git a/benchmarks/rate.py b/benchmarks/rate.py index a366bca..8b7ceb8 100644 --- a/benchmarks/rate.py +++ b/benchmarks/rate.py @@ -3,7 +3,9 @@ # This measures the rate at which uasyncio can schedule a minimal coro which # mereley increments a global. -# Outcome: minimal coros are scheduled at an interval of ~150us +# 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 diff --git a/benchmarks/rate_fastio.py b/benchmarks/rate_fastio.py index 3e957f2..d1ce969 100644 --- a/benchmarks/rate_fastio.py +++ b/benchmarks/rate_fastio.py @@ -4,7 +4,8 @@ # This measures the rate at which uasyncio can schedule a minimal coro which # mereley increments a global. -# Outcome: minimal coros are scheduled at an interval of ~200us +# 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 @@ -41,7 +42,7 @@ async def test(): done = True ntasks = max(num_coros) + 2 -loop = asyncio.get_event_loop(ntasks, ntasks, 6) +loop = asyncio.get_event_loop(ntasks, ntasks, 6, 6) loop.create_task(test()) loop.run_until_complete(report()) From 22dfb1b8c2be345a3ea0b350c77fc7107eeedc1b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 27 Jul 2018 12:01:39 +0100 Subject: [PATCH 072/472] Doc improvements. --- README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 13bec4d..f0a480b 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,9 @@ This repository comprises the following parts. 1. A modified [fast_io](./FASTPOLL.md) version of `uasyncio`. This is a "drop in" replacement for the official version providing additional functionality. - 2. A module enabling the `fast_io` version to run with very low power draw. - 3. Resources for users of official or `fast_io` versions: + 2. A module enabling the [fast_io](./FASTPOLL.md) version to run with very low + power draw. + 3. Resources for users of official or [fast_io](./FASTPOLL.md) versions: * [A tutorial](./TUTORIAL.md) An introductory tutorial on asynchronous programming and the use of the `uasyncio` library (asyncio subset). @@ -98,6 +99,11 @@ 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#54-writing-streaming-device-drivers) +has details of how to write streaming drivers. + ## 4.1 A Pyboard-only low power module This is documented [here](./lowpower/README.md). In essence a Python file is @@ -107,12 +113,14 @@ using `uasyncio` in battery powered projects. ## 4.2 Historical note -This repo formerly included `asyncio_priority.py` which is replaced. Its main +This repo formerly included `asyncio_priority.py` which is obsolete. Its main purpose was to provide a means of servicing fast hardware devices by means of -coroutines running at a high priority. The official firmware now includes +coroutines running at a high priority. This was essentially a workround. + +The official firmware now includes [this major improvement](https://github.com/micropython/micropython/pull/3836) -which offers a much more efficient way of achieving this end. The tutorial has -details of how to use this. +which offers a much more efficient way of achieving the same end using stream +I/O and efficient polling using `select.poll`. # 5. The asyn.py library From 75d8bd4f36cecb546197ab81a397a28759859187 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 28 Jul 2018 14:47:25 +0100 Subject: [PATCH 073/472] Add current waveform images. --- README.md | 2 +- lowpower/README.md | 12 ++++++++++++ lowpower/current.png | Bin 0 -> 9794 bytes lowpower/current1.png | Bin 0 -> 10620 bytes 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 lowpower/current.png create mode 100644 lowpower/current1.png diff --git a/README.md b/README.md index f0a480b..f118beb 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ This repository comprises the following parts. # 2. Version and installation of uasyncio The documentation and code in this repository assume `uasyncio` version -2.0, which is the version on PyPi and in the official micropython-lib. This +2.0.x, which is the version on PyPi and in the official micropython-lib. This requires firmware dated 22nd Feb 2018 or later. Use of the stream I/O mechanism requires firmware after 17th June 2018. diff --git a/lowpower/README.md b/lowpower/README.md index 169360d..a3c56d0 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -171,6 +171,18 @@ 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 + +Running `lpdemo.py` while it waits for a button press with latency = 200ms. +Vertical 20mA/div +Horizontal 50ms/div +![Image](./lowpower.png) + +Vertical 20mA/div +Horizontal 500μs/div +![Image](./lowpower1.png) + + # 4. The rtc_time module This provides the following. diff --git a/lowpower/current.png b/lowpower/current.png new file mode 100644 index 0000000000000000000000000000000000000000..df45b56bf72c3cfadb6ffce1e38e34d1a549745f GIT binary patch literal 9794 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 EUmxY~UH||9 literal 0 HcmV?d00001 diff --git a/lowpower/current1.png b/lowpower/current1.png new file mode 100644 index 0000000000000000000000000000000000000000..feec09142598a3ad6b5438f90803d4700d0bf67b GIT binary patch literal 10620 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 Date: Sat, 28 Jul 2018 14:50:12 +0100 Subject: [PATCH 074/472] Add current waveform images. --- lowpower/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lowpower/README.md b/lowpower/README.md index a3c56d0..bac3209 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -176,11 +176,11 @@ for such applications during their waiting period. Running `lpdemo.py` while it waits for a button press with latency = 200ms. Vertical 20mA/div Horizontal 50ms/div -![Image](./lowpower.png) +![Image](./current.png) Vertical 20mA/div Horizontal 500μs/div -![Image](./lowpower1.png) +![Image](./current1.png) # 4. The rtc_time module From 75c3a92d2e9e9c8875b1c500f34af5b3ec1f95a6 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 28 Jul 2018 15:34:48 +0100 Subject: [PATCH 075/472] Tidy up lowpower README. --- lowpower/README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/lowpower/README.md b/lowpower/README.md index bac3209..81f5f59 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -2,6 +2,25 @@ Release 0.1 25th July 2018 + 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 pyb.stop](./README.md#321-consequences-of-pyb.stop) + 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](./README.md#322-measured-results) + 3.2.3 [Current waveforms](./README.md#323-current-waveforms) + 4. [The rtc_time module](./README.md#4-the-rtc_time-module) + 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 @@ -33,6 +52,8 @@ 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 @@ -53,6 +74,8 @@ tested. Copy the file `rtc_time.py` to the device so that it is on `sys.path`. The test program `lowpower.py` requires a link between pins X1 and X2 to enable UART 4 to receive data via a loopback. +###### [Contents](./README.md#a-low-power-usayncio-adaptation) + # 3 Low power uasyncio operation ## 3.1 The official uasyncio package @@ -98,6 +121,8 @@ before yielding with a zero delay. The duration of the `stop` condition full speed. The `yield` allows each pending task to run once before the scheduler is again paused (if `latency` > 0). +###### [Contents](./README.md#a-low-power-usayncio-adaptation) + ### 3.2.1 Consequences of pyb.stop #### 3.2.1.1 Timing Accuracy and rollover @@ -142,6 +167,8 @@ else: 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 The `lpdemo.py` script consumes a mean current of 980μA with 100ms latency, and @@ -174,14 +201,18 @@ for such applications during their waiting period. ### 3.2.3 Current waveforms 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) +###### [Contents](./README.md#a-low-power-usayncio-adaptation) # 4. The rtc_time module @@ -224,6 +255,8 @@ around or to make it global. Once instantiated, latency may be changed by rtc_time.Latency().value(t) ``` +###### [Contents](./README.md#a-low-power-usayncio-adaptation) + # 5. Application design Attention to detail is required to minimise power consumption, both in terms of @@ -312,6 +345,8 @@ 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. +###### [Contents](./README.md#a-low-power-usayncio-adaptation) + # 6. Note on the design The `rtc_time` module represents a compromise designed to minimise changes to @@ -337,3 +372,5 @@ The `rtc_time` module ensures that `uasyncio` uses `utime` for timing if the module is present in the path but is unused. This can occur because of an active USB connection or if running on an an incompatible platform. This ensures that under such conditions performance is unaffected. + +###### [Contents](./README.md#a-low-power-usayncio-adaptation) From 9fbcb739253de10d856fdbe822bda903f5c6c6f4 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 28 Jul 2018 16:03:19 +0100 Subject: [PATCH 076/472] Tidy up lowpower README. --- lowpower/README.md | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/lowpower/README.md b/lowpower/README.md index 81f5f59..a132119 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -8,7 +8,7 @@ Release 0.1 25th July 2018 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 pyb.stop](./README.md#321-consequences-of-pyb.stop) + 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](./README.md#322-measured-results) @@ -89,16 +89,24 @@ 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()`. An application-level scheme using `pyb.stop` to conserve power -would cause all `uasyncio` timing to become highly inaccurate. +`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`. Libraries using `uasyncio` will run unmodified, barring any -timing issues if user code increases scheduler latency. +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: @@ -123,7 +131,7 @@ scheduler is again paused (if `latency` > 0). ###### [Contents](./README.md#a-low-power-usayncio-adaptation) -### 3.2.1 Consequences of pyb.stop +### 3.2.1 Consequences of stop mode #### 3.2.1.1 Timing Accuracy and rollover @@ -207,7 +215,7 @@ 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. +typical that experienced when Python code is running. Vertical 20mA/div Horizontal 500μs/div ![Image](./current1.png) @@ -225,8 +233,8 @@ Variable: 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) +timebase. See the `utime` +[official documentation](http://docs.micropython.org/en/latest/pyboard/library/utime.html) for these. * `ticks_ms` * `ticks_add` @@ -289,8 +297,9 @@ 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 then reconfigure any pins which are to be used. -If I2C is to be used there are further implications: see the above reference. +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 @@ -327,8 +336,8 @@ async def foo(): await asyncio.sleep(0) ``` -will execute (at best) at a rate of 5Hz; possibly considerably less frequently -depending on the behaviour of competing tasks. Likewise +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(): From 177087a1c5b72911e1703fcae446433120112dbc Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 29 Jul 2018 11:47:55 +0100 Subject: [PATCH 077/472] run_until_complete returns coro return value. --- FASTPOLL.md | 17 ++++++++++------- fast_io/core.py | 6 +++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 44b44d0..25cc738 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -18,15 +18,18 @@ This version has the following features: 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 version and the presence of an event loop instance can be tested at - runtime. + * The presence 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). Note that priority device drivers are written by using the officially supported -technique for writing stream I/O drivers. If official `uasyncio` acquires a -means of prioritising I/O other than that in this version, application code -changes should be minimal. Using the fast I/O mechanism in this version -requires changing just one line of code compared to running under the official -version. +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. 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 diff --git a/fast_io/core.py b/fast_io/core.py index be90279..ed960a2 100644 --- a/fast_io/core.py +++ b/fast_io/core.py @@ -264,10 +264,10 @@ def run_forever(self): def run_until_complete(self, coro): assert not isinstance(coro, type_genf), 'Coroutine arg expected.' # upy issue #3241 def _run_and_stop(): - yield from coro - yield StopLoop(0) + ret = yield from coro # https://github.com/micropython/micropython-lib/pull/270 + yield StopLoop(ret) self.call_soon(_run_and_stop()) - self.run_forever() + return self.run_forever() def stop(self): self.call_soon((lambda: (yield StopLoop(0)))()) From efeeff3f855a701a190e0a436290b0c5cc94a4ad Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 30 Jul 2018 16:52:13 +0100 Subject: [PATCH 078/472] NEC IR driver and demos now run on ESP32. --- nec_ir/aremote.py | 4 ++++ nec_ir/art.py | 6 +++++- nec_ir/art1.py | 7 ++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/nec_ir/aremote.py b/nec_ir/aremote.py index 09cfa58..59c00c2 100644 --- a/nec_ir/aremote.py +++ b/nec_ir/aremote.py @@ -15,6 +15,8 @@ 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) @@ -48,6 +50,8 @@ def __init__(self, pin, callback, extended, *args): # Optional args for callbac 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) + 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 diff --git a/nec_ir/art.py b/nec_ir/art.py index 2174372..c861a50 100644 --- a/nec_ir/art.py +++ b/nec_ir/art.py @@ -8,9 +8,11 @@ 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': +elif platform == 'esp8266' or ESP32: from machine import Pin, freq else: print('Unsupported platform', platform) @@ -36,6 +38,8 @@ def test(): 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() diff --git a/nec_ir/art1.py b/nec_ir/art1.py index a11beb3..ae1978d 100644 --- a/nec_ir/art1.py +++ b/nec_ir/art1.py @@ -10,9 +10,10 @@ 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': +elif platform == 'esp8266' or ESP32: from machine import Pin, freq else: print('Unsupported platform', platform) @@ -48,6 +49,10 @@ def test(): 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() From 68f714586b78b08280b41b05c14ff3caae1fdc01 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 15 Oct 2018 12:01:14 +0100 Subject: [PATCH 079/472] I2C library added. --- README.md | 7 +- i2c/README.md | 290 ++++++++++++++++++++++++++++++++++++++++++++++++ i2c/asi2c.py | 185 ++++++++++++++++++++++++++++++ i2c/asi2c_i.py | 122 ++++++++++++++++++++ i2c/i2c_esp.py | 63 +++++++++++ i2c/i2c_init.py | 76 +++++++++++++ i2c/i2c_resp.py | 62 +++++++++++ 7 files changed, 803 insertions(+), 2 deletions(-) create mode 100644 i2c/README.md create mode 100644 i2c/asi2c.py create mode 100644 i2c/asi2c_i.py create mode 100644 i2c/i2c_esp.py create mode 100644 i2c/i2c_init.py create mode 100644 i2c/i2c_resp.py diff --git a/README.md b/README.md index f118beb..5cb1640 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,12 @@ This repository comprises the following parts. * [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. Primarily intended to enable a - a Pyboard-like device to achieve bidirectional communication with an ESP8266. + boards to communicate without using a UART. This is hardware agnostic but + slower than the I2C version. * [Under the hood](./UNDER_THE_HOOD.md) A guide to help understand the `uasyncio` code. For scheduler geeks and those wishing to modify `uasyncio`. diff --git a/i2c/README.md b/i2c/README.md new file mode 100644 index 0000000..242451e --- /dev/null +++ b/i2c/README.md @@ -0,0 +1,290 @@ +# 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. The other end may be any hardware +running MicroPython. + +The `Initiator` implements a timeout enabling it to detect failure of the other +end of the interface (the `Responder`). There is optional provision to reset +the `Responder` in this event. + +###### [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#21-configuration) Fine-tuning the interface. + 4.3 [Responder class](./README.md#431-responder-class) + 5. [Limitations](./README.md#5-limitations) + 5.1 [Blocking](./TUTORIAL.md#51-blocking) + 5.2 [Buffering](./TUTORIAL.md#52-buffering) + 5.3 [Responder crash detection](./TUTORIAL.md#53-responder-crash-detection) + +# 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 and may be as low as 100ms. + +The module will run under official or `fast_io` builds of `uasyncio`. Owing to +the latency discussed above the performance of this interface is largely +unaffected. + +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 iterface +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 is a typical `Initiator` usage example where the two participants +exchange Python objects serialised using `ujson`: + +```python +import uasyncio as asyncio +from pyb import I2C # Only pyb supports slave mode +from machine import Pin +import asi2c +import ujson + +i2c = I2C(1, mode=I2C.SLAVE) # Soft I2C may be used +syn = Pin('X11') # Pins are arbitrary but must be declared +ack = Pin('Y8') # using machine +rst = (Pin('X12'), 0, 200) # Responder reset is low for 200ms +chan = asi2c.Initiator(i2c, syn, ack, rst) + +async def receiver(): + sreader = asyncio.StreamReader(chan) + 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[0] += 1 + await asyncio.sleep_ms(800) +``` + +Code for `Responder` is very similar. See `i2c_init.py` and `i2c_resp.py` for +complete examples. + +###### [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. + +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 target with an active high 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. `timeout=1000` Timeout (in ms) before `Initiator` assumes `Responder` has + failed. + 2. `t_poll=100` Interval (ms) for `Initiator` polling `Responder`. + +Class variables should be set before instantiating `Initiator` or `Responder`. +See [Section 4.4](./README.md#44-configuration). + +Instance variables: +The `Initiator` maintains instance variables which may be used to measure its +peformance. See [Section 4.4](./README.md#44-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 instantiation. + +`Initiator.timeout` If the `Responder` fails the `Initiator` times out and +resets the `Responder`; this occurs if `reset` tuple with a pin is supplied. +Otherwise the `Initiator` raises an `OSError`. + +`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`. + +###### [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 variable: + 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. + +###### [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. + +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. This is 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 I/O pins. + +## 5.2 Buffering + +The protocol does not implement flow control, incoming data being buffered +until read. To avoid the risk of memory errors a coroutine should read incoming +data as it arrives. Since this is the normal mode of using such an interface I +see little merit in increasing the complexity of the code with flow control. + +Outgoing data is unbuffered. `StreamWriter.awrite` will pause until pending +data has been transmitted. + +## 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`. + +###### [Contents](./README.md#contents) diff --git a/i2c/asi2c.py b/i2c/asi2c.py new file mode 100644 index 0000000..66d2c90 --- /dev/null +++ b/i2c/asi2c.py @@ -0,0 +1,185 @@ +# 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): + 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'' + self.rxbyt = b'' + self.last_tx = 0 # Size of last buffer sent + + 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 + + # Concatenate incoming bytes instance. + def _handle_rxd(self, msg): + self.rxbyt = b''.join((self.rxbyt, msg)) + + # Get bytes if present. Return bytes, len as 2 bytes, len + def _get_tx_data(self): + d = self.txbyt + n = len(d) + return d, n.to_bytes(2, 'little'), n + + def _txdone(self): + self.txbyt = b'' + +# 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: + 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() + + def write(self, buf, off, sz): + if self.synchronised: + if self.txbyt: # Waiting for existing data to go out + return 0 + d = buf[off : off + sz] + self.txbyt = d + return len(d) + 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 + def __init__(self, i2c, pin, pinack, verbose=True): + super().__init__(i2c, pinack, pin, verbose) + 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)): +# tstart = utime.ticks_us() # TEST + 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 = int.from_bytes(sn, 'little') # no of bytes to receive + if n: + self.waitfor(1) + utime.sleep_us(_DELAY) + data = self.i2c.readfrom(addr, n) + self.own(1) + self.waitfor(0) + self.own(0) + self._handle_rxd(data) + + s, nb, n = self._get_tx_data() + self.own(1) # Request to send + self.waitfor(1) + utime.sleep_us(_DELAY) + self.i2c.writeto(addr, nb) + self.own(0) + self.waitfor(0) + if n: + self.own(1) + self.waitfor(1) + utime.sleep_us(_DELAY) + self.i2c.writeto(addr, s) + self.own(0) + self.waitfor(0) + self._txdone() # Invalidate source + self.rem.irq(handler = self._handler, trigger = machine.Pin.IRQ_RISING) +# print('Time: ', utime.ticks_diff(utime.ticks_us(), tstart)) diff --git a/i2c/asi2c_i.py b/i2c/asi2c_i.py new file mode 100644 index 0000000..55a8d4a --- /dev/null +++ b/i2c/asi2c_i.py @@ -0,0 +1,122 @@ +# 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): + timeout = 1000 # ms Timeout to detect slave down + t_poll = 100 # ms between Initiator polling Responder + + def __init__(self, i2c, pin, pinack, reset=None, verbose=True): + super().__init__(i2c, pin, pinack, verbose) + self.reset = reset + 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 + loop = asyncio.get_event_loop() + loop.create_task(self._run()) + + def waitfor(self, val): + to = Initiator.timeout + tim = utime.ticks_ms() + while not self.rem() == val: + if utime.ticks_diff(utime.ticks_ms(), tim) > to: + raise OSError + + async def reboot(self): + 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 + 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.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)): + to = Initiator.timeout + s, nb, n = self._get_tx_data() + self.own(1) # Triggers interrupt on responder + self.i2c.send(nb, timeout=to) # Must start before RX begins, but blocks until then + self.waitfor(1) + self.own(0) + self.waitfor(0) + if n: + # start timer: timer CB sets self.own which causes RX + self.own(1) + self.i2c.send(s, timeout=to) + 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, timeout=to) + self.waitfor(0) + self.own(0) + n = int.from_bytes(sn, 'little') # no of bytes to receive + if n: + self.waitfor(1) # Wait for responder to request send + self.own(1) # Acknowledge + data = self.i2c.recv(n, timeout=to) + self.waitfor(0) + self.own(0) + self._handle_rxd(data) diff --git a/i2c/i2c_esp.py b/i2c/i2c_esp.py new file mode 100644 index 0000000..4d54166 --- /dev/null +++ b/i2c/i2c_esp.py @@ -0,0 +1,63 @@ +# 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) + 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/i2c/i2c_init.py b/i2c/i2c_init.py new file mode 100644 index 0000000..d3fefbc --- /dev/null +++ b/i2c/i2c_init.py @@ -0,0 +1,76 @@ +# 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) + 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 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/i2c/i2c_resp.py b/i2c/i2c_resp.py new file mode 100644 index 0000000..f093949 --- /dev/null +++ b/i2c/i2c_resp.py @@ -0,0 +1,62 @@ +# 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) + 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 From 4ecaa13ca8c659b776224623b6610dabd8d8406f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 15 Oct 2018 12:08:30 +0100 Subject: [PATCH 080/472] I2C library added. --- i2c/README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/i2c/README.md b/i2c/README.md index 242451e..16a42bd 100644 --- a/i2c/README.md +++ b/i2c/README.md @@ -32,12 +32,12 @@ the `Responder` in this event. 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#21-configuration) Fine-tuning the interface. - 4.3 [Responder class](./README.md#431-responder-class) + 4.2.1 [Configuration](./README.md#421-configuration) Fine-tuning the interface. + 4.3 [Responder class](./README.md#43-responder-class) 5. [Limitations](./README.md#5-limitations) - 5.1 [Blocking](./TUTORIAL.md#51-blocking) - 5.2 [Buffering](./TUTORIAL.md#52-buffering) - 5.3 [Responder crash detection](./TUTORIAL.md#53-responder-crash-detection) + 5.1 [Blocking](./README.md#51-blocking) + 5.2 [Buffering](./README.md#52-buffering) + 5.3 [Responder crash detection](./README.md#53-responder-crash-detection) # 1. Files @@ -169,7 +169,7 @@ Constructor args: 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 target with an active high reset might have: +Pyboard or ESP8266 target with an active low reset might have: ```python (machine.Pin('X12'), 0, 200) @@ -188,7 +188,8 @@ Class variables: Class variables should be set before instantiating `Initiator` or `Responder`. See [Section 4.4](./README.md#44-configuration). -Instance variables: +Instance variables: + The `Initiator` maintains instance variables which may be used to measure its peformance. See [Section 4.4](./README.md#44-configuration). From b08304fe516f1d9b3790d3354805974ed2711119 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 19 Oct 2018 08:51:26 +0100 Subject: [PATCH 081/472] I2C: Add flow control. Reduce allocation. --- i2c/README.md | 26 ++++++++++++++----- i2c/asi2c.py | 69 ++++++++++++++++++++++++++++++------------------- i2c/asi2c_i.py | 39 +++++++++++++++++++--------- i2c/i2c_esp.py | 6 +++++ i2c/i2c_init.py | 5 ++++ i2c/i2c_resp.py | 6 +++++ 6 files changed, 106 insertions(+), 45 deletions(-) diff --git a/i2c/README.md b/i2c/README.md index 16a42bd..de56ab5 100644 --- a/i2c/README.md +++ b/i2c/README.md @@ -22,6 +22,11 @@ The `Initiator` implements a timeout enabling it to detect failure of the other end of the interface (the `Responder`). There is optional provision to reset the `Responder` in this event. +## Changes + +V0.15 RAM allocation reduced and flow control implemented. +V0.1 Initial release. + ###### [Main README](../README.md) # Contents @@ -36,7 +41,7 @@ the `Responder` in this event. 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](./README.md#52-buffering) + 5.2 [Buffering and RAM usage](./README.md#52-buffering-and-ram-usage) 5.3 [Responder crash detection](./README.md#53-responder-crash-detection) # 1. Files @@ -184,6 +189,8 @@ Class variables: 1. `timeout=1000` Timeout (in ms) before `Initiator` assumes `Responder` has failed. 2. `t_poll=100` Interval (ms) for `Initiator` polling `Responder`. + 3. `rxbufsize=200` Size of receive buffer. This should exceed the maximum + message length. Class variables should be set before instantiating `Initiator` or `Responder`. See [Section 4.4](./README.md#44-configuration). @@ -230,10 +237,12 @@ Constructor args: `Pin` instances passed to the constructor must be instantiated by `machine`. -Class variable: +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` Size of receive buffer. This should exceed the maximum message + length. ###### [Contents](./README.md#contents) @@ -270,16 +279,19 @@ 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 I/O pins. -## 5.2 Buffering +## 5.2 Buffering and RAM usage -The protocol does not implement flow control, incoming data being buffered -until read. To avoid the risk of memory errors a coroutine should read incoming -data as it arrives. Since this is the normal mode of using such an interface I -see little merit in increasing the complexity of the code with flow control. +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. +Efforts are under way to remove RAM allocation by the `Responder`. This would +enable hard interrupts to be used, further reducing blocking. With this aim +incoming data is buffered in a pre-allocated bytearray. + ## 5.3 Responder crash detection The `Responder` protocol executes in a soft interrupt context. This means that diff --git a/i2c/asi2c.py b/i2c/asi2c.py index 66d2c90..ade4ea4 100644 --- a/i2c/asi2c.py +++ b/i2c/asi2c.py @@ -25,7 +25,7 @@ import uasyncio as asyncio import machine import utime -from micropython import const +from micropython import const, schedule import io _MP_STREAM_POLL_RD = const(1) @@ -39,7 +39,7 @@ # Base class provides user interface and send/receive object buffers class Channel(io.IOBase): - def __init__(self, i2c, own, rem, verbose): + def __init__(self, i2c, own, rem, verbose, rxbufsize): self.verbose = verbose self.synchronised = False # Hardware @@ -49,9 +49,12 @@ def __init__(self, i2c, own, rem, verbose): own.init(mode=machine.Pin.OUT, value=1) rem.init(mode=machine.Pin.IN, pull=machine.Pin.PULL_UP) # I/O - self.txbyt = b'' + self.txbyt = b'' # Data to send + self.txsiz = bytearray(2) # Size of .txbyt encoded as 2 bytes self.rxbyt = b'' - self.last_tx = 0 # Size of last buffer sent + 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') @@ -67,18 +70,14 @@ def waitfor(self, val): # Initiator overrides while not self.rem() == val: pass - # Concatenate incoming bytes instance. + # Get incoming bytes instance from memoryview. def _handle_rxd(self, msg): - self.rxbyt = b''.join((self.rxbyt, msg)) - - # Get bytes if present. Return bytes, len as 2 bytes, len - def _get_tx_data(self): - d = self.txbyt - n = len(d) - return d, n.to_bytes(2, 'little'), n + self.rxbyt = bytes(msg) def _txdone(self): self.txbyt = b'' + self.txsiz[0] = 0 + self.txsiz[1] = 0 # Stream interface @@ -91,7 +90,7 @@ def ioctl(self, req, arg): if self.rxbyt: ret |= _MP_STREAM_POLL_RD if arg & _MP_STREAM_POLL_WR: - if not self.txbyt: + if (not self.txbyt) and self.cantx: ret |= _MP_STREAM_POLL_WR return ret @@ -110,13 +109,23 @@ def read(self, 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: # Waiting for existing data to go out - return 0 - d = buf[off : off + sz] + 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 - return len(d) + self.txsiz[0] = l & 0xff + self.txsiz[1] = l >> 8 + return l return 0 # User interface @@ -136,8 +145,9 @@ def close(self): class Responder(Channel): addr = 0x12 + rxbufsize = 200 def __init__(self, i2c, pin, pinack, verbose=True): - super().__init__(i2c, pinack, pin, verbose) + super().__init__(i2c, pinack, pin, verbose, self.rxbufsize) loop = asyncio.get_event_loop() loop.create_task(self._run()) @@ -147,7 +157,7 @@ async def _run(self): # 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)): + def _handler(self, _, sn=bytearray(2), txnull=bytearray(2)): # tstart = utime.ticks_us() # TEST addr = Responder.addr self.rem.irq(handler = None, trigger = machine.Pin.IRQ_RISING) @@ -156,28 +166,35 @@ def _handler(self, _, sn=bytearray(2)): self.own(1) self.waitfor(0) self.own(0) - n = int.from_bytes(sn, 'little') # no of bytes to receive + n = sn[0] + ((sn[1] & 0x7f) << 8) # no of bytes to receive + self.cantx = not bool(sn[1] & 0x80) # Can Initiator accept a payload? if n: self.waitfor(1) utime.sleep_us(_DELAY) - data = self.i2c.readfrom(addr, n) + 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(data) + schedule(self._handle_rxd, mv) # Postpone allocation - s, nb, n = self._get_tx_data() self.own(1) # Request to send self.waitfor(1) utime.sleep_us(_DELAY) - self.i2c.writeto(addr, nb) + dtx = self.txbyt != b'' and self.cantx # Data to send + siz = self.txsiz if dtx else txnull + if n or self.rxbyt: # test n because .rxbyt may not be populated yet + siz[1] |= 0x80 # Hold off Initiator TX + else: + siz[1] &= 0x7f + self.i2c.writeto(addr, siz) self.own(0) self.waitfor(0) - if n: + if dtx: self.own(1) self.waitfor(1) utime.sleep_us(_DELAY) - self.i2c.writeto(addr, s) + self.i2c.writeto(addr, self.txbyt) self.own(0) self.waitfor(0) self._txdone() # Invalidate source diff --git a/i2c/asi2c_i.py b/i2c/asi2c_i.py index 55a8d4a..2a08735 100644 --- a/i2c/asi2c_i.py +++ b/i2c/asi2c_i.py @@ -27,6 +27,7 @@ import machine import utime import gc +from micropython import schedule from asi2c import Channel # The initiator is an I2C slave. It runs on a Pyboard. I2C uses pyb for slave @@ -36,9 +37,10 @@ class Initiator(Channel): timeout = 1000 # ms Timeout to detect slave down t_poll = 100 # ms between Initiator polling Responder + rxbufsize = 200 def __init__(self, i2c, pin, pinack, reset=None, verbose=True): - super().__init__(i2c, pin, pinack, verbose) + super().__init__(i2c, pin, pinack, verbose, self.rxbufsize) self.reset = reset if reset is not None: reset[0].init(mode=machine.Pin.OUT, value = not(reset[1])) @@ -73,11 +75,12 @@ async def _run(self): self.rxbyt = b'' await self._sync() await asyncio.sleep(1) # Ensure Responder is ready + rxbusy = False while True: gc.collect() try: tstart = utime.ticks_us() - self._sendrx() + rxbusy = self._sendrx(rxbusy) t = utime.ticks_diff(utime.ticks_us(), tstart) except OSError: break @@ -90,18 +93,26 @@ async def _run(self): raise OSError('Responder fail.') # Send payload length (may be 0) then payload (if any) - def _sendrx(self, sn=bytearray(2)): + def _sendrx(self, rxbusy, sn=bytearray(2), txnull=bytearray(2)): to = Initiator.timeout - s, nb, n = self._get_tx_data() - self.own(1) # Triggers interrupt on responder - self.i2c.send(nb, timeout=to) # Must start before RX begins, but blocks until then + siz = self.txsiz if self.cantx else txnull + # rxbusy handles the (unlikely) case where last call received data but + # schedule() has not yet processed it + if self.rxbyt or rxbusy: + 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, timeout=to) # Blocks until RX complete. self.waitfor(1) self.own(0) self.waitfor(0) - if n: - # start timer: timer CB sets self.own which causes RX + if self.txbyt and self.cantx: self.own(1) - self.i2c.send(s, timeout=to) + self.i2c.send(self.txbyt, timeout=to) self.waitfor(1) self.own(0) self.waitfor(0) @@ -112,11 +123,15 @@ def _sendrx(self, sn=bytearray(2)): self.i2c.recv(sn, timeout=to) self.waitfor(0) self.own(0) - n = int.from_bytes(sn, 'little') # no of bytes to receive + n = sn[0] + ((sn[1] & 0x7f) << 8) # no of bytes to receive + self.cantx = not bool(sn[1] & 0x80) if n: self.waitfor(1) # Wait for responder to request send self.own(1) # Acknowledge - data = self.i2c.recv(n, timeout=to) + mv = memoryview(self.rx_mv[0 : n]) + self.i2c.recv(mv, timeout=to) self.waitfor(0) self.own(0) - self._handle_rxd(data) + schedule(self._handle_rxd, mv) # Postpone allocation + return True + return False diff --git a/i2c/i2c_esp.py b/i2c/i2c_esp.py index 4d54166..881dfb9 100644 --- a/i2c/i2c_esp.py +++ b/i2c/i2c_esp.py @@ -42,6 +42,12 @@ 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)) diff --git a/i2c/i2c_init.py b/i2c/i2c_init.py index d3fefbc..04f177d 100644 --- a/i2c/i2c_init.py +++ b/i2c/i2c_init.py @@ -44,6 +44,10 @@ 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)) @@ -63,6 +67,7 @@ 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))) diff --git a/i2c/i2c_resp.py b/i2c/i2c_resp.py index f093949..645c79e 100644 --- a/i2c/i2c_resp.py +++ b/i2c/i2c_resp.py @@ -41,6 +41,12 @@ 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)) From 78340d7dd24f98c07a2dc3626b5398abcbe2730b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 22 Oct 2018 10:39:13 +0100 Subject: [PATCH 082/472] I2C version 0.16 --- FASTPOLL.md | 16 ++--- i2c/README.md | 157 ++++++++++++++++++++++++++++++++++++------------ i2c/asi2c.py | 13 ++-- i2c/asi2c_i.py | 32 ++++------ i2c/i2c_init.py | 2 +- 5 files changed, 146 insertions(+), 74 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 25cc738..d9ff4c8 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -126,11 +126,11 @@ 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. -It 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. +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 @@ -404,8 +404,8 @@ 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 become overdue by -more than 1s. +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 @@ -473,7 +473,7 @@ version. The benchmark `rate_fastio` is identical except it instantiates an I/O queue and a low priority queue. Results were as follows. | Script | Uasyncio version | Period (100 coros) | Overhead | -| --- | --- | --- | +|:------:|:----------------:|:------------------:|:--------:| | rate | Official V2 | 156μs | 0% | | rate | fast_io | 162μs | 3.4% | | rate_fastio | fast_io | 206μs | 32% | diff --git a/i2c/README.md b/i2c/README.md index de56ab5..a7dc135 100644 --- a/i2c/README.md +++ b/i2c/README.md @@ -24,7 +24,9 @@ the `Responder` in this event. ## Changes -V0.15 RAM allocation reduced and flow control implemented. +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) @@ -43,6 +45,8 @@ V0.1 Initial release. 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#5-hacker-notes) For anyone wanting to hack on + the code. # 1. Files @@ -97,11 +101,11 @@ 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 and may be as low as 100ms. +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 performance of this interface is largely -unaffected. +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` @@ -115,39 +119,89 @@ used. # 4. API -The following is a typical `Initiator` usage example where the two participants -exchange Python objects serialised using `ujson`: +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 -import ujson +import asi2c_i -i2c = I2C(1, mode=I2C.SLAVE) # Soft I2C may be used -syn = Pin('X11') # Pins are arbitrary but must be declared -ack = Pin('Y8') # using machine -rst = (Pin('X12'), 0, 200) # Responder reset is low for 200ms -chan = asi2c.Initiator(i2c, syn, ack, rst) +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', ujson.loads(res)) + print('Received', int(res)) async def sender(): swriter = asyncio.StreamWriter(chan, {}) - txdata = [0, 0] + n = 0 while True: - await swriter.awrite(''.join((ujson.dumps(txdata), '\n'))) - txdata[0] += 1 + 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 ``` -Code for `Responder` is very similar. See `i2c_init.py` and `i2c_resp.py` for -complete examples. +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) @@ -186,10 +240,8 @@ If the `Initiator` has no `reset` tuple and the `Responder` times out, an `Pin` instances passed to the constructor must be instantiated by `machine`. Class variables: - 1. `timeout=1000` Timeout (in ms) before `Initiator` assumes `Responder` has - failed. - 2. `t_poll=100` Interval (ms) for `Initiator` polling `Responder`. - 3. `rxbufsize=200` Size of receive buffer. This should exceed the maximum + 1. `t_poll=100` Interval (ms) for `Initiator` polling `Responder`. + 2. `rxbufsize=200` Size of receive buffer. This should exceed the maximum message length. Class variables should be set before instantiating `Initiator` or `Responder`. @@ -208,10 +260,6 @@ Coroutine: The `Initiator` class variables determine the behaviour of the interface. Where these are altered, it should be done before instantiation. -`Initiator.timeout` If the `Responder` fails the `Initiator` times out and -resets the `Responder`; this occurs if `reset` tuple with a pin is supplied. -Otherwise the `Initiator` raises an `OSError`. - `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. @@ -225,6 +273,8 @@ variables may be read: 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. + ###### [Contents](./README.md#contents) ## 4.3 Responder class @@ -242,7 +292,7 @@ Class variables: `Initiator` or `Responder`. If the default address (0x12) is to be overriden, `Initiator` application code must instantiate the I2C accordingly. 2. `rxbufsize` Size of receive buffer. This should exceed the maximum message - length. + length. Consider reducing this in ESP8266 applications to save RAM. ###### [Contents](./README.md#contents) @@ -270,15 +320,6 @@ 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. -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. This is 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 I/O pins. - ## 5.2 Buffering and RAM usage The protocol implements flow control: the `StreamWriter` at one end of the link @@ -288,9 +329,9 @@ will pause until the last string transmitted has been read by the corresponding Outgoing data is unbuffered. `StreamWriter.awrite` will pause until pending data has been transmitted. -Efforts are under way to remove RAM allocation by the `Responder`. This would -enable hard interrupts to be used, further reducing blocking. With this aim -incoming data is buffered in a pre-allocated bytearray. +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 @@ -300,4 +341,40 @@ 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. + diff --git a/i2c/asi2c.py b/i2c/asi2c.py index ade4ea4..a9c2df8 100644 --- a/i2c/asi2c.py +++ b/i2c/asi2c.py @@ -25,7 +25,7 @@ import uasyncio as asyncio import machine import utime -from micropython import const, schedule +from micropython import const import io _MP_STREAM_POLL_RD = const(1) @@ -40,6 +40,7 @@ # 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 @@ -158,7 +159,6 @@ async def _run(self): # 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)): -# tstart = utime.ticks_us() # TEST addr = Responder.addr self.rem.irq(handler = None, trigger = machine.Pin.IRQ_RISING) utime.sleep_us(_DELAY) # Ensure Initiator has set up to write. @@ -167,6 +167,8 @@ def _handler(self, _, sn=bytearray(2), txnull=bytearray(2)): 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) @@ -176,18 +178,18 @@ def _handler(self, _, sn=bytearray(2), txnull=bytearray(2)): self.own(1) self.waitfor(0) self.own(0) - schedule(self._handle_rxd, mv) # Postpone allocation + 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 n or self.rxbyt: # test n because .rxbyt may not be populated yet + if self.rxbyt: siz[1] |= 0x80 # Hold off Initiator TX else: siz[1] &= 0x7f - self.i2c.writeto(addr, siz) + self.i2c.writeto(addr, siz) # Was getting ENODEV occasionally on Pyboard self.own(0) self.waitfor(0) if dtx: @@ -199,4 +201,3 @@ def _handler(self, _, sn=bytearray(2), txnull=bytearray(2)): self.waitfor(0) self._txdone() # Invalidate source self.rem.irq(handler = self._handler, trigger = machine.Pin.IRQ_RISING) -# print('Time: ', utime.ticks_diff(utime.ticks_us(), tstart)) diff --git a/i2c/asi2c_i.py b/i2c/asi2c_i.py index 2a08735..c0773b8 100644 --- a/i2c/asi2c_i.py +++ b/i2c/asi2c_i.py @@ -27,7 +27,6 @@ import machine import utime import gc -from micropython import schedule from asi2c import Channel # The initiator is an I2C slave. It runs on a Pyboard. I2C uses pyb for slave @@ -35,7 +34,6 @@ # reset (if provided) is a means of resetting Responder in case of error: it # is (pin, active_level, ms) class Initiator(Channel): - timeout = 1000 # ms Timeout to detect slave down t_poll = 100 # ms between Initiator polling Responder rxbufsize = 200 @@ -52,11 +50,10 @@ def __init__(self, i2c, pin, pinack, reset=None, verbose=True): loop = asyncio.get_event_loop() loop.create_task(self._run()) - def waitfor(self, val): - to = Initiator.timeout + 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) > to: + if utime.ticks_diff(utime.ticks_ms(), tim) > 1000: raise OSError async def reboot(self): @@ -75,12 +72,11 @@ async def _run(self): self.rxbyt = b'' await self._sync() await asyncio.sleep(1) # Ensure Responder is ready - rxbusy = False while True: gc.collect() try: tstart = utime.ticks_us() - rxbusy = self._sendrx(rxbusy) + self._sendrx() t = utime.ticks_diff(utime.ticks_us(), tstart) except OSError: break @@ -93,12 +89,9 @@ async def _run(self): raise OSError('Responder fail.') # Send payload length (may be 0) then payload (if any) - def _sendrx(self, rxbusy, sn=bytearray(2), txnull=bytearray(2)): - to = Initiator.timeout + def _sendrx(self, sn=bytearray(2), txnull=bytearray(2)): siz = self.txsiz if self.cantx else txnull - # rxbusy handles the (unlikely) case where last call received data but - # schedule() has not yet processed it - if self.rxbyt or rxbusy: + if self.rxbyt: siz[1] |= 0x80 # Hold off further received data else: siz[1] &= 0x7f @@ -106,13 +99,13 @@ def _sendrx(self, rxbusy, sn=bytearray(2), txnull=bytearray(2)): # 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, timeout=to) # Blocks until RX complete. + 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, timeout=to) + self.i2c.send(self.txbyt) self.waitfor(1) self.own(0) self.waitfor(0) @@ -120,18 +113,19 @@ def _sendrx(self, rxbusy, sn=bytearray(2), txnull=bytearray(2)): # Send complete self.waitfor(1) # Wait for responder to request send self.own(1) # Acknowledge - self.i2c.recv(sn, timeout=to) + 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, timeout=to) + self.i2c.recv(mv) self.waitfor(0) self.own(0) - schedule(self._handle_rxd, mv) # Postpone allocation - return True - return False + self._handle_rxd(mv) diff --git a/i2c/i2c_init.py b/i2c/i2c_init.py index 04f177d..12f24d8 100644 --- a/i2c/i2c_init.py +++ b/i2c/i2c_init.py @@ -40,7 +40,7 @@ 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) +chan = asi2c_i.Initiator(i2c, syn, ack, rst) async def receiver(): sreader = asyncio.StreamReader(chan) From 3af09c3f0ff87e76c371d2553c17c7e2b56c885f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 22 Oct 2018 10:44:51 +0100 Subject: [PATCH 083/472] I2C version 0.16 --- i2c/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i2c/README.md b/i2c/README.md index a7dc135..d2a9344 100644 --- a/i2c/README.md +++ b/i2c/README.md @@ -45,7 +45,7 @@ V0.1 Initial release. 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#5-hacker-notes) For anyone wanting to hack on + 6. [Hacker notes](./README.md#6-hacker-notes) For anyone wanting to hack on the code. # 1. Files From 8152d1be0224423ee91d252e0c65550720f3eb10 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 16 Nov 2018 14:00:26 +0000 Subject: [PATCH 084/472] PRIMITIVES.md Add Gather usage example. --- PRIMITIVES.md | 125 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 50 deletions(-) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index 1ebac96..daca0a5 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -12,56 +12,32 @@ obvious workround is to produce a version with unused primitives removed. # 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) - - 3.2 [Class Lock](./PRIMITIVES.md#32-class-lock) - - 3.2.1 [Definition](./PRIMITIVES.md#321-definition) - - 3.3 [Class Event](./PRIMITIVES.md#33-class-event) - - 3.3.1 [Definition](./PRIMITIVES.md#331-definition) - - 3.4 [Class Barrier](./PRIMITIVES.md#34-class-barrier) - - 3.5 [Class Semaphore](./PRIMITIVES.md#35-class-semaphore) - - 3.5.1 [Class BoundedSemaphore](./PRIMITIVES.md#351-class-boundedsemaphore) - - 3.6 [Class Condition](./PRIMITIVES.md#36-class-condition) - - 3.6.1 [Definition](./PRIMITIVES.md#361-definition) - - 3.7 [Class Gather](./PRIMITIVES.md#37-class-gather) - - 3.7.1 [Definition](./PRIMITIVES.md#371-definition) - - 4. [Task Cancellation](./PRIMITIVES.md#4-task-cancellation) - - 4.1 [Coro sleep](./PRIMITIVES.md#41-coro-sleep) - - 4.2 [Class Cancellable](./PRIMITIVES.md#42-class-cancellable) - - 4.2.1 [Groups](./PRIMITIVES.md#421-groups) - - 4.2.2 [Custom cleanup](./PRIMITIVES.md#422-custom-cleanup) - - 4.3 [Class NamedTask](./PRIMITIVES.md#43-class-namedtask) - - 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. [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) + 3.2 [Class Lock](./PRIMITIVES.md#32-class-lock) + 3.2.1 [Definition](./PRIMITIVES.md#321-definition) + 3.3 [Class Event](./PRIMITIVES.md#33-class-event) + 3.3.1 [Definition](./PRIMITIVES.md#331-definition) + 3.4 [Class Barrier](./PRIMITIVES.md#34-class-barrier) + 3.5 [Class Semaphore](./PRIMITIVES.md#35-class-semaphore) + 3.5.1 [Class BoundedSemaphore](./PRIMITIVES.md#351-class-boundedsemaphore) + 3.6 [Class Condition](./PRIMITIVES.md#36-class-condition) + 3.6.1 [Definition](./PRIMITIVES.md#361-definition) + 3.7 [Class Gather](./PRIMITIVES.md#37-class-gather) + 3.7.1 [Definition](./PRIMITIVES.md#371-definition) + 3.7.2 [Use with timeouts and cancellation](./PRIMITIVES.md#372-use-with-timeouts-and-cancellation) + 4. [Task Cancellation](./PRIMITIVES.md#4-task-cancellation) + 4.1 [Coro sleep](./PRIMITIVES.md#41-coro-sleep) + 4.2 [Class Cancellable](./PRIMITIVES.md#42-class-cancellable) + 4.2.1 [Groups](./PRIMITIVES.md#421-groups) + 4.2.2 [Custom cleanup](./PRIMITIVES.md#422-custom-cleanup) + 4.3 [Class NamedTask](./PRIMITIVES.md#43-class-namedtask) + 4.3.1 [Latency and Barrier objects](./PRIMITIVES.md#431-latency-and-barrier-objects) + 4.3.2 [Custom cleanup](./PRIMITIVES.md#432-custom-cleanup) 4.3.3 [Changes](./PRIMITIVES.md#433-changes) ## 1.1 Synchronisation Primitives @@ -377,6 +353,10 @@ 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 @@ -411,6 +391,51 @@ 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 foo(n): + while True: + try: + await asyncio.sleep(1) + n += 1 + except asyncio.TimeoutError: + print('foo timeout') + return n + +@asyn.cancellable +async def bar(n): + while True: + try: + await asyncio.sleep(1) + n += 1 + except asyn.StopTask: + print('bar stopped') + return n + +async def do_cancel(): + await asyncio.sleep(5) + await asyn.Cancellable.cancel_all() + + +async def main(loop): + bar_task = asyn.Cancellable(bar, 70) + gatherables = [asyn.Gatherable(bar_task),] + gatherables.append(asyn.Gatherable(foo, 10, timeout=7)) + loop.create_task(do_cancel()) + res = await asyn.Gather(gatherables) + print('Result: ', res) + +loop = asyncio.get_event_loop() +loop.run_until_complete(main(loop)) +``` + ###### [Contents](./PRIMITIVES.md#contents) # 4. Task Cancellation From a73c3871b3f3d463dcbde5f3c02411da8b6a0e07 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 16 Nov 2018 14:03:36 +0000 Subject: [PATCH 085/472] PRIMITIVES.md Add Gather usage example. --- PRIMITIVES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index daca0a5..20fb6c6 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -16,7 +16,7 @@ obvious workround is to produce a version with unused primitives removed. 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. [Synchronisation Primitives](./PRIMITIVES.md#3-synchronisation-primitives) 3.1 [Function launch](./PRIMITIVES.md#31-function-launch) 3.2 [Class Lock](./PRIMITIVES.md#32-class-lock) 3.2.1 [Definition](./PRIMITIVES.md#321-definition) From e6208b422bf38c532bfa30208dcc6f50e1138f65 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 17 Nov 2018 10:57:37 +0000 Subject: [PATCH 086/472] Minor doc improvements. --- PRIMITIVES.md | 41 +++++++++++++++++++++++++---------------- TUTORIAL.md | 9 +++++---- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index 20fb6c6..67ff768 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -400,37 +400,46 @@ are subject to cancellation or timeout. 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): - while True: - try: + print('Start timeout coro foo()') + try: + while True: await asyncio.sleep(1) n += 1 - except asyncio.TimeoutError: - print('foo timeout') - return n + except asyncio.TimeoutError: + print('foo timeout.') + return n @asyn.cancellable async def bar(n): - while True: - try: + print('Start cancellable bar()') + try: + while True: await asyncio.sleep(1) n += 1 - except asyn.StopTask: - print('bar stopped') - return n + except asyn.StopTask: + print('bar stopped.') + return n async def do_cancel(): - await asyncio.sleep(5) + await asyncio.sleep(5.5) await asyn.Cancellable.cancel_all() - async def main(loop): - bar_task = asyn.Cancellable(bar, 70) - gatherables = [asyn.Gatherable(bar_task),] - gatherables.append(asyn.Gatherable(foo, 10, timeout=7)) + 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) + print('Result: ', res) # Expect [42, 17, 75] loop = asyncio.get_event_loop() loop.run_until_complete(main(loop)) diff --git a/TUTORIAL.md b/TUTORIAL.md index bd33b57..746c813 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -608,15 +608,16 @@ controlled. Documentation of this is in the code. ## 3.6 Task cancellation -`uasyncio` now provides a `cancel(coro)` function. This works by throwing an +`uasyncio` provides 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. However there is a limitation. If a coro issues `await uasyncio.sleep(secs)` or `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` has -no mechanism for verifying when cancellation has actually occurred. The `asyn` -library provides verification via the following classes: +Other potential sources of latency take the form of slow code. + +`uasyncio` lacks a mechanism for verifying when cancellation has actually +occurred. The `asyn` library provides verification via the following classes: 1. `Cancellable` This allows one or more tasks to be assigned to a group. A coro can cancel all tasks in the group, pausing until this has been achieved. From 5f760c9b624c692bf97d98470b54295b720dc1c0 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 17 Nov 2018 11:11:55 +0000 Subject: [PATCH 087/472] Minor doc improvements. --- PRIMITIVES.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index 67ff768..c978a47 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -562,8 +562,8 @@ 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 `StopTask` exception is an alias for `usayncio.CancelledError`. In my view -the name is more descriptive of its function. +The `asyn.StopTask` exception is an alias for `usayncio.CancelledError`. In my +view the name is more descriptive of its function. ### 4.2.1 Groups @@ -579,12 +579,12 @@ A task created with the `cancellable` decorator can intercept the `StopTask` exception to perform custom cleanup operations. This may be done as below: ```python -@cancellable +@asyn.cancellable async def foo(): while True: try: await sleep(1) # Main body of task - except StopTask: + except asyn.StopTask: # perform custom cleanup return # Respond by quitting ``` @@ -593,11 +593,11 @@ The following example returns `True` if it ends normally or `False` if cancelled. ```python -@cancellable +@asyn.cancellable async def bar(): try: await sleep(1) # Main body of task - except StopTask: + except asyn.StopTask: return False else: return True @@ -697,11 +697,11 @@ if necessary. This might be done for cleanup or to return a 'cancelled' status. The coro should have the following form: ```python -@cancellable +@asyn.cancellable async def foo(): try: await asyncio.sleep(1) # User code here - except StopTask: + except asyn.StopTask: return False # Cleanup code else: return True # Normal exit @@ -716,7 +716,7 @@ latest API changes are: * `NamedTask.cancel()` is now asynchronous. * `NamedTask` and `Cancellable` coros no longer receive a `TaskId` instance as their 1st arg. - * `@namedtask` still works but is now an alias for `@cancellable`. + * `@asyn.namedtask` still works but is now an alias for `@asyn.cancellable`. The drive to simplify code comes from the fact that `uasyncio` is itself under development. Tracking changes is an inevitable headache. From 9849e3ec6eb0abf17b4ff501abbb9330df03aa6f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 18 Nov 2018 10:36:33 +0000 Subject: [PATCH 088/472] PRIMITIVES.md add cancellation examples. --- PRIMITIVES.md | 61 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index c978a47..8e1b535 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -500,7 +500,7 @@ async def comms(): # Perform some communications task try: await do_communications() # Launches Cancellable tasks except CommsError: - await Cancellable.cancel_all() + 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. ``` @@ -508,7 +508,7 @@ async def comms(): # Perform some communications task A `Cancellable` task is declared with the `@cancellable` decorator: ```python -@cancellable +@asyn.cancellable async def print_nums(num): while True: print(num) @@ -521,16 +521,16 @@ 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 Cancellable(print_nums, 5) # single arg to print_nums. +await asyn.Cancellable(print_nums, 5) # single arg to print_nums. loop = asyncio.get_event_loop() -loop.create_task(Cancellable(print_nums, 42)()) # Note () syntax. +loop.create_task(asyn.Cancellable(print_nums, 42)()) # Note () syntax. ``` The following will cancel any tasks still running, pausing until cancellation is complete: ```python -await Cancellable.cancel_all() +await asyn.Cancellable.cancel_all() ``` Constructor mandatory args: @@ -565,6 +565,29 @@ Public bound method: 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 @@ -577,7 +600,6 @@ with `cancel_all` cancelling all `Cancellable` tasks. 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(): @@ -588,10 +610,8 @@ async def foo(): # 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(): @@ -602,6 +622,31 @@ async def bar(): 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) From 781ffc6cd462040f88763d03a9053cdcf4801717 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 18 Nov 2018 10:54:02 +0000 Subject: [PATCH 089/472] PRIMITIVES.md various improvements. --- PRIMITIVES.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index 8e1b535..22bf131 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -17,28 +17,28 @@ obvious workround is to produce a version with unused primitives removed. 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) - 3.2 [Class Lock](./PRIMITIVES.md#32-class-lock) + 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) + 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) - 3.5 [Class Semaphore](./PRIMITIVES.md#35-class-semaphore) + 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) + 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) + 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) - 4. [Task Cancellation](./PRIMITIVES.md#4-task-cancellation) - 4.1 [Coro sleep](./PRIMITIVES.md#41-coro-sleep) - 4.2 [Class Cancellable](./PRIMITIVES.md#42-class-cancellable) - 4.2.1 [Groups](./PRIMITIVES.md#421-groups) + 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) + 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) - 4.3.3 [Changes](./PRIMITIVES.md#433-changes) + 4.3.3 [Changes](./PRIMITIVES.md#433-changes) June 2018 asyn API changes affecting cancellation. ## 1.1 Synchronisation Primitives @@ -667,7 +667,7 @@ async def foo(arg1, arg2): ``` The `NamedTask` constructor takes the name, the coro, plus any user positional -or keyword args. Th eresultant instance can be scheduled in the usual ways: +or keyword args. The resultant instance can be scheduled in the usual ways: ```python await NamedTask('my foo', foo, 1, 2) # Pause until complete or killed From b1e592ce7c4f9d9cecf9a6ecb6dd10296b23912c Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 18 Nov 2018 10:55:22 +0000 Subject: [PATCH 090/472] PRIMITIVES.md various improvements. --- PRIMITIVES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index 22bf131..fcadc2b 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -17,7 +17,7 @@ obvious workround is to produce a version with unused primitives removed. 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.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. @@ -35,7 +35,7 @@ obvious workround is to produce a version with unused primitives removed. 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 [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) 4.3.3 [Changes](./PRIMITIVES.md#433-changes) June 2018 asyn API changes affecting cancellation. From fa86ab46a3343b9ae46fafb25e6916a067c1e723 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 18 Nov 2018 10:57:45 +0000 Subject: [PATCH 091/472] PRIMITIVES.md various improvements. --- PRIMITIVES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index fcadc2b..122e1c7 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -29,7 +29,7 @@ obvious workround is to produce a version with unused primitives removed. 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) + 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. From 26e49b5316b86d1d64b0863a434cce12233d0fa2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 24 Nov 2018 17:01:07 +0000 Subject: [PATCH 092/472] asyn.py Event now has optional delay_ms arg. --- PRIMITIVES.md | 19 +++++++++++-------- TUTORIAL.md | 3 ++- asyn.py | 5 +++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index 122e1c7..f57c8fb 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -127,15 +127,18 @@ async def bar(lock): 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 has a bug in its implementation of asynchronous context -managers: 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). +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 facilitate this at the expense of latency. +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. @@ -190,10 +193,10 @@ Example of this are in `event_test` and `ack_test` in asyntest.py. ### 3.3.1 Definition -Constructor: takes one optional boolean argument, defaulting False. - * `lp` If `True` and the experimental low priority core.py is installed, - low priority scheduling will be used while awaiting the event. If the standard - version of uasyncio is installed the arg will have no effect. +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, diff --git a/TUTORIAL.md b/TUTORIAL.md index 746c813..3b4fe99 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -883,7 +883,8 @@ value returned by `__aenter__`. There was a bug in the implementation whereby if an explicit `return` was issued within an `async with` block, the `__aexit__` method was not called. This was -fixed as of 27th June 2018 [PR 3890](https://github.com/micropython/micropython/pull/3890). +fixed as of 27th June 2018 [PR 3890](https://github.com/micropython/micropython/pull/3890) +but the fix was too late for the current release build (V1.9.4). ###### [Contents](./TUTORIAL.md#contents) diff --git a/asyn.py b/asyn.py index df7762d..97493d0 100644 --- a/asyn.py +++ b/asyn.py @@ -96,7 +96,8 @@ def release(self): # When all waiting coros have run # event.clear() should be issued class Event(): - def __init__(self, lp=False): # Redundant arg retained for compatibility + def __init__(self, delay_ms=0): + self.delay_ms = delay_ms self.clear() def clear(self): @@ -105,7 +106,7 @@ def clear(self): def __await__(self): while not self._flag: - yield + await asyncio.sleep_ms(self.delay_ms) __iter__ = __await__ From b0dcc78cc71c9e719021e2e28022b5e8b95304c5 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 28 Nov 2018 11:57:30 +0000 Subject: [PATCH 093/472] Implement Pusbutton lpmode. --- DRIVERS.md | 17 +++++++++++++++-- astests.py | 5 +++-- aswitch.py | 14 +++++++++++--- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/DRIVERS.md b/DRIVERS.md index e6c42f6..c1f6053 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -101,9 +101,10 @@ implementation. click or long press events; where the **function** is a coroutine it will be scheduled for execution and will run asynchronously. -Constructor argument (mandatory): +Constructor arguments: - 1. `pin` The initialised Pin instance. + 1. `pin` Mandatory. The initialised Pin instance. + 2. `lpmode` Default `False`. See below. Methods: @@ -147,6 +148,18 @@ loop = asyncio.get_event_loop() loop.run_until_complete(my_app()) # Run main application code ``` +The `lpmode` constructor argument modifies the behaviour of `press_func` in the +event that a `long_func` is specified. + +If `lpmode` is `False`, if a button press occurs `press_func` runs immediately; +`long_func` runs if the button is still pressed when the timeout has elapsed. +If `lpmode` is `True`, `press_func` is delayed until button release. If, at the +time of release, `long_func` has run, `press_func` will be suppressed. + +The default provides for low latency but a long button press will trigger +`press_func` and `long_func`. `lpmode` = `True` prevents `press_func` from +running. + ## 3.3 Delay_ms class This implements the software equivalent of a retriggerable monostable or a diff --git a/astests.py b/astests.py index 6846061..80838ec 100644 --- a/astests.py +++ b/astests.py @@ -60,7 +60,8 @@ def test_swcb(): loop.run_until_complete(killer()) # Test for the Pushbutton class (coroutines) -def test_btn(): +# Pass True to test lpmode +def test_btn(lpmode=False): print('Test of pushbutton scheduling coroutines.') print(helptext) pin = Pin('X1', Pin.IN, Pin.PULL_UP) @@ -68,7 +69,7 @@ def test_btn(): green = LED(2) yellow = LED(3) blue = LED(4) - pb = Pushbutton(pin) + pb = Pushbutton(pin, lpmode) pb.press_func(pulse, (red, 1000)) pb.release_func(pulse, (green, 1000)) pb.double_func(pulse, (yellow, 1000)) diff --git a/aswitch.py b/aswitch.py index 94cc059..3780a66 100644 --- a/aswitch.py +++ b/aswitch.py @@ -118,8 +118,10 @@ class Pushbutton(object): debounce_ms = 50 long_press_ms = 1000 double_click_ms = 400 - def __init__(self, pin): + def __init__(self, pin, lpmode=False): self.pin = pin # Initialise for input + self._lpmode = lpmode + self._press_pend = False self._true_func = False self._false_func = False self._double_func = False @@ -176,13 +178,19 @@ async def buttoncheck(self): # First click: start doubleclick timer doubledelay.trigger(Pushbutton.double_click_ms) if self._true_func: - launch(self._true_func, self._true_args) + if self._long_func and self._lpmode: + self._press_pend = True # Delay launch until release + else: + launch(self._true_func, self._true_args) else: # Button release - if self._long_func and longdelay.running(): + if longdelay.running(): # Avoid interpreting a second click as a long push longdelay.stop() + if self._press_pend: + launch(self._true_func, self._true_args) if self._false_func: launch(self._false_func, self._false_args) + self._press_pend = False # Ignore state changes until switch has settled await asyncio.sleep_ms(Pushbutton.debounce_ms) From 0d8312a28f8ee61596536c2947b8429e1c3d3372 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 29 Nov 2018 10:00:51 +0000 Subject: [PATCH 094/472] Pushbutton: suppress release on long and double clicks. --- DRIVERS.md | 51 +++++++++++++++++++++--------- astests.py | 14 +++++---- aswitch.py | 92 ++++++++++++++++++++++++++++++++---------------------- 3 files changed, 99 insertions(+), 58 deletions(-) diff --git a/DRIVERS.md b/DRIVERS.md index c1f6053..ab75e5a 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -16,8 +16,7 @@ events. 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. `asyn.py` Provides synchronisation primitives. Required by `aswitch.py`. - 4. `astests.py` Test/demonstration programs for `aswitch.py`. + 3. `astests.py` Test/demonstration programs for `aswitch.py`. # 3. Module aswitch.py @@ -104,7 +103,7 @@ scheduled for execution and will run asynchronously. Constructor arguments: 1. `pin` Mandatory. The initialised Pin instance. - 2. `lpmode` Default `False`. See below. + 2. `suppress` Default `False`. See 3.2.1 below. Methods: @@ -148,17 +147,28 @@ loop = asyncio.get_event_loop() loop.run_until_complete(my_app()) # Run main application code ``` -The `lpmode` constructor argument modifies the behaviour of `press_func` in the -event that a `long_func` is specified. - -If `lpmode` is `False`, if a button press occurs `press_func` runs immediately; -`long_func` runs if the button is still pressed when the timeout has elapsed. -If `lpmode` is `True`, `press_func` is delayed until button release. If, at the -time of release, `long_func` has run, `press_func` will be suppressed. - -The default provides for low latency but a long button press will trigger -`press_func` and `long_func`. `lpmode` = `True` prevents `press_func` from -running. +### 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 @@ -192,6 +202,7 @@ Methods: 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. 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 @@ -236,11 +247,18 @@ of its behaviour if the switch is toggled rapidly. Demonstrates the use of callbacks to toggle the red and green LED's. -## 4.3 Function test_btn() +## 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() @@ -268,3 +286,6 @@ 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/astests.py b/astests.py index 80838ec..5abbc46 100644 --- a/astests.py +++ b/astests.py @@ -2,7 +2,7 @@ # Tested on Pyboard but should run on other microcontroller platforms # running MicroPython with uasyncio library. # Author: Peter Hinch. -# Copyright Peter Hinch 2017 Released under the MIT license. +# Copyright Peter Hinch 2017-2018 Released under the MIT license. from machine import Pin from pyb import LED @@ -60,8 +60,8 @@ def test_swcb(): loop.run_until_complete(killer()) # Test for the Pushbutton class (coroutines) -# Pass True to test lpmode -def test_btn(lpmode=False): +# Pass True to test suppress +def test_btn(suppress=False, lf=True, df=True): print('Test of pushbutton scheduling coroutines.') print(helptext) pin = Pin('X1', Pin.IN, Pin.PULL_UP) @@ -69,11 +69,13 @@ def test_btn(lpmode=False): green = LED(2) yellow = LED(3) blue = LED(4) - pb = Pushbutton(pin, lpmode) + pb = Pushbutton(pin, suppress) pb.press_func(pulse, (red, 1000)) pb.release_func(pulse, (green, 1000)) - pb.double_func(pulse, (yellow, 1000)) - pb.long_func(pulse, (blue, 1000)) + if df: + pb.double_func(pulse, (yellow, 1000)) + if lf: + pb.long_func(pulse, (blue, 1000)) loop = asyncio.get_event_loop() loop.run_until_complete(killer()) diff --git a/aswitch.py b/aswitch.py index 3780a66..4315e35 100644 --- a/aswitch.py +++ b/aswitch.py @@ -67,6 +67,8 @@ def trigger(self, duration=0): # Update end time def running(self): return self.tstop is not None + __call__ = running + async def killer(self): twait = time.ticks_diff(self.tstop, time.ticks_ms()) while twait > 0: # Must loop here: might be retriggered @@ -118,34 +120,37 @@ class Pushbutton(object): debounce_ms = 50 long_press_ms = 1000 double_click_ms = 400 - def __init__(self, pin, lpmode=False): + def __init__(self, pin, suppress=False): self.pin = pin # Initialise for input - self._lpmode = lpmode - self._press_pend = False - self._true_func = False - self._false_func = False - self._double_func = False - self._long_func = False + 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.buttonstate = self.rawstate() # Initial state loop = asyncio.get_event_loop() loop.create_task(self.buttoncheck()) # Thread runs forever def press_func(self, func, args=()): - self._true_func = func - self._true_args = args + self._tf = func + self._ta = args def release_func(self, func, args=()): - self._false_func = func - self._false_args = args + self._ff = func + self._fa = args def double_func(self, func, args=()): - self._double_func = func - self._double_args = args + self._df = func + self._da = args def long_func(self, func, args=()): - self._long_func = func - self._long_args = args + self._lf = func + self._la = args # Current non-debounced logical button state: True == pressed def rawstate(self): @@ -155,12 +160,22 @@ def rawstate(self): def __call__(self): return self.buttonstate + def _ddto(self): # Doubleclick timeout: no doubleclick occurred + self._dblpend = False + if self._supp: + if not self._ld or (self._ld and not self._ld()): + launch(self._ff, self._fa) + + def _ldip(self): # True if a long delay exists and is running + d = self._ld + return d and d() + async def buttoncheck(self): loop = asyncio.get_event_loop() - if self._long_func: - longdelay = Delay_ms(self._long_func, self._long_args) - if self._double_func: - doubledelay = Delay_ms() + if self._lf: + 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. @@ -168,29 +183,32 @@ async def buttoncheck(self): self.buttonstate = state if state: # Button is pressed - if self._long_func and not longdelay.running(): + if self._lf: # Start long press delay - longdelay.trigger(Pushbutton.long_press_ms) - if self._double_func: - if doubledelay.running(): - launch(self._double_func, self._double_args) + self._ld.trigger(Pushbutton.long_press_ms) + if self._df: + if self._dd(): + 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 - doubledelay.trigger(Pushbutton.double_click_ms) - if self._true_func: - if self._long_func and self._lpmode: - self._press_pend = True # Delay launch until release - else: - launch(self._true_func, self._true_args) + self._dd.trigger(Pushbutton.double_click_ms) + self._dblpend = True # Prevent suppressed launch on release + if self._tf: + launch(self._tf, self._ta) else: # Button release - if longdelay.running(): + if self._ff: + if self._supp: + if self._ldip() and not self._dblpend and not self._dblran: + launch(self._ff, self._fa) + else: + launch(self._ff, self._fa) + if self._ldip(): # Avoid interpreting a second click as a long push - longdelay.stop() - if self._press_pend: - launch(self._true_func, self._true_args) - if self._false_func: - launch(self._false_func, self._false_args) - self._press_pend = False + self._ld.stop() + self._dblran = False # Ignore state changes until switch has settled await asyncio.sleep_ms(Pushbutton.debounce_ms) From a685b5adb5f42c5f3e99ab7adb5fecd7f67ab360 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 29 Nov 2018 13:39:00 +0000 Subject: [PATCH 095/472] Fix bug where no long press func provided. --- astests.py | 2 ++ aswitch.py | 44 +++++++++++++++++++------------------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/astests.py b/astests.py index 5abbc46..db87cc9 100644 --- a/astests.py +++ b/astests.py @@ -73,8 +73,10 @@ def test_btn(suppress=False, lf=True, df=True): 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()) diff --git a/aswitch.py b/aswitch.py index 4315e35..066eda4 100644 --- a/aswitch.py +++ b/aswitch.py @@ -132,7 +132,7 @@ def __init__(self, pin, suppress=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.buttonstate = self.rawstate() # Initial state + self.state = self.rawstate() # Initial state loop = asyncio.get_event_loop() loop.create_task(self.buttoncheck()) # Thread runs forever @@ -158,36 +158,31 @@ def rawstate(self): # Current debounced state of button (True == pressed) def __call__(self): - return self.buttonstate + return self.state def _ddto(self): # Doubleclick timeout: no doubleclick occurred self._dblpend = False - if self._supp: + if self._supp and not self.state: if not self._ld or (self._ld and not self._ld()): launch(self._ff, self._fa) - def _ldip(self): # True if a long delay exists and is running - d = self._ld - return d and d() - async def buttoncheck(self): - loop = asyncio.get_event_loop() - if self._lf: + 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.buttonstate: - self.buttonstate = state - if state: - # Button is pressed - if self._lf: - # Start long press delay + 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(): + if self._dd(): # Second click: timer running self._dd.stop() self._dblpend = False self._dblran = True # Prevent suppressed launch on release @@ -196,19 +191,18 @@ async def buttoncheck(self): # First click: start doubleclick timer self._dd.trigger(Pushbutton.double_click_ms) self._dblpend = True # Prevent suppressed launch on release - if self._tf: - launch(self._tf, self._ta) - else: - # Button release + else: # Button release. Is there a release func? if self._ff: if self._supp: - if self._ldip() and not self._dblpend and not self._dblran: - launch(self._ff, self._fa) + 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._ldip(): - # Avoid interpreting a second click as a long push - self._ld.stop() + 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) From 83284957c3bd3951c992238fc5eedf6fc02884c7 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 1 Dec 2018 13:39:50 +0000 Subject: [PATCH 096/472] Restore copyright notice in fast_io code. --- fast_io/__init__.py | 1 + fast_io/core.py | 1 + 2 files changed, 2 insertions(+) diff --git a/fast_io/__init__.py b/fast_io/__init__.py index d588a80..1b40128 100644 --- a/fast_io/__init__.py +++ b/fast_io/__init__.py @@ -1,4 +1,5 @@ # uasyncio.__init__ fast_io +# (c) 2014-2018 Paul Sokolovsky. MIT license. # fork: peterhinch/micropython-lib branch: uasyncio-io-fast-and-rw import uerrno import uselect as select diff --git a/fast_io/core.py b/fast_io/core.py index ed960a2..378383d 100644 --- a/fast_io/core.py +++ b/fast_io/core.py @@ -1,4 +1,5 @@ # uasyncio.core fast_io +# (c) 2014-2018 Paul Sokolovsky. MIT license. # fork: peterhinch/micropython-lib branch: uasyncio-io-fast-and-rw version = 'fast_io' try: From ff37d9b3441e4b10b9b67b4b6ae2b3b9db5fd9a5 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 2 Dec 2018 11:35:54 +0000 Subject: [PATCH 097/472] TUTORIAL.md Add note re socket programming and update 6.7 --- TUTORIAL.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 3b4fe99..5907d7e 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1554,6 +1554,10 @@ application dependent. An alternative approach is to use blocking sockets with `StreamReader` and `StreamWriter` instances to control polling. +[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 offers a solution. + ###### [Contents](./TUTORIAL.md#contents) ## 6.7 Event loop constructor args @@ -1571,7 +1575,7 @@ bar = some_module.Bar() # Constructor calls get_event_loop() loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) ``` -Given that importing a module can run code the only safe way is to instantiate +Given that importing a module can run code the safest way is to instantiate the event loop immediately after importing `uasyncio`. ```python @@ -1581,6 +1585,17 @@ import some_module bar = some_module.Bar() # The get_event_loop() call is now safe ``` +If imported modules do not run `uasyncio` code, another approach is to pass the +loop as an arg to any user code which needs it. Ensure that only the initially +loaded module calls `get_event_loop` e.g. + +```python +import uasyncio as asyncio +import some_module +loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) +bar = some_module.Bar(loop) +``` + Ref [this issue](https://github.com/micropython/micropython-lib/issues/295). ###### [Contents](./TUTORIAL.md#contents) From 320ba4b33ea5c932f5a5c37349d5bb1f3334c71e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 16 Dec 2018 11:17:02 +0000 Subject: [PATCH 098/472] i2c/README Remove references to now non-existent timeout feature. --- i2c/README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/i2c/README.md b/i2c/README.md index d2a9344..c1cef66 100644 --- a/i2c/README.md +++ b/i2c/README.md @@ -18,9 +18,9 @@ supporting I2C slave mode. Consequently at least one end of the interface (known as the`Initiator`) must be a Pyboard. The other end may be any hardware running MicroPython. -The `Initiator` implements a timeout enabling it to detect failure of the other -end of the interface (the `Responder`). There is optional provision to reset -the `Responder` in this event. +`Initiator` user applications may implement a timeout to enable detection of +failure of the other end of the interface (the `Responder`). The `Initiator` +can reset the `Responder` in this event. ## Changes @@ -376,5 +376,4 @@ 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. - +targets. Something I haven't yet achieved (with hard IRQ's). From 11cb3ed4fa3de347fb41f195e650568dd490345f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 16 Dec 2018 18:03:58 +0000 Subject: [PATCH 099/472] DRIVERS.md Add note on timing. --- DRIVERS.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/DRIVERS.md b/DRIVERS.md index ab75e5a..fd612f3 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -36,6 +36,13 @@ 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 @@ -100,6 +107,8 @@ implementation. 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. From 07aae17ce8acdadbdc2bd8091926c3a7f9e322be Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 18 Dec 2018 06:09:27 +0000 Subject: [PATCH 100/472] fast_io: update legalise in source files. --- fast_io/__init__.py | 7 +++++++ fast_io/core.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/fast_io/__init__.py b/fast_io/__init__.py index 1b40128..d2de3e5 100644 --- a/fast_io/__init__.py +++ b/fast_io/__init__.py @@ -1,6 +1,13 @@ # 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 diff --git a/fast_io/core.py b/fast_io/core.py index 378383d..d7b71a5 100644 --- a/fast_io/core.py +++ b/fast_io/core.py @@ -1,6 +1,13 @@ # 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 +# Code at https://github.com/peterhinch/micropython-async.git # fork: peterhinch/micropython-lib branch: uasyncio-io-fast-and-rw + version = 'fast_io' try: import rtc_time as time # Low power timebase using RTC From 7161dade92115a16415ed92404e2ca0bba4b96b8 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 19 Dec 2018 09:15:18 +0000 Subject: [PATCH 101/472] as_i2c_i.py Add optional coro args. --- i2c/README.md | 79 ++++++++++++++++++++++++++++++++++++++------------ i2c/asi2c_i.py | 18 ++++++++++-- 2 files changed, 75 insertions(+), 22 deletions(-) diff --git a/i2c/README.md b/i2c/README.md index c1cef66..ad98235 100644 --- a/i2c/README.md +++ b/i2c/README.md @@ -15,16 +15,20 @@ 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. The other end may be any hardware -running MicroPython. +(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`. -`Initiator` user applications may implement a timeout to enable detection of -failure of the other end of the interface (the `Responder`). The `Initiator` -can reset the `Responder` in this event. +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.16 Minor improvements and bugfixes. Eliminate `timeout` option which caused +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. @@ -40,6 +44,7 @@ V0.1 Initial release. 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) @@ -109,7 +114,7 @@ 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 iterface +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 @@ -219,12 +224,17 @@ Coroutine: ## 4.2 Initiator class -Constructor args: +##### 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 @@ -239,26 +249,26 @@ If the `Initiator` has no `reset` tuple and the `Responder` times out, an `Pin` instances passed to the constructor must be instantiated by `machine`. -Class variables: +##### 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. -Class variables should be set before instantiating `Initiator` or `Responder`. -See [Section 4.4](./README.md#44-configuration). +See [Section 4.2.1](./README.md#421-configuration). -Instance variables: +##### Instance variables: The `Initiator` maintains instance variables which may be used to measure its -peformance. See [Section 4.4](./README.md#44-configuration). +peformance. See [Section 4.2.1](./README.md#421-configuration). -Coroutine: +##### 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 instantiation. +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 @@ -275,11 +285,42 @@ variables may be read: 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: +##### 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. @@ -287,12 +328,12 @@ Constructor args: `Pin` instances passed to the constructor must be instantiated by `machine`. -Class variables: +##### 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` Size of receive buffer. This should exceed the maximum message - length. Consider reducing this in ESP8266 applications to save RAM. + 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) diff --git a/i2c/asi2c_i.py b/i2c/asi2c_i.py index c0773b8..0cfdf29 100644 --- a/i2c/asi2c_i.py +++ b/i2c/asi2c_i.py @@ -37,9 +37,14 @@ class Initiator(Channel): t_poll = 100 # ms between Initiator polling Responder rxbufsize = 200 - def __init__(self, i2c, pin, pinack, reset=None, verbose=True): + 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 @@ -47,8 +52,8 @@ def __init__(self, i2c, pin, pinack, reset=None, verbose=True): self.block_max = 0 # Blocking times: max self.block_sum = 0 # Total self.block_cnt = 0 # Count - loop = asyncio.get_event_loop() - loop.create_task(self._run()) + 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() @@ -57,7 +62,10 @@ def waitfor(self, val): # Wait for response for 1 sec raise OSError async def reboot(self): + self.close() # Leave own pin high if self.reset is not None: + if self.cr_fail: + await self.cr_fail(*self.f_args) rspin, rsval, rstim = self.reset self.verbose and print('Resetting target.') rspin(rsval) # Pulse reset line @@ -72,6 +80,8 @@ async def _run(self): 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: @@ -85,6 +95,8 @@ async def _run(self): 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.') From 66985f074afe563e1cbac308403eabb9f8191924 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 19 Dec 2018 12:32:35 +0000 Subject: [PATCH 102/472] as_i2c_i.py Add optional coro args. --- i2c/asi2c_i.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i2c/asi2c_i.py b/i2c/asi2c_i.py index 0cfdf29..41c97da 100644 --- a/i2c/asi2c_i.py +++ b/i2c/asi2c_i.py @@ -81,7 +81,7 @@ async def _run(self): 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) + self.loop.create_task(self.cr_go(*self.go_args)) while True: gc.collect() try: From 402055e6a7dcb7e2dfaeb6773c6294e5727ab72b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 21 Dec 2018 06:51:15 +0000 Subject: [PATCH 103/472] as_i2c_i.py Fix bug where fail coro ran on startup. --- i2c/asi2c_i.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/i2c/asi2c_i.py b/i2c/asi2c_i.py index 41c97da..fa74936 100644 --- a/i2c/asi2c_i.py +++ b/i2c/asi2c_i.py @@ -64,8 +64,6 @@ def waitfor(self, val): # Wait for response for 1 sec async def reboot(self): self.close() # Leave own pin high if self.reset is not None: - if self.cr_fail: - await self.cr_fail(*self.f_args) rspin, rsval, rstim = self.reset self.verbose and print('Resetting target.') rspin(rsval) # Pulse reset line From acdae03e4be5a05783a1d7d29e3d59b731d1a447 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 21 Dec 2018 11:18:28 +0000 Subject: [PATCH 104/472] asyn.Event: add .wait method. --- PRIMITIVES.md | 4 ++++ asyn.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index f57c8fb..2b90f10 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -205,6 +205,10 @@ Synchronous Methods: * `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()`. diff --git a/asyn.py b/asyn.py index 97493d0..eac09bd 100644 --- a/asyn.py +++ b/asyn.py @@ -104,6 +104,10 @@ 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) From 3d4e398636aa7f083c7874c22c64f8950b5c2e3d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 22 Dec 2018 09:26:30 +0000 Subject: [PATCH 105/472] Tutorial: improve task cancellation section. --- TUTORIAL.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 5907d7e..3f81ca9 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -614,10 +614,24 @@ is next scheduled. This mechanism works with nested coros. However there is a limitation. If a coro issues `await uasyncio.sleep(secs)` or `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. +Another source of latency is where a task is waiting on I/O. In many +applications it is necessary for the task performing cancellation to pause +until all cancelled coros have actually stopped. -`uasyncio` lacks a mechanism for verifying when cancellation has actually -occurred. The `asyn` library provides verification via the following classes: +If the task to be cancelled only pauses on zero delays and never waits on I/O, +the round-robin nature of the scheduler avoids the need to verify cancellation: + +```python +asyncio.cancel(my_coro) +await asyncio.sleep(0) # Ensure my_coro gets scheduled with the exception + # my_coro will be cancelled now +``` +This does require that all coros awaited by `my_coro` also meet the zero delay +criterion. + +That special case notwithstanding, `uasyncio` lacks a mechanism for verifying +when cancellation has actually occurred. The `asyn` library provides +verification via the following classes: 1. `Cancellable` This allows one or more tasks to be assigned to a group. A coro can cancel all tasks in the group, pausing until this has been achieved. From 2a682a1a744d23c524021b828135236bca6d2317 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 24 Dec 2018 09:28:03 +0000 Subject: [PATCH 106/472] Docs: implications of uasyncio V2.2. aswitch: remove asyn dependency. --- FASTPOLL.md | 27 +++++++++++++++---------- README.md | 16 ++++++++++++--- TUTORIAL.md | 58 +++++++++++++++++++++++++---------------------------- aswitch.py | 14 ++++++++++++- 4 files changed, 69 insertions(+), 46 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index d9ff4c8..e08a631 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -60,19 +60,24 @@ formerly provided by `asyncio_priority.py` is now implemented. # 1. Installation -Install and test uasyncio on the target hardware. Replace `core.py` and -`__init__.py` with the files in the `fast_io` directory. - -In MicroPython 1.9 `uasyncio` was implemented as a frozen module on the -ESP8266. To install this version it is necessary to build the firmware with the -above two files implemented as frozen bytecode. See -[ESP Platforms](./FASTPOLL.md#6-esp-platforms) for general comments on the +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.9.4) 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. + +See [ESP Platforms](./FASTPOLL.md#6-esp-platforms) for general comments on the suitability of ESP platforms for systems requiring fast response. -It is possible to load modules in the filesystem in preference to frozen ones -by modifying `sys.path`. However the ESP8266 probably has too little RAM for -this to be useful. - ## 1.1 Benchmarks The benchmarks directory contains files demonstrating the performance gains diff --git a/README.md b/README.md index 5cb1640..bc3d629 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,20 @@ This repository comprises the following parts. # 2. Version and installation of uasyncio +As of 24th Dec 2018 Paul Sokolovsky has released uasyncio V2.2. This version +is on PyPi and requires his [Pycopy](https://github.com/pfalcon/micropython) +fork of MicroPython. + +I support only the official build of MicroPython. The library code guaranteed +to work with this build is in [micropython-lib](https://github.com/micropython/micropython-lib). +Most of the resources in here should work with Paul's forks (the great majority +work with CPython). I am unlikely to fix issues which are only evident in an +unofficial fork. + The documentation and code in this repository assume `uasyncio` version -2.0.x, which is the version on PyPi and in the official micropython-lib. This -requires firmware dated 22nd Feb 2018 or later. Use of the stream I/O mechanism -requires firmware after 17th June 2018. +2.0.x, the version in [micropython-lib](https://github.com/micropython/micropython-lib). +This requires firmware dated 22nd Feb 2018 or later. Use of the stream I/O +mechanism requires firmware after 17th June 2018. See [tutorial](./TUTORIAL.md#installing-uasyncio-on-bare-metal) for installation instructions. diff --git a/TUTORIAL.md b/TUTORIAL.md index 3f81ca9..72be4b7 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -91,42 +91,38 @@ CPython V3.5 and above. ## 0.1 Installing uasyncio on bare metal -MicroPython libraries are located on [PyPi](https://pypi.python.org/pypi). -Libraries to be installed are: - - * micropython-uasyncio - * micropython-uasyncio.queues - * micropython-uasyncio.synchro +If a release build of firmware is used no installation is necessary as uasyncio +is compiled into the build. The current release build (V1.9.4) does not support +asynchronous stream I/O. + +The following instructions cover the case where a release build is not used or +where a later official `uasyncio` version is required for stream I/O. The +instructions have changed as the version on PyPi is no longer compatible with +official MicroPython firmware. + +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 +``` +git clone https://github.com/micropython/micropython-lib.git +``` +On the target hardware create a `uasyncio` directory 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 `queues` and `synchro` modules are optional, but are required to run all the examples below. -The official approach is to use the `upip` utility as described -[here](https://github.com/micropython/micropython-lib). Network enabled -hardware has this included in the firmware so it can be run locally. This is -the preferred approach. - -On non-networked hardware there are two options. One is to use `upip` under a -Linux real or virtual machine. This involves installing and building the Unix -version of MicroPython, using `upip` to install to a directory on the PC, and -then copying the library to the target. - -The need for Linux and the Unix build may be avoided by using -[micropip.py](https://github.com/peterhinch/micropython-samples/tree/master/micropip). -This runs under Python 3.2 or above. Create a temporary directory on your PC -and install to that. Then copy the contents of the temporary directory to the -device. The following assume Linux and a temporary directory named `~/syn` - -adapt to suit your OS. The first option requires that `micropip.py` has -executable permission. - -``` -$ ./micropip.py install -p ~/syn micropython-uasyncio -$ python3 -m micropip.py install -p ~/syn micropython-uasyncio -``` - The `uasyncio` modules may be frozen as bytecode in the usual way, by placing -the `uasyncio` and `collections` directories in the port's `modules` directory -and rebuilding. +the `uasyncio` directory and its contents in the port's `modules` directory and +rebuilding. ###### [Main README](./README.md) diff --git a/aswitch.py b/aswitch.py index 066eda4..e428c05 100644 --- a/aswitch.py +++ b/aswitch.py @@ -30,8 +30,20 @@ import uasyncio as asyncio import utime as time -from asyn import launch +# 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(object): From ad181df5f4edc050ef4fc47da75e8a8812af6330 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 31 Dec 2018 07:17:59 +0000 Subject: [PATCH 107/472] i2c: PEP8 compliance. --- i2c/asi2c.py | 19 +++++++++++-------- i2c/asi2c_i.py | 9 +++++---- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/i2c/asi2c.py b/i2c/asi2c.py index a9c2df8..c41e5f1 100644 --- a/i2c/asi2c.py +++ b/i2c/asi2c.py @@ -37,6 +37,7 @@ # 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): @@ -80,7 +81,7 @@ def _txdone(self): self.txsiz[0] = 0 self.txsiz[1] = 0 -# Stream interface + # Stream interface def ioctl(self, req, arg): ret = _MP_STREAM_ERROR @@ -102,7 +103,7 @@ def readline(self): self.rxbyt = b'' else: t = self.rxbyt[: n + 1] - self.rxbyt = self.rxbyt[n + 1 :] + self.rxbyt = self.rxbyt[n + 1:] return t.decode() def read(self, n): @@ -120,7 +121,7 @@ def write(self, buf, off, sz): if off == 0 and sz == len(buf): d = buf else: - d = buf[off : off + sz] + d = buf[off: off + sz] d = d.encode() l = len(d) self.txbyt = d @@ -129,7 +130,7 @@ def write(self, buf, off, sz): return l return 0 -# User interface + # User interface # Wait for sync async def ready(self): @@ -140,6 +141,7 @@ async def ready(self): 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. @@ -147,6 +149,7 @@ def close(self): 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() @@ -154,13 +157,13 @@ def __init__(self, i2c, pin, pinack, verbose=True): 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) + 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) + 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) @@ -173,7 +176,7 @@ def _handler(self, _, sn=bytearray(2), txnull=bytearray(2)): if n: self.waitfor(1) utime.sleep_us(_DELAY) - mv = memoryview(self.rx_mv[0 : n]) # allocates + mv = memoryview(self.rx_mv[0: n]) # allocates self.i2c.readfrom_into(addr, mv) self.own(1) self.waitfor(0) @@ -200,4 +203,4 @@ def _handler(self, _, sn=bytearray(2), txnull=bytearray(2)): self.own(0) self.waitfor(0) self._txdone() # Invalidate source - self.rem.irq(handler = self._handler, trigger = machine.Pin.IRQ_RISING) + self.rem.irq(handler=self._handler, trigger=machine.Pin.IRQ_RISING) diff --git a/i2c/asi2c_i.py b/i2c/asi2c_i.py index fa74936..b3f7ddb 100644 --- a/i2c/asi2c_i.py +++ b/i2c/asi2c_i.py @@ -29,6 +29,7 @@ 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 @@ -46,7 +47,7 @@ def __init__(self, i2c, pin, pinack, reset=None, verbose=True, 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])) + 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 @@ -109,7 +110,7 @@ def _sendrx(self, sn=bytearray(2), txnull=bytearray(2)): # 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.i2c.send(siz) # Blocks until RX complete. self.waitfor(1) self.own(0) self.waitfor(0) @@ -132,9 +133,9 @@ def _sendrx(self, sn=bytearray(2), txnull=bytearray(2)): self.cantx = not bool(sn[1] & 0x80) if n: self.waitfor(1) # Wait for responder to request send - #print('setting up receive', n,' bytes') + # print('setting up receive', n,' bytes') self.own(1) # Acknowledge - mv = memoryview(self.rx_mv[0 : n]) + mv = memoryview(self.rx_mv[0: n]) self.i2c.recv(mv) self.waitfor(0) self.own(0) From 26b66e371d060ab7b43409c9a916c9e451bea72e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 1 Feb 2019 12:41:58 +0000 Subject: [PATCH 108/472] Tutorial: improve timeout section. --- TUTORIAL.md | 25 ++++++++++++++++++++++++- aswitch.py | 1 - 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 72be4b7..527c26f 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -904,7 +904,9 @@ Timeouts are implemented by means of `uasyncio.wait_for()`. This takes as arguments a coroutine and a timeout in seconds. If the timeout expires a `TimeoutError` will be thrown to the coro in such a way that the next time the coro is scheduled for execution the exception will be raised. The coro should -trap this and quit. +trap this and quit; alternatively the calling coro should trap and ignore the +exception. If the exception is not trapped the code will hang: this appears to +be a bug in `uasyncio` V2.0. ```python import uasyncio as asyncio @@ -922,6 +924,27 @@ async def foo(): await asyncio.wait_for(forever(), 5) await asyncio.sleep(2) +loop = asyncio.get_event_loop() +loop.run_until_complete(foo()) +``` +Alternatively: +```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()) ``` diff --git a/aswitch.py b/aswitch.py index e428c05..e362af3 100644 --- a/aswitch.py +++ b/aswitch.py @@ -115,7 +115,6 @@ def __call__(self): return self.switchstate async def switchcheck(self): - loop = asyncio.get_event_loop() while True: state = self.pin.value() if state != self.switchstate: From 990732222f1cc6d3f78f08e229cda8f171451967 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 1 Feb 2019 13:56:25 +0000 Subject: [PATCH 109/472] Tutorial: update note on async context managers. --- TUTORIAL.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 527c26f..0092c1d 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -891,10 +891,7 @@ comes from the `Lock` class: If the `async with` has an `as variable` clause the variable receives the value returned by `__aenter__`. -There was a bug in the implementation whereby if an explicit `return` was issued -within an `async with` block, the `__aexit__` method was not called. This was -fixed as of 27th June 2018 [PR 3890](https://github.com/micropython/micropython/pull/3890) -but the fix was too late for the current release build (V1.9.4). +To ensure correct behaviour firmware should be V1.9.10 or later. ###### [Contents](./TUTORIAL.md#contents) From 190a0f9c0a4a98057a9435795f46dc2369489b3f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 3 Feb 2019 11:21:03 +0000 Subject: [PATCH 110/472] Tutorial: improve section on exceptions. --- TUTORIAL.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index 0092c1d..d14a22d 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -959,8 +959,10 @@ in a loop. The coro `asyn.sleep` [supports this](./PRIMITIVES.md#41-coro-sleep). 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 it would stop running, -passing the exception to the code which started the scheduler. +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 From c3f859a65befdeec84212057e10a3a01f9f3f09d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 14 Feb 2019 18:47:29 +0000 Subject: [PATCH 111/472] Tutorial: improve task cancellation section. --- TUTORIAL.md | 78 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index d14a22d..1f929ec 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -605,17 +605,31 @@ controlled. Documentation of this is in the code. ## 3.6 Task cancellation `uasyncio` provides 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. However there is a -limitation. If a coro issues `await uasyncio.sleep(secs)` or -`uasyncio.sleep_ms(ms)` scheduling will not occur until the time has elapsed. -This introduces latency into cancellation which matters in some use-cases. -Another source of latency is where a task is waiting on I/O. In many -applications it is necessary for the task performing cancellation to pause -until all cancelled coros have actually stopped. - -If the task to be cancelled only pauses on zero delays and never waits on I/O, -the round-robin nature of the scheduler avoids the need to verify cancellation: +exception to the coro in a special way: when the coro is next scheduled it +receives the exception. This mechanism 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) +``` +In this example 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. + +In many applications it is necessary for the task performing cancellation to +pause until all cancelled coros have actually stopped. If the task to be +cancelled only pauses on zero delays and never waits on I/O, the round-robin +nature of the scheduler avoids the need to verify cancellation: ```python asyncio.cancel(my_coro) @@ -623,11 +637,45 @@ await asyncio.sleep(0) # Ensure my_coro gets scheduled with the exception # my_coro will be cancelled now ``` This does require that all coros awaited by `my_coro` also meet the zero delay -criterion. +criterion. For the general case where latency exists, solutions are discussed +below. + +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 `cancel` is called and `foo` is next scheduled it is removed from the +scheduler's queue; because it lacks a `return` statement the calling routine +`foo_runner` never resumes. The solution is to trap the exception: +```python +async def foo(): + try: + while True: + # do something every 10 secs + await asyncio.sleep(10) + except asyncio.CancelledError: + return +``` -That special case notwithstanding, `uasyncio` lacks a mechanism for verifying -when cancellation has actually occurred. The `asyn` library provides -verification via the following classes: +In general `uasyncio` lacks a mechanism for verifying when cancellation has +actually occurred. Ad-hoc mechanisms based on trapping `CancelledError` may be +devised. For convenience the `asyn` library provides means of awaiting the +cancellation of one or more coros via these classes: 1. `Cancellable` This allows one or more tasks to be assigned to a group. A coro can cancel all tasks in the group, pausing until this has been achieved. From d88e48b43e9df1726c8ee279a38dc8829a491b7e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 20 Feb 2019 16:34:15 +0000 Subject: [PATCH 112/472] fast_io Improve code comments around pend_throw. --- fast_io/__init__.py | 2 +- fast_io/core.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/fast_io/__init__.py b/fast_io/__init__.py index d2de3e5..6622743 100644 --- a/fast_io/__init__.py +++ b/fast_io/__init__.py @@ -103,7 +103,7 @@ def wait(self, delay): if isinstance(cb, tuple): cb[0](*cb[1]) else: - cb.pend_throw(None) # Clears the pend_throw(False) executed when IOWrite was yielded + cb.pend_throw(None) # Ensure that, if task is cancelled, it doesn't get queued again self._call_io(cb) # Put coro onto runq (or ioq if one exists) if ev & select.POLLIN: cb = self.rdobjmap[id(sock)] diff --git a/fast_io/core.py b/fast_io/core.py index d7b71a5..330a93b 100644 --- a/fast_io/core.py +++ b/fast_io/core.py @@ -200,7 +200,8 @@ def run_forever(self): if isinstance(ret, After): delay = int(delay*1000) elif isinstance(ret, IORead): # coro was a StreamReader read method - cb.pend_throw(False) # Why? I think this is for debugging. If it is scheduled other than by wait + cb.pend_throw(False) # If task is cancelled or times out, it is put on runq to process exception. + # Note if it 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 @@ -361,7 +362,7 @@ def __next__(self): def cancel(coro): prev = coro.pend_throw(CancelledError()) - if prev is False: + if prev is False: # Not on runq so put it there _event_loop.call_soon(coro) From 96c501e2ca5c37834c29d31574a206d266e74c86 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 24 Feb 2019 11:25:02 +0000 Subject: [PATCH 113/472] V0.24 Fast response to cancellation and timeout. --- FASTPOLL.md | 93 ++++--- PRIMITIVES.md | 65 +++-- README.md | 41 ++- TUTORIAL.md | 514 ++++++++++++++++++------------------ UNDER_THE_HOOD.md | 101 +++---- asyn.py | 5 +- benchmarks/call_lp.py | 4 +- benchmarks/latency.py | 2 +- benchmarks/overdue.py | 8 +- benchmarks/priority_test.py | 8 +- fast_io/__init__.py | 30 ++- fast_io/core.py | 67 +++-- fast_io/ms_timer_test.py | 21 +- fast_io/pin_cb_test.py | 23 +- 14 files changed, 543 insertions(+), 439 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index e08a631..2a982f8 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -2,9 +2,15 @@ This version is a "drop in" replacement for official `uasyncio`. Existing applications should run under it unchanged and with essentially identical -performance. +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: +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. @@ -18,7 +24,7 @@ This version has the following features: 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 of the `fast_io` version can be tested at runtime. + * 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 @@ -31,6 +37,12 @@ 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 retuens 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 @@ -63,7 +75,7 @@ formerly provided by `asyncio_priority.py` is now implemented. 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.9.4) has `uasyncio` implemented as a +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 @@ -75,33 +87,43 @@ frozen module. The following options for installing `fast_io` exist: 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 benchmarks directory contains files demonstrating the performance gains -offered by prioritisation. They also offer illustrations of the use of these -features. Documentation is in the code. +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` Demos low priority callbacks. + * `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/ms_timer.py` Provides higher precision timing than `wait_ms()`. - * `fast_io/ms_timer_test.py` Test/demo program for above. - * `fast_io/pin_cb.py` Demo of an I/O device driver which causes a pin state - change to trigger a callback. - * `fast_io/pin_cb_test.py` Demo of above. - -With the exceptions of `call_lp`, `priority` and `rate_fastio`, benchmarks can -be run against the official and priority versions of usayncio. + * `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 @@ -325,8 +347,8 @@ See [Low priority callbacks](./FASTPOLL.md#35-low-priority-callbacks) ## 3.3 Other Features Variable: - * `version` Contains 'fast_io'. Enables the presence of this version to be - determined at runtime. + * `version` Returns a 2-tuple. Current contents ('fast_io', '0.24'). Enables + the presence and realease state of this version to be determined at runtime. Function: * `got_event_loop()` No arg. Returns a `bool`: `True` if the event loop has @@ -348,6 +370,8 @@ bar = Bar() # Constructor calls get_event_loop() # and renders these args inoperative loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) ``` +This is mainly for retro-fitting to existing classes and functions. The +preferred approach is to pass the event loop to classes as a constructor arg. ###### [Contents](./FASTPOLL.md#contents) @@ -415,8 +439,8 @@ 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#36-task-cancellation) -and [Coroutines with timeouts](./TUTORIAL.md#44-coroutines-with-timeouts). +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) @@ -468,22 +492,27 @@ Support was finally [added here](https://github.com/micropython/micropython/pull # 6. Performance -This version is designed to enable existing applications to run without change -to code and to minimise the effect on raw scheduler performance in the case -where the added functionality is unused. +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: -The benchmark `rate.py` measures the rate at which tasks can be scheduled. It -was run (on a Pyboard V1.1) under official `uasyncio` V2, then under this -version. The benchmark `rate_fastio` is identical except it instantiates an I/O -queue and a low priority queue. 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 | -| Script | Uasyncio version | Period (100 coros) | Overhead | -|:------:|:----------------:|:------------------:|:--------:| -| rate | Official V2 | 156μs | 0% | -| rate | fast_io | 162μs | 3.4% | -| rate_fastio | fast_io | 206μs | 32% | +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/PRIMITIVES.md b/PRIMITIVES.md index 2b90f10..bad474e 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -38,7 +38,6 @@ obvious workround is to produce a version with unused primitives removed. 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) - 4.3.3 [Changes](./PRIMITIVES.md#433-changes) June 2018 asyn API changes affecting cancellation. ## 1.1 Synchronisation Primitives @@ -456,18 +455,46 @@ loop.run_until_complete(main(loop)) # 4. Task Cancellation -This has been under active development. Existing users please see -[Changes](./PRIMITIVES.md#433-changes) for recent API changes. +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. -`uasyncio` now provides 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. However there is a -limitation. 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` has no mechanism for verifying when cancellation has actually -occurred. The `asyn.py` library provides solutions in the form of two classes. +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 @@ -759,18 +786,4 @@ async def foo(): return True # Normal exit ``` -### 4.3.3 Changes - -The `NamedTask` class has been rewritten as a subclass of `Cancellable`. This -is to simplify the code and to ensure accuracy of the `is_running` method. The -latest API changes are: - * `Cancellable.stopped()` is no longer a public method. - * `NamedTask.cancel()` is now asynchronous. - * `NamedTask` and `Cancellable` coros no longer receive a `TaskId` instance as - their 1st arg. - * `@asyn.namedtask` still works but is now an alias for `@asyn.cancellable`. - -The drive to simplify code comes from the fact that `uasyncio` is itself under -development. Tracking changes is an inevitable headache. - ###### [Contents](./PRIMITIVES.md#contents) diff --git a/README.md b/README.md index bc3d629..2774ae6 100644 --- a/README.md +++ b/README.md @@ -35,23 +35,25 @@ This repository comprises the following parts. # 2. Version and installation of uasyncio -As of 24th Dec 2018 Paul Sokolovsky has released uasyncio V2.2. This version +Paul Sokolovsky (`uasyncio` author) has released `uasyncio` V2.4. This version is on PyPi and requires his [Pycopy](https://github.com/pfalcon/micropython) -fork of MicroPython. +fork of MicroPython firmware. His `uasyncio` code may also be found in +[his fork of micropython-lib](https://github.com/pfalcon/micropython-lib). I support only the official build of MicroPython. The library code guaranteed to work with this build is in [micropython-lib](https://github.com/micropython/micropython-lib). -Most of the resources in here should work with Paul's forks (the great majority -work with CPython). I am unlikely to fix issues which are only evident in an -unofficial fork. +Most of the resources in here should work with Paul's forks (most work with +CPython). -The documentation and code in this repository assume `uasyncio` version -2.0.x, the version in [micropython-lib](https://github.com/micropython/micropython-lib). -This requires firmware dated 22nd Feb 2018 or later. Use of the stream I/O -mechanism requires firmware after 17th June 2018. +Most documentation and code in this repository assumes the current official +version of `uasyncio`. This is V2.0 from +[micropython-lib](https://github.com/micropython/micropython-lib). +If release build of MicroPython V1.10 or later is used, V2.0 is incorporated +and no installation is required. Some examples illustrate the features of the +`fast_io` fork and therefore require this version. See [tutorial](./TUTORIAL.md#installing-uasyncio-on-bare-metal) for -installation instructions. +installation instructions where a realease build is not used. # 3. uasyncio development state @@ -114,9 +116,15 @@ 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#54-writing-streaming-device-drivers) +`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 version 2.0, where a coroutine is waiting on a `sleep()` or on +I/O, a timeout or cancellation are deferred until the coroutine is next +scheduled. This introduces uncertainty into when the coroutine is stopped. This +issue is also addressed in Paul Sokolovsky's fork. + ## 4.1 A Pyboard-only low power module This is documented [here](./lowpower/README.md). In essence a Python file is @@ -124,17 +132,6 @@ 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. -## 4.2 Historical note - -This repo formerly included `asyncio_priority.py` which is obsolete. Its main -purpose was to provide a means of servicing fast hardware devices by means of -coroutines running at a high priority. This was essentially a workround. - -The official firmware now includes -[this major improvement](https://github.com/micropython/micropython/pull/3836) -which offers a much more efficient way of achieving the same end using stream -I/O and efficient polling using `select.poll`. - # 5. The asyn.py library This library ([docs](./PRIMITIVES.md)) provides 'micro' implementations of the diff --git a/TUTORIAL.md b/TUTORIAL.md index 1f929ec..00bd160 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -25,42 +25,44 @@ asyncio and includes a section for complete beginners. 3.4 [Semaphore](./TUTORIAL.md#34-semaphore) 3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore) 3.5 [Queue](./TUTORIAL.md#35-queue) - 3.6 [Task cancellation](./TUTORIAL.md#36-task-cancellation) - 3.7 [Other synchronisation primitives](./TUTORIAL.md#37-other-synchronisation-primitives) + 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) - 4.4 [Coroutines with timeouts](./TUTORIAL.md#44-coroutines-with-timeouts) - 4.5 [Exceptions](./TUTORIAL.md#45-exceptions) - 5. [Interfacing hardware](./TUTORIAL.md#5-interfacing-hardware) - 5.1 [Timing issues](./TUTORIAL.md#51-timing-issues) - 5.2 [Polling hardware with a coroutine](./TUTORIAL.md#52-polling-hardware-with-a-coroutine) - 5.3 [Using the stream mechanism](./TUTORIAL.md#53-using-the-stream-mechanism) - 5.3.1 [A UART driver example](./TUTORIAL.md#531-a-uart-driver-example) - 5.4 [Writing streaming device drivers](./TUTORIAL.md#54-writing-streaming-device-drivers) - 5.5 [A complete example: aremote.py](./TUTORIAL.md#55-a-complete-example-aremotepy) + 5 [Exceptions timeouts and cancellation](./TUTORIAL.md#45-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. - 5.6 [Driver for HTU21D](./TUTORIAL.md#56-htu21d-environment-sensor) A + 6.6 [Driver for HTU21D](./TUTORIAL.md#66-htu21d-environment-sensor) A temperature and humidity sensor. - 6. [Hints and tips](./TUTORIAL.md#6-hints-and-tips) - 6.1 [Program hangs](./TUTORIAL.md#61-program-hangs) - 6.2 [uasyncio retains state](./TUTORIAL.md#62-uasyncio-retains-state) - 6.3 [Garbage Collection](./TUTORIAL.md#63-garbage-collection) - 6.4 [Testing](./TUTORIAL.md#64-testing) - 6.5 [A common error](./TUTORIAL.md#65-a-common-error) This can be hard to find. - 6.6 [Socket programming](./TUTORIAL.md#66-socket-programming) - 6.7 [Event loop constructor args](./TUTORIAL.md#67-event-loop-constructor-args) - 7. [Notes for beginners](./TUTORIAL.md#7-notes-for-beginners) - 7.1 [Problem 1: event loops](./TUTORIAL.md#71-problem-1:-event-loops) - 7.2 [Problem 2: blocking methods](./TUTORIAL.md#7-problem-2:-blocking-methods) - 7.3 [The uasyncio approach](./TUTORIAL.md#73-the-uasyncio-approach) - 7.4 [Scheduling in uasyncio](./TUTORIAL.md#74-scheduling-in-uasyncio) - 7.5 [Why cooperative rather than pre-emptive?](./TUTORIAL.md#75-why-cooperative-rather-than-pre-emptive) - 7.6 [Communication](./TUTORIAL.md#76-communication) - 7.7 [Polling](./TUTORIAL.md#77-polling) + 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.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) @@ -68,7 +70,7 @@ asyncio and includes a section for complete beginners. 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#7-notes-for-beginners). +[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 @@ -92,13 +94,12 @@ CPython V3.5 and above. ## 0.1 Installing uasyncio on bare metal If a release build of firmware is used no installation is necessary as uasyncio -is compiled into the build. The current release build (V1.9.4) does not support +is compiled into the build. The current release build (V1.9.10) now supports asynchronous stream I/O. -The following instructions cover the case where a release build is not used or -where a later official `uasyncio` version is required for stream I/O. The -instructions have changed as the version on PyPi is no longer compatible with -official MicroPython firmware. +The following instructions cover the case where a release build is not used. +The instructions have changed as the version on PyPi is no longer compatible +with official MicroPython firmware. 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 @@ -110,8 +111,8 @@ Clone the library to a PC with ``` git clone https://github.com/micropython/micropython-lib.git ``` -On the target hardware create a `uasyncio` directory and copy the following -files to it: +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` @@ -157,7 +158,7 @@ results by accessing Pyboard hardware. 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. + 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. @@ -187,7 +188,7 @@ results by accessing Pyboard hardware. 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 6.5](./TUTORIAL.md#65-a-common-error). + [para 7.5](./TUTORIAL.md#75-a-common-error). **Benchmarks** @@ -238,7 +239,7 @@ 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#67-event-loop-constructor-args). +[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. @@ -345,7 +346,7 @@ 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 5](./TUTORIAL.md#5-interfacing-hardware). +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 @@ -422,7 +423,7 @@ 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#44-coroutines-with-timeouts) +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. @@ -602,133 +603,7 @@ controlled. Documentation of this is in the code. ###### [Contents](./TUTORIAL.md#contents) -## 3.6 Task cancellation - -`uasyncio` provides a `cancel(coro)` function. This works by throwing an -exception to the coro in a special way: when the coro is next scheduled it -receives the exception. This mechanism 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) -``` -In this example 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. - -In many applications it is necessary for the task performing cancellation to -pause until all cancelled coros have actually stopped. If the task to be -cancelled only pauses on zero delays and never waits on I/O, the round-robin -nature of the scheduler avoids the need to verify cancellation: - -```python -asyncio.cancel(my_coro) -await asyncio.sleep(0) # Ensure my_coro gets scheduled with the exception - # my_coro will be cancelled now -``` -This does require that all coros awaited by `my_coro` also meet the zero delay -criterion. For the general case where latency exists, solutions are discussed -below. - -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 `cancel` is called and `foo` is next scheduled it is removed from the -scheduler's queue; because it lacks a `return` statement the calling routine -`foo_runner` never resumes. The solution is to trap the exception: -```python -async def foo(): - try: - while True: - # do something every 10 secs - await asyncio.sleep(10) - except asyncio.CancelledError: - return -``` - -In general `uasyncio` lacks a mechanism for verifying when cancellation has -actually occurred. Ad-hoc mechanisms based on trapping `CancelledError` may be -devised. For convenience the `asyn` library provides means of awaiting the -cancellation of one or more coros via these classes: - - 1. `Cancellable` This allows one or more tasks to be assigned to a group. A - coro can cancel all tasks in the group, pausing until this has been achieved. - Documentation may be found [here](./PRIMITIVES.md#42-class-cancellable). - 2. `NamedTask` This enables a coro to be associated with a user-defined name. - The running status of named coros may be checked. For advanced usage more - complex groupings of tasks can be created. Documentation may be found - [here](./PRIMITIVES.md#43-class-namedtask). - -A typical use-case is as follows: - -```python -async def comms(): # Perform some communications task - while True: - await initialise_link() - try: - await do_communications() # Launches Cancellable tasks - except CommsError: - await Cancellable.cancel_all() - # All sub-tasks are now known to be stopped. They can be re-started - # with known initial state on next pass. -``` - -Examples of the usage of these classes may be found in `asyn_demos.py`. For an -illustration of the mechanism a cancellable task is defined as below: - -```python -@asyn.cancellable -async def print_nums(num): - while True: - print(num) - num += 1 - await asyn.sleep(1) -``` - -It is launched and cancelled with: - -```python -async def foo(): - loop = asyncio.get_event_loop() - loop.create_task(asyn.Cancellable(print_nums, 42)()) - await asyn.sleep(7.5) - await asyn.Cancellable.cancel_all() - print('Done') -``` - -**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) - -## 3.7 Other synchronisation primitives +## 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) @@ -943,67 +818,13 @@ To ensure correct behaviour firmware should be V1.9.10 or later. ###### [Contents](./TUTORIAL.md#contents) -## 4.4 Coroutines with timeouts - -Timeouts are implemented by means of `uasyncio.wait_for()`. This takes as -arguments a coroutine and a timeout in seconds. If the timeout expires a -`TimeoutError` will be thrown to the coro in such a way that the next time the -coro is scheduled for execution the exception will be raised. The coro should -trap this and quit; alternatively the calling coro should trap and ignore the -exception. If the exception is not trapped the code will hang: this appears to -be a bug in `uasyncio` V2.0. - -```python -import uasyncio as asyncio - -async def forever(): - print('Starting') - try: - while True: - await asyncio.sleep_ms(300) - print('Got here') - except asyncio.TimeoutError: - print('Got timeout') - -async def foo(): - await asyncio.wait_for(forever(), 5) - await asyncio.sleep(2) - -loop = asyncio.get_event_loop() -loop.run_until_complete(foo()) -``` -Alternatively: -```python -import uasyncio as asyncio - -async def forever(): - print('Starting') - while True: - await asyncio.sleep_ms(300) - print('Got here') +# 5 Exceptions timeouts and cancellation -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()) -``` - -Note that if the coro issues `await asyncio.sleep(t)` where `t` is a long delay -it 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. +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. -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). - -## 4.5 Exceptions +## 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 @@ -1058,7 +879,177 @@ a keyboard interrupt should trap the exception at the event loop level. ###### [Contents](./TUTORIAL.md#contents) -# 5 Interfacing hardware +## 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 provides `uasyncio` V2.4, but this requires + his [Pycopy](https://github.com/pfalcon/micropython) firmware. + * The `fast_io` fork also solves this (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 @@ -1084,21 +1075,21 @@ async def poll_my_device(): 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#52-polling-hardware-with-a-coroutine). +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#53-using-the-stream-mechanism). +[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 5.4](./TUTORIAL.md#54-writing-streaming-device-drivers). +streaming devices [section 6.4](./TUTORIAL.md#64-writing-streaming-device-drivers). ###### [Contents](./TUTORIAL.md#contents) -## 5.1 Timing issues +## 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 @@ -1135,7 +1126,7 @@ hoped that official `uasyncio` will adopt code to this effect in due course. ###### [Contents](./TUTORIAL.md#contents) -## 5.2 Polling hardware with a coroutine +## 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 @@ -1213,7 +1204,7 @@ loop.run_until_complete(run()) ###### [Contents](./TUTORIAL.md#contents) -## 5.3 Using the stream mechanism +## 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 @@ -1245,7 +1236,7 @@ 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#54-writing-streaming-device-drivers) +[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 @@ -1257,7 +1248,7 @@ 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. -### 5.3.1 A UART driver example +### 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. @@ -1281,7 +1272,7 @@ returned. See the code comments for more details. ###### [Contents](./TUTORIAL.md#contents) -## 5.4 Writing streaming device drivers +## 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 @@ -1451,7 +1442,7 @@ The [fast_io](./FASTPOLL.md) version addresses this issue. ###### [Contents](./TUTORIAL.md#contents) -## 5.5 A complete example: aremote.py +## 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 @@ -1468,7 +1459,7 @@ any `asyncio` latency when setting its delay period. ###### [Contents](./TUTORIAL.md#contents) -## 5.6 HTU21D environment sensor +## 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 @@ -1480,11 +1471,11 @@ 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. -# 6 Hints and tips +# 7 Hints and tips ###### [Contents](./TUTORIAL.md#contents) -## 6.1 Program hangs +## 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 @@ -1493,7 +1484,7 @@ scheduler is running. ###### [Contents](./TUTORIAL.md#contents) -## 6.2 uasyncio retains state +## 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 @@ -1501,7 +1492,7 @@ which can lead to confusing behaviour. ###### [Contents](./TUTORIAL.md#contents) -## 6.3 Garbage Collection +## 7.3 Garbage Collection You may want to consider running a coro which issues: @@ -1516,7 +1507,7 @@ in the section on the heap. ###### [Contents](./TUTORIAL.md#contents) -## 6.4 Testing +## 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 @@ -1565,7 +1556,7 @@ Welcome to the joys of realtime programming. ###### [Contents](./TUTORIAL.md#contents) -## 6.5 A common error +## 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 @@ -1613,7 +1604,7 @@ I find it useful as-is but improvements are always welcome. ###### [Contents](./TUTORIAL.md#contents) -## 6.6 Socket programming +## 7.6 Socket programming The use of nonblocking sockets requires some attention to detail. If a nonblocking read is performed, because of server latency, there is no guarantee @@ -1640,7 +1631,7 @@ long periods, and offers a solution. ###### [Contents](./TUTORIAL.md#contents) -## 6.7 Event loop constructor args +## 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 @@ -1665,35 +1656,36 @@ import some_module bar = some_module.Bar() # The get_event_loop() call is now safe ``` -If imported modules do not run `uasyncio` code, another approach is to pass the -loop as an arg to any user code which needs it. Ensure that only the initially -loaded module calls `get_event_loop` e.g. +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 some_module +import my_module # Does not run code on loading loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) -bar = some_module.Bar(loop) +bar = my_module.Bar(loop) ``` Ref [this issue](https://github.com/micropython/micropython-lib/issues/295). ###### [Contents](./TUTORIAL.md#contents) -# 7 Notes for beginners +# 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 7.5](./TUTORIAL.md#75-why-cooperative-rather-than-pre-emptive) +[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) -## 7.1 Problem 1: event loops +## 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 @@ -1752,7 +1744,7 @@ created. ###### [Contents](./TUTORIAL.md#contents) -## 7.2 Problem 2: blocking methods +## 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 @@ -1764,7 +1756,7 @@ 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. -## 7.3 The uasyncio approach +## 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 @@ -1842,7 +1834,7 @@ loop.run_until_complete(killer()) # Execution passes to coroutines. ###### [Contents](./TUTORIAL.md#contents) -## 7.4 Scheduling in uasyncio +## 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 @@ -1902,7 +1894,7 @@ required, especially one below a few ms, it may be necessary to use ###### [Contents](./TUTORIAL.md#contents) -## 7.5 Why cooperative rather than pre-emptive? +## 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 @@ -1948,7 +1940,7 @@ An eloquent discussion of the evils of threading may be found ###### [Contents](./TUTORIAL.md#contents) -## 7.6 Communication +## 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 @@ -1960,7 +1952,7 @@ communications; in a cooperative system these are seldom required. ###### [Contents](./TUTORIAL.md#contents) -## 7.7 Polling +## 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 diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index 0c9c858..25b9113 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -11,25 +11,24 @@ wishing to modify it. 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](./UNDER_THE_HOOD.md#42-task-cancellation) + 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. [Debug code](./UNDER_THE_HOOD.md#6-debug-code) - 7. [Modifying uasyncio](./UNDER_THE_HOOD.md#7-modifying-uasyncio) - 8. [Links](./UNDER_THE_HOOD.md#8-links) + 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. -Differences are largely in `__init__.py`: the scheduling algorithm in `core.py` -is little changed. Note that the code in `fast_io` contains additional comments -to explain its operation. The code the `fast_io` directory is also in +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. @@ -54,7 +53,7 @@ fast_io/__init__.py fast_io/core.py ``` -This has additional comments to aid in its understanding. +This has additional code comments to aid in its understanding. ###### [Main README](./README.md) @@ -81,7 +80,33 @@ 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 (see below). +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. + +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) @@ -111,7 +136,7 @@ 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)` + 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 @@ -148,14 +173,14 @@ 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 three queues, `.runq`, `.waitq` and `.ioq`, -although `.ioq` is only instantiated if it is specified. Official `uasyncio` -does not have `.ioq`. +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 the wait queue are sorted in order of the time when they are -to run, the task having the soonest time to run at the top. +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 @@ -175,7 +200,7 @@ 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 +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 @@ -205,7 +230,7 @@ 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 +## 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 @@ -213,6 +238,15 @@ 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 discarded. This pure 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 @@ -289,36 +323,7 @@ Writing is handled similarly. ###### [Contents](./UNDER_THE_HOOD.md#0-contents) -# 6. Debug code - -The official `uasyncio` contains considerable explicit debug code: schedulers -are hard to debug. - -There is also code which I believe is for debugging purposes including some I -have added myself for this purpose. The aim is to ensure that, if an error -causes a coro to be scheduled when it shouldn't be, an exception is thrown. The -alternative is weird, hard to diagnose, behaviour. - -Consider these instances: -[pend_throw(false)](https://github.com/peterhinch/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/uasyncio.core/uasyncio/core.py#L119) -also [here](https://github.com/peterhinch/micropython-lib/blob/f20d89c6aad9443a696561ca2a01f7ef0c8fb302/uasyncio.core/uasyncio/core.py#L123). -I think the intention here is to throw an exception (exception doesn't inherit -from Exception) if it is scheduled incorrectly. Correct scheduling countermands -this -[here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L97) -and [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L114): -these lines ensures that the exception will not be thrown. If my interpretation -of this is wrong I'd be very glad to be enlightened. - -The `rdobjmap` and `wrobjmap` dictionary entries are invalidated -[here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L91) -and [here](https://github.com/peterhinch/micropython-lib/blob/819562312bae807ce0d01aa8ad36a13c22ba9e40/uasyncio/uasyncio/__init__.py#L101). -This has the same aim: if an attempt is made incorrectly to reschedule them, an -exception is thrown. - -###### [Contents](./UNDER_THE_HOOD.md#0-contents) - -# 7. Modifying uasyncio +# 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 @@ -352,7 +357,7 @@ def get_event_loop(args): ###### [Contents](./UNDER_THE_HOOD.md#0-contents) -# 8. Links +# 7. Links Initial discussion of priority I/O scheduling [here](https://github.com/micropython/micropython/issues/2664). diff --git a/asyn.py b/asyn.py index eac09bd..7496027 100644 --- a/asyn.py +++ b/asyn.py @@ -2,7 +2,6 @@ # Test/demo programs asyntest.py, barrier_test.py # Provides Lock, Event, Barrier, Semaphore, BoundedSemaphore, Condition, # NamedTask and Cancellable classes, also sleep coro. -# Uses low_priority where available and appropriate. # Updated 31 Dec 2017 for uasyncio.core V1.6 and to provide task cancellation. # The MIT License (MIT) @@ -136,8 +135,6 @@ def value(self): # it. The use of nowait promotes efficiency by enabling tasks which have been # cancelled to leave the task queue as soon as possible. -# Uses low_priority if available - class Barrier(): def __init__(self, participants, func=None, args=()): self._participants = participants @@ -235,7 +232,7 @@ def __call__(self): return self.taskid # Sleep coro breaks up a sleep into shorter intervals to ensure a rapid -# response to StopTask exceptions +# 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') diff --git a/benchmarks/call_lp.py b/benchmarks/call_lp.py index becc1a0..813787f 100644 --- a/benchmarks/call_lp.py +++ b/benchmarks/call_lp.py @@ -4,10 +4,10 @@ import pyb import uasyncio as asyncio try: - if asyncio.version != 'fast_io': + if not(isinstance(asyncio.version, tuple)): raise AttributeError except AttributeError: - raise OSError('This program requires uasyncio fast_io version.') + raise OSError('This program requires uasyncio fast_io version V0.24 or above.') loop = asyncio.get_event_loop(lp_len=16) diff --git a/benchmarks/latency.py b/benchmarks/latency.py index 0d6985a..786cd22 100644 --- a/benchmarks/latency.py +++ b/benchmarks/latency.py @@ -20,7 +20,7 @@ import uasyncio as asyncio lp_version = True try: - if asyncio.version != 'fast_io': + if not(isinstance(asyncio.version, tuple)): raise AttributeError except AttributeError: lp_version = False diff --git a/benchmarks/overdue.py b/benchmarks/overdue.py index 85f5b9c..a777f5e 100644 --- a/benchmarks/overdue.py +++ b/benchmarks/overdue.py @@ -1,14 +1,10 @@ # overdue.py Test for "low priority" uasyncio. Author Peter Hinch April 2017. import uasyncio as asyncio -p_version = True try: - if asyncio.version != 'fast_io': + if not(isinstance(asyncio.version, tuple)): raise AttributeError except AttributeError: - p_version = False - -if not p_version: - raise OSError('This program requires uasyncio fast_io version.') + raise OSError('This program requires uasyncio fast_io version V0.24 or above.') loop = asyncio.get_event_loop(lp_len=16) ntimes = 0 diff --git a/benchmarks/priority_test.py b/benchmarks/priority_test.py index cb8892c..b6a4636 100644 --- a/benchmarks/priority_test.py +++ b/benchmarks/priority_test.py @@ -5,15 +5,11 @@ # Check availability of 'priority' version import uasyncio as asyncio -p_version = True try: - if asyncio.version != 'fast_io': + if not(isinstance(asyncio.version, tuple)): raise AttributeError except AttributeError: - p_version = False - -if not p_version: - raise OSError('This program requires uasyncio fast_io version.') + raise OSError('This program requires uasyncio fast_io version V0.24 or above.') loop = asyncio.get_event_loop(lp_len=16) import asyn diff --git a/fast_io/__init__.py b/fast_io/__init__.py index 6622743..298307b 100644 --- a/fast_io/__init__.py +++ b/fast_io/__init__.py @@ -95,19 +95,34 @@ def wait(self, delay): for sock, ev in res: if ev & select.POLLOUT: cb = self.wrobjmap[id(sock)] - # Test code. Invalidate objmap: this ensures an exception is thrown - # rather than exhibiting weird behaviour when testing. - self.wrobjmap[id(sock)] = None # TEST + 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: - cb.pend_throw(None) # Ensure that, if task is cancelled, it doesn't get queued again + 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)] - self.rdobjmap[id(sock)] = None # TEST + 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. @@ -120,7 +135,10 @@ def wait(self, delay): if isinstance(cb, tuple): cb[0](*cb[1]) else: - cb.pend_throw(None) + 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) diff --git a/fast_io/core.py b/fast_io/core.py index 330a93b..bfc6e92 100644 --- a/fast_io/core.py +++ b/fast_io/core.py @@ -4,13 +4,13 @@ # 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 +# 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' +version = ('fast_io', '0.24') try: - import rtc_time as time # Low power timebase using RTC + import rtc_time as time # Low power timebase using RTC except ImportError: import utime as time import utimeq @@ -43,9 +43,10 @@ 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_overdue_ms = 0 + 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 @@ -76,8 +77,8 @@ def _call_now(self, callback, *args): # For stream I/O only def max_overdue_ms(self, t=None): if t is not None: - self._max_overdue_ms = int(t) - return self._max_overdue_ms + 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): @@ -89,6 +90,8 @@ def call_after(self, delay, 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.') @@ -111,6 +114,8 @@ 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 @@ -121,6 +126,18 @@ def wait(self, 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() @@ -129,7 +146,7 @@ def run_forever(self): to_run = False # Assume no LP task is to run t = self.lpq.peektime() tim = time.ticks_diff(t, tnow) - to_run = self._max_overdue_ms > 0 and tim < -self._max_overdue_ms + 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 @@ -138,7 +155,7 @@ def run_forever(self): to_run = time.ticks_diff(t, tnow) > 0 # No normal task is ready if to_run: self.lpq.pop(cur_task) - self.call_soon(cur_task[1], *cur_task[2]) + runq_add() while self.waitq: t = self.waitq.peektime() @@ -148,7 +165,7 @@ def run_forever(self): self.waitq.pop(cur_task) if __debug__ and DEBUG: log.debug("Moving from waitq to runq: %s", cur_task[1]) - self.call_soon(cur_task[1], *cur_task[2]) + 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. @@ -200,9 +217,10 @@ def run_forever(self): if isinstance(ret, After): delay = int(delay*1000) elif isinstance(ret, IORead): # coro was a StreamReader read method - cb.pend_throw(False) # If task is cancelled or times out, it is put on runq to process exception. - # Note if it is scheduled other than by wait - # (which does pend_throw(None) an exception (exception doesn't inherit from Exception) is thrown + 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. @@ -225,8 +243,8 @@ def run_forever(self): 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 line 195 will put the current task back on runq - # Just reschedule + 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. @@ -362,8 +380,14 @@ def __next__(self): def cancel(coro): prev = coro.pend_throw(CancelledError()) - if prev is False: # Not on runq so put it there - _event_loop.call_soon(coro) + 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: @@ -385,9 +409,14 @@ def timeout_func(timeout_obj): if __debug__ and DEBUG: log.debug("timeout_func: cancelling %s", timeout_obj.coro) prev = timeout_obj.coro.pend_throw(TimeoutError()) - #print("prev pend", prev) - if prev is False: - _event_loop.call_soon(timeout_obj.coro) + 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) diff --git a/fast_io/ms_timer_test.py b/fast_io/ms_timer_test.py index 4c6e999..5870317 100644 --- a/fast_io/ms_timer_test.py +++ b/fast_io/ms_timer_test.py @@ -17,12 +17,29 @@ async def foo(): 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_forever() + 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('Run test() to test fast I/O, test(False) to test normal I/O.') +print(s) diff --git a/fast_io/pin_cb_test.py b/fast_io/pin_cb_test.py index 69cdc9f..60dab70 100644 --- a/fast_io/pin_cb_test.py +++ b/fast_io/pin_cb_test.py @@ -34,6 +34,9 @@ async def dummy(): 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) @@ -44,10 +47,22 @@ def test(fast_io=True, latency=False): 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_forever() + 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() args: -fast_io=True test fast I/O mechanism. -latency=False test latency (delay between X1 and X3 leading edge)''') + +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.''') From 67bf35f76a88c61f68d37b0e43472c3c1645145e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 24 Feb 2019 11:27:59 +0000 Subject: [PATCH 114/472] V0.24 Fast response to cancellation and timeout. --- fast_io/fast_can_test.py | 67 ++++++++++++++++++ fast_io/iorw_can.py | 140 ++++++++++++++++++++++++++++++++++++++ fast_io/iorw_to.py | 143 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 350 insertions(+) create mode 100644 fast_io/fast_can_test.py create mode 100644 fast_io/iorw_can.py create mode 100644 fast_io/iorw_to.py diff --git a/fast_io/fast_can_test.py b/fast_io/fast_can_test.py new file mode 100644 index 0000000..1600080 --- /dev/null +++ b/fast_io/fast_can_test.py @@ -0,0 +1,67 @@ +# 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/fast_io/iorw_can.py b/fast_io/iorw_can.py new file mode 100644 index 0000000..8ef7929 --- /dev/null +++ b/fast_io/iorw_can.py @@ -0,0 +1,140 @@ +# 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/fast_io/iorw_to.py b/fast_io/iorw_to.py new file mode 100644 index 0000000..79e05fd --- /dev/null +++ b/fast_io/iorw_to.py @@ -0,0 +1,143 @@ +# 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() From 117188f78192f6fbb26177bb555fcc8602794b5a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 24 Feb 2019 12:01:03 +0000 Subject: [PATCH 115/472] Fix doc typos. --- FASTPOLL.md | 2 +- README.md | 8 ++++---- TUTORIAL.md | 11 ++++++----- UNDER_THE_HOOD.md | 9 +++++---- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 2a982f8..a5962c3 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -40,7 +40,7 @@ changes should be minimal. #### Changes incompatible with prior versions V0.24 -The `version` bound variable now retuens a 2-tuple. +The `version` bound variable now returns a 2-tuple. Prior versions. The high priority mechanism formerly provided in `asyncio_priority.py` was a diff --git a/README.md b/README.md index 2774ae6..b413e85 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,10 @@ employ stream I/O. To operate at low latency they are simply run under the has details of how to write streaming drivers. The current `fast_io` version 0.24 fixes an issue with task cancellation and -timeouts. In version 2.0, where a coroutine is waiting on a `sleep()` or on -I/O, a timeout or cancellation are deferred until the coroutine is next -scheduled. This introduces uncertainty into when the coroutine is stopped. This -issue is also addressed in Paul Sokolovsky's fork. +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. This issue is also addressed in Paul Sokolovsky's fork. ## 4.1 A Pyboard-only low power module diff --git a/TUTORIAL.md b/TUTORIAL.md index 00bd160..3df98a0 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -32,7 +32,7 @@ asyncio and includes a section for complete beginners. 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#45-exceptions-timeouts-and-cancellation) + 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) @@ -892,10 +892,11 @@ 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 provides `uasyncio` V2.4, but this requires - his [Pycopy](https://github.com/pfalcon/micropython) firmware. - * The `fast_io` fork also solves this (in a less elegant manner) and runs - under official firmware. + * [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`. diff --git a/UNDER_THE_HOOD.md b/UNDER_THE_HOOD.md index 25b9113..64a3fff 100644 --- a/UNDER_THE_HOOD.md +++ b/UNDER_THE_HOOD.md @@ -102,7 +102,8 @@ 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. +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 @@ -241,9 +242,9 @@ 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 discarded. This pure 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. +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. From 332c171b0464636f51a2aa1a2f7da4448187f8b2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 25 Feb 2019 18:42:55 +0000 Subject: [PATCH 116/472] Improved readme. --- README.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b413e85..d5fd5ea 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,24 @@ -# 1. The MicroPython uasyncio library +# 1. Asynchronous programming in MicroPython -This repository comprises the following parts. - 1. A modified [fast_io](./FASTPOLL.md) version of `uasyncio`. This is a "drop - in" replacement for the official version providing additional functionality. - 2. A module enabling the [fast_io](./FASTPOLL.md) version to run with very low - power draw. - 3. Resources for users of official or [fast_io](./FASTPOLL.md) versions: +CPython supports asynchronous programming via the `asyncio` library. +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. It also +contains an optional `fast_io` variant of `uasyncio`. + +## 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 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. + +## Resources for users of all versions * [A tutorial](./TUTORIAL.md) An introductory tutorial on asynchronous - programming and the use of the `uasyncio` library (asyncio subset). + 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 @@ -35,7 +45,7 @@ This repository comprises the following parts. # 2. Version and installation of uasyncio -Paul Sokolovsky (`uasyncio` author) has released `uasyncio` V2.4. This version +Paul Sokolovsky (`uasyncio` author) has released `uasyncio` V2.2.1. This version is on PyPi and requires his [Pycopy](https://github.com/pfalcon/micropython) fork of MicroPython firmware. His `uasyncio` code may also be found in [his fork of micropython-lib](https://github.com/pfalcon/micropython-lib). From b52b86956a638c0e9c4da7d6d3cdc69cc42d0013 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 15 Mar 2019 18:23:05 +0000 Subject: [PATCH 117/472] Add client_server example files. --- client_server/uclient.py | 47 ++++++++++++++++++++++++++ client_server/userver.py | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 client_server/uclient.py create mode 100644 client_server/userver.py diff --git a/client_server/uclient.py b/client_server/uclient.py new file mode 100644 index 0000000..bd45f77 --- /dev/null +++ b/client_server/uclient.py @@ -0,0 +1,47 @@ +# 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 +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() +try: + loop.run_until_complete(run()) +except KeyboardInterrupt: + print('Interrupted') # This mechanism doesn't work on Unix build. diff --git a/client_server/userver.py b/client_server/userver.py new file mode 100644 index 0000000..2227d91 --- /dev/null +++ b/client_server/userver.py @@ -0,0 +1,71 @@ +# 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 + +class Server: + @staticmethod + async def flash(): # ESP8266 only: demo that it is nonblocking + from machine import Pin + pin = Pin(2, Pin.OUT) + while True: + pin(not pin()) + await asyncio.sleep_ms(100) + + async def run(self, loop, port=8123, led=True): + 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 + if led: + loop.create_task(self.flash()) + 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() +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() From 5d495ed003905f517cfcd26fe8de132c43aed5c7 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 17 Mar 2019 14:27:46 +0000 Subject: [PATCH 118/472] client_server: heartbeat is now a separate file. --- TUTORIAL.md | 41 +++++++++++++++++++++++++++++++++----- client_server/heartbeat.py | 26 ++++++++++++++++++++++++ client_server/uclient.py | 4 ++++ client_server/userver.py | 15 ++++---------- 4 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 client_server/heartbeat.py diff --git a/TUTORIAL.md b/TUTORIAL.md index 3df98a0..c5d5df2 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -54,6 +54,7 @@ asyncio and includes a section for complete beginners. 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) @@ -1607,6 +1608,26 @@ I find it useful as-is but improvements are always welcome. ## 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 @@ -1616,19 +1637,29 @@ 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, at the time of writing, the ESP32 port has -issues which require rather unpleasant hacks for error-free operation. +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. -An alternative approach is to use blocking sockets with `StreamReader` and -`StreamWriter` instances to control polling. +### 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 offers a solution. +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) diff --git a/client_server/heartbeat.py b/client_server/heartbeat.py new file mode 100644 index 0000000..68a821e --- /dev/null +++ b/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/client_server/uclient.py b/client_server/uclient.py index bd45f77..cc394cd 100644 --- a/client_server/uclient.py +++ b/client_server/uclient.py @@ -6,6 +6,8 @@ import usocket as socket import uasyncio as asyncio import ujson +from heartbeat import heartbeat # Optional LED flash + server = '192.168.0.32' port = 8123 @@ -41,6 +43,8 @@ def close(): 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: diff --git a/client_server/userver.py b/client_server/userver.py index 2227d91..ec7c07c 100644 --- a/client_server/userver.py +++ b/client_server/userver.py @@ -7,17 +7,10 @@ import uasyncio as asyncio import uselect as select import ujson +from heartbeat import heartbeat # Optional LED flash class Server: - @staticmethod - async def flash(): # ESP8266 only: demo that it is nonblocking - from machine import Pin - pin = Pin(2, Pin.OUT) - while True: - pin(not pin()) - await asyncio.sleep_ms(100) - - async def run(self, loop, port=8123, led=True): + 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) @@ -28,8 +21,6 @@ async def run(self, loop, port=8123, led=True): poller = select.poll() poller.register(s_sock, select.POLLIN) client_id = 1 # For user feedback - if led: - loop.create_task(self.flash()) while True: res = poller.poll(1) # 1ms block if res: # Only s_sock is polled @@ -62,6 +53,8 @@ def close(self): 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)) From d1cf81d177c12406fda29cdf8e282d1bdf7071b2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 3 Apr 2019 16:32:03 +0100 Subject: [PATCH 119/472] lowpower: README and rtc_time.py refelect change to uasyncio.version --- lowpower/README.md | 6 +++--- lowpower/rtc_time.py | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lowpower/README.md b/lowpower/README.md index a132119..c2e019c 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -113,7 +113,7 @@ following: ```python try: - if asyncio.version != 'fast_io': + if asyncio.version[0] != 'fast_io': raise AttributeError except AttributeError: raise OSError('This requires fast_io fork of uasyncio.') @@ -161,7 +161,7 @@ Applications can detect which timebase is in use by issuing: ```python try: - if asyncio.version != 'fast_io': + if asyncio.version[0] != 'fast_io': raise AttributeError except AttributeError: raise OSError('This requires fast_io fork of uasyncio.') @@ -272,7 +272,7 @@ hardware and code. The only *required* change to application code is to add ```python try: - if asyncio.version != 'fast_io': + if asyncio.version[0] != 'fast_io': raise AttributeError except AttributeError: raise OSError('This requires fast_io fork of uasyncio.') diff --git a/lowpower/rtc_time.py b/lowpower/rtc_time.py index 1da484b..b98f014 100644 --- a/lowpower/rtc_time.py +++ b/lowpower/rtc_time.py @@ -23,7 +23,12 @@ if mode is None: # USB is disabled use_utime = False # use RTC timebase elif 'VCP' in mode: # User has enabled VCP in boot.py - if pyb.Pin.board.USB_VBUS.value() == 1: # USB physically connected + usb_conn = pyb.Pin.board.USB_VBUS.value() # USB physically connected to pyb V1.x + if not usb_conn: + usb_conn = hasattr(pyb.Pin.board, 'USB_HS_DP') and pyb.Pin.board.USB_HS_DP.value() + if not usb_conn: + usb_conn = hasattr(pyb.Pin.board, 'USB_DP') and pyb.Pin.board.USB_DP.value() + if usb_conn: print('USB connection: rtc_time disabled.') else: pyb.usb_mode(None) # Save power From ad7e508d61e406a822edfb10846460d155660b7b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 9 Apr 2019 09:37:54 +0100 Subject: [PATCH 120/472] 1st pass at Pyboard D port. --- lowpower/README.md | 36 ++++++++++--- lowpower/{lowpower.py => lp_uart.py} | 15 +++--- lowpower/lpdemo.py | 75 ++++++++++++++++++++++++++ lowpower/rtc_time.py | 79 +++++++++++++++++----------- lowpower/rtc_time_cfg.py | 2 + 5 files changed, 164 insertions(+), 43 deletions(-) rename lowpower/{lowpower.py => lp_uart.py} (75%) create mode 100644 lowpower/lpdemo.py create mode 100644 lowpower/rtc_time_cfg.py diff --git a/lowpower/README.md b/lowpower/README.md index c2e019c..b7fe054 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -1,6 +1,10 @@ # A low power usayncio adaptation -Release 0.1 25th July 2018 +Release 0.11 8th April 2019 + +API change: low power applications must now import `rtc_time_cfg` and set its +`enabled` flag. +Now supports Pyboard D. 1. [Introduction](./README.md#1-introduction) 2. [Installation](./README.md#2-installation) @@ -62,16 +66,17 @@ tested. Copy the file `rtc_time.py` to the device so that it is 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. - * `lowpower.py` Send and receive messages on UART4, echoing received messages - to UART2 at a different baudrate. This consumes about 1.4mA and serves to + * `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. -The test program `lowpower.py` requires a link between pins X1 and X2 to enable +The test program `lp_uart.py` requires a link between pins X1 and X2 to enable UART 4 to receive data via a loopback. ###### [Contents](./README.md#a-low-power-usayncio-adaptation) @@ -112,6 +117,10 @@ 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 @@ -160,6 +169,10 @@ from a separate power source for power measurements. 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 @@ -271,6 +284,10 @@ 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 @@ -377,9 +394,16 @@ 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. +`rtc_time` imports `rtc_time_cfg` and quits if `rtc_time_cfg.enabled` is +`False`. This ensures that `uasyncio` will only be affected by the `rtc_time` +module if `rtc_time` has specifically been enabled by application code. + The `rtc_time` module ensures that `uasyncio` uses `utime` for timing if the module is present in the path but is unused. This can occur because of an -active USB connection or if running on an an incompatible platform. This -ensures that under such conditions performance is unaffected. +active USB connection or if running on an an incompatible platform. + +The above precautions ensures that application behaviour and performance are +unaffected unless `rtc_time` has been enabled, a USB connection is absent, and +the hardware is a Pyboard 1.x or Pyboard D. ###### [Contents](./README.md#a-low-power-usayncio-adaptation) diff --git a/lowpower/lowpower.py b/lowpower/lp_uart.py similarity index 75% rename from lowpower/lowpower.py rename to lowpower/lp_uart.py index 2fa02f9..b0eef15 100644 --- a/lowpower/lowpower.py +++ b/lowpower/lp_uart.py @@ -1,13 +1,16 @@ -# lowpower.py Demo of using uasyncio to reduce Pyboard power consumption +# lp_uart.py Demo of using uasyncio to reduce Pyboard power consumption # Author: Peter Hinch # Copyright Peter Hinch 2018 Released under the MIT license -# The file rtc_time.py must be on the path. +# The files rtc_time.py and rtc_time_cfg.py must be on the path. # Requires a link between X1 and X2. -# Periodically sends a line on UART4 at 115200 baud. -# This is received on UART4 and re-sent on UART2 (pin X3) at 9600 baud. +# Periodically sends a line on UART4 at 9600 baud. +# This is received on UART4 and re-sent on UART1 (pin Y1) at 115200 baud. import pyb +import rtc_time_cfg +rtc_time_cfg.enabled = True # Must be done before importing uasyncio + import uasyncio as asyncio try: if asyncio.version != 'fast_io': @@ -38,8 +41,8 @@ async def receiver(uart_in, uart_out): def test(duration): if rtc_time.use_utime: # Not running in low power mode pyb.LED(3).on() - uart2 = pyb.UART(2, 9600) - uart4 = pyb.UART(4, 115200) + uart2 = pyb.UART(1, 115200) + uart4 = pyb.UART(4, 9600) # Instantiate event loop before using it in Latency class loop = asyncio.get_event_loop() lp = rtc_time.Latency(50) # ms diff --git a/lowpower/lpdemo.py b/lowpower/lpdemo.py new file mode 100644 index 0000000..a18321f --- /dev/null +++ b/lowpower/lpdemo.py @@ -0,0 +1,75 @@ +# lpdemo.py Demo/test program for MicroPython asyncio low power operation +# Author: Peter Hinch +# Copyright Peter Hinch 2018-2019 Released under the MIT license + +import rtc_time_cfg +rtc_time_cfg.enabled = True + +from pyb import LED, Pin +import aswitch +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 + +class Button(aswitch.Switch): + def __init__(self, pin): + super().__init__(pin) + self.close_func(self._sw_close) + self._flag = False + + def pressed(self): + f = self._flag + self._flag = False + return f + + def _sw_close(self): + self._flag = True + +running = False +def start(loop, leds, tims): + global running + running = True + coros = [] + # Demo: assume app requires higher speed (not true in this instance) + rtc_time.Latency().value(50) + # Here you might apply power to external hardware + for x, led in enumerate(leds): # Create a coroutine for each LED + coros.append(toggle(led, tims[x])) + loop.create_task(coros[-1]) + return coros + +def stop(leds, coros): + global running + running = False + while coros: + asyncio.cancel(coros.pop()) + # Remove power from external hardware + for led in leds: + led.off() + rtc_time.Latency().value(200) # Slow down scheduler to conserve power + +async def monitor(loop, button): + leds = [LED(x) for x in (1, 2, 3)] # Create list of LED's and times + tims = [200, 700, 1200] + coros = start(loop, leds, tims) + while True: + if button.pressed(): + if running: + stop(leds, coros) + else: + coros = start(loop, leds, tims) + await asyncio.sleep_ms(0) + +async def toggle(objLED, time_ms): + while True: + await asyncio.sleep_ms(time_ms) + objLED.toggle() + +loop = asyncio.get_event_loop() +button = Button(Pin('X1', Pin.IN, Pin.PULL_UP)) +loop.create_task(monitor(loop, button)) +loop.run_forever() diff --git a/lowpower/rtc_time.py b/lowpower/rtc_time.py index b98f014..5f8e472 100644 --- a/lowpower/rtc_time.py +++ b/lowpower/rtc_time.py @@ -11,23 +11,28 @@ import sys import utime +from os import uname +from rtc_time_cfg import enabled +if not enabled: + print('rtc_time module has not been enabled.') + sys.exit(0) _PERIOD = const(604800000) # ms in 7 days _PERIOD_2 = const(302400000) # half period _SS_TO_MS = 1000/256 # Subsecs to ms - +d_series = uname().machine[:5] == 'PYBD_' use_utime = True # Assume the normal utime timebase + if sys.platform == 'pyboard': import pyb mode = pyb.usb_mode() if mode is None: # USB is disabled use_utime = False # use RTC timebase elif 'VCP' in mode: # User has enabled VCP in boot.py - usb_conn = pyb.Pin.board.USB_VBUS.value() # USB physically connected to pyb V1.x - if not usb_conn: - usb_conn = hasattr(pyb.Pin.board, 'USB_HS_DP') and pyb.Pin.board.USB_HS_DP.value() - if not usb_conn: - usb_conn = hasattr(pyb.Pin.board, 'USB_DP') and pyb.Pin.board.USB_DP.value() + if d_series: # Detect an active connection to the PC + usb_conn = pyb.USB_VCP().isconnected() + else: + usb_conn = pyb.Pin.board.USB_VBUS.value() # USB physically connected to pyb V1.x if usb_conn: print('USB connection: rtc_time disabled.') else: @@ -38,10 +43,44 @@ # For lowest power consumption set unused pins as inputs with pullups. # Note the 4K7 I2C pullups on X9 X10 Y9 Y10. -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) +if d_series: + print('Running on Pyboard D') # Investigate which pins we can do this to TODO +else: + print('Running on Pyboard 1.x') + 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) # User code redefines any pins in use +# sleep_ms is defined to stop things breaking if someone imports uasyncio.core +# Power won't be saved if this is done. +sleep_ms = utime.sleep_ms +if use_utime: # Run utime: Pyboard connected to PC via USB or alien platform + ticks_ms = utime.ticks_ms + ticks_add = utime.ticks_add + ticks_diff = utime.ticks_diff +else: + rtc = pyb.RTC() + # dt: (year, month, day, weekday, hours, minutes, seconds, subseconds) + # weekday is 1-7 for Monday through Sunday. + if d_series: + # Subseconds are μs + def ticks_ms(): + dt = rtc.datetime() + return ((dt[3] - 1)*86400000 + dt[4]*3600000 + dt[5]*60000 + dt[6]*1000 + + int(dt[7] / 1000)) + else: + # subseconds counts down from 255 to 0 + def ticks_ms(): + dt = rtc.datetime() + return ((dt[3] - 1)*86400000 + dt[4]*3600000 + dt[5]*60000 + dt[6]*1000 + + int(_SS_TO_MS * (255 - dt[7]))) + + def ticks_add(a, b): + return (a + b) % _PERIOD + + def ticks_diff(end, start): + return ((end - start + _PERIOD_2) % _PERIOD) - _PERIOD_2 + import uasyncio as asyncio # Common version has a needless dict: https://www.python.org/dev/peps/pep-0318/#examples @@ -70,6 +109,7 @@ def __init__(self, t_ms=100): raise OSError('Event loop not instantiated.') def _run(self): + print('Low power mode is ON.') rtc = pyb.RTC() rtc.wakeup(self._t_ms) t_ms = self._t_ms @@ -88,26 +128,3 @@ def value(self, val=None): if val is not None and not use_utime: self._t_ms = max(val, 0) return self._t_ms - -# sleep_ms is defined to stop things breaking if someone imports uasyncio.core -# Power won't be saved if this is done. -sleep_ms = utime.sleep_ms -if use_utime: # Run utime: Pyboard connected to PC via USB or alien platform - ticks_ms = utime.ticks_ms - ticks_add = utime.ticks_add - ticks_diff = utime.ticks_diff -else: - rtc = pyb.RTC() - # dt: (year, month, day, weekday, hours, minutes, seconds, subseconds) - # weekday is 1-7 for Monday through Sunday. - # subseconds counts down from 255 to 0 - def ticks_ms(): - dt = rtc.datetime() - return ((dt[3] - 1)*86400000 + dt[4]*3600000 + dt[5]*60000 + dt[6]*1000 + - int(_SS_TO_MS * (255 - dt[7]))) - - def ticks_add(a, b): - return (a + b) % _PERIOD - - def ticks_diff(end, start): - return ((end - start + _PERIOD_2) % _PERIOD) - _PERIOD_2 diff --git a/lowpower/rtc_time_cfg.py b/lowpower/rtc_time_cfg.py new file mode 100644 index 0000000..d4484e3 --- /dev/null +++ b/lowpower/rtc_time_cfg.py @@ -0,0 +1,2 @@ +# rtc_time_cfg.py +enabled = False From c9b68b8ea0216e44cda638d2c1dfef36cc97dac2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 9 Apr 2019 13:04:53 +0100 Subject: [PATCH 121/472] Low power variant supports Pyboard D. --- FASTPOLL.md | 3 ++- README.md | 2 +- lowpower/README.md | 20 ++++++++++++++------ lowpower/lp_uart.py | 2 +- lowpower/rtc_time.py | 2 ++ 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index a5962c3..9452d9f 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -19,7 +19,8 @@ This version has the following features relative to official V2.0: 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). + [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 diff --git a/README.md b/README.md index d5fd5ea..b4eb45a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This comprises two parts. replacement for the official version 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. + to run with very low power draw. This is Pyboard-only including Pyboard D. ## Resources for users of all versions diff --git a/lowpower/README.md b/lowpower/README.md index b7fe054..7c03d5d 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -1,6 +1,6 @@ -# A low power usayncio adaptation +# A low power usayncio adaptation for Pyboards -Release 0.11 8th April 2019 +Release 0.11 9th April 2019 API change: low power applications must now import `rtc_time_cfg` and set its `enabled` flag. @@ -15,8 +15,9 @@ Now supports Pyboard D. 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](./README.md#322-measured-results) - 3.2.3 [Current waveforms](./README.md#323-current-waveforms) + 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) 5. [Application design](./README.md#5-application-design) 5.1 [Hardware](./README.md#51-hardware) @@ -190,7 +191,7 @@ adaptor. ###### [Contents](./README.md#a-low-power-usayncio-adaptation) -### 3.2.2 Measured results +### 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. @@ -219,7 +220,7 @@ 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 +### 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. @@ -233,6 +234,13 @@ Vertical 20mA/div Horizontal 500μs/div ![Image](./current1.png) +### 3.2.4 Pyboard D measurements + +As of this release the two demo applications consume around 3.3mA. This is high +because the 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 diff --git a/lowpower/lp_uart.py b/lowpower/lp_uart.py index b0eef15..ecf6a0c 100644 --- a/lowpower/lp_uart.py +++ b/lowpower/lp_uart.py @@ -13,7 +13,7 @@ import uasyncio as asyncio try: - if asyncio.version != 'fast_io': + if asyncio.version[0] != 'fast_io': raise AttributeError except AttributeError: raise OSError('This requires fast_io fork of uasyncio.') diff --git a/lowpower/rtc_time.py b/lowpower/rtc_time.py index 5f8e472..2750a73 100644 --- a/lowpower/rtc_time.py +++ b/lowpower/rtc_time.py @@ -45,6 +45,8 @@ # Note the 4K7 I2C pullups on X9 X10 Y9 Y10. if d_series: print('Running on Pyboard D') # Investigate which pins we can do this to TODO + #for pin in [p for p in dir(pyb.Pin.board) if p[0] in 'XYW']: + #pin_x = pyb.Pin(pin, pyb.Pin.IN, pyb.Pin.PULL_UP) else: print('Running on Pyboard 1.x') for pin in [p for p in dir(pyb.Pin.board) if p[0] in 'XY']: From 6873c0a50997ea32214c5593337fb0d492628b1b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 9 Apr 2019 13:08:36 +0100 Subject: [PATCH 122/472] Low power variant supports Pyboard D. --- lowpower/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lowpower/README.md b/lowpower/README.md index 7c03d5d..5c96d8f 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -1,5 +1,6 @@ -# A low power usayncio adaptation for Pyboards +# A low power usayncio adaptation +This is specific to Pyboards including the D series. Release 0.11 9th April 2019 API change: low power applications must now import `rtc_time_cfg` and set its From 42c18c9f46b32a700f3dc8f19d84d507ec0ef3f2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 9 Apr 2019 13:43:51 +0100 Subject: [PATCH 123/472] Low power variant supports Pyboard D. --- lowpower/README.md | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/lowpower/README.md b/lowpower/README.md index 5c96d8f..46c1b33 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -1,11 +1,10 @@ # A low power usayncio adaptation -This is specific to Pyboards including the D series. Release 0.11 9th April 2019 API change: low power applications must now import `rtc_time_cfg` and set its `enabled` flag. -Now supports Pyboard D. +This is specific to Pyboards including the D series. 1. [Introduction](./README.md#1-introduction) 2. [Installation](./README.md#2-installation) @@ -146,9 +145,10 @@ scheduler is again paused (if `latency` > 0). #### 3.2.1.1 Timing Accuracy and rollover -A minor limitation is that the Pyboard RTC cannot resolve times of less than -4ms so there is a theoretical reduction in the accuracy of delays. In practice, -as explained in the [tutorial](../TUTORIAL.md), issuing +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. In practice this is somewhat academic. As +explained in the [tutorial](../TUTORIAL.md), issuing ```python await asyncio.sleep_ms(t) @@ -248,9 +248,10 @@ comparable to Pyboard 1.x. This provides the following. -Variable: +Variables (treat as read-only): * `use_utime` `True` if the `uasyncio` timebase is `utime`, `False` if it is - the RTC. Treat as read-only. + 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` @@ -403,16 +404,18 @@ 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. -`rtc_time` imports `rtc_time_cfg` and quits if `rtc_time_cfg.enabled` is -`False`. This ensures that `uasyncio` will only be affected by the `rtc_time` -module if `rtc_time` has specifically been enabled by application code. - -The `rtc_time` module ensures that `uasyncio` uses `utime` for timing if the -module is present in the path but is unused. This can occur because of an -active USB connection or if running on an an incompatible platform. - -The above precautions ensures that application behaviour and performance are -unaffected unless `rtc_time` has been enabled, a USB connection is absent, and -the hardware is a Pyboard 1.x or Pyboard D. +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) From eafc9767c2705bdb9523e833c68864f2cbb6209b Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 9 Apr 2019 17:13:31 +0100 Subject: [PATCH 124/472] lowpower: improve USB detection and README.md --- lowpower/README.md | 23 +++++++++++++++++++---- lowpower/rtc_time.py | 9 +++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/lowpower/README.md b/lowpower/README.md index 46c1b33..4f7b7c9 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -139,6 +139,14 @@ before yielding with a zero delay. The duration of the `stop` condition full speed. The `yield` allows each pending task to run once before the scheduler is again paused (if `latency` > 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 @@ -147,15 +155,15 @@ scheduler is again paused (if `latency` > 0). 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. In practice this is somewhat academic. As -explained in the [tutorial](../TUTORIAL.md), issuing +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. +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). @@ -166,7 +174,14 @@ 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. +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: diff --git a/lowpower/rtc_time.py b/lowpower/rtc_time.py index 2750a73..05e6457 100644 --- a/lowpower/rtc_time.py +++ b/lowpower/rtc_time.py @@ -1,6 +1,6 @@ # rtc_time.py Pyboard-only RTC based timing for low power uasyncio # Author: Peter Hinch -# Copyright Peter Hinch 2018 Released under the MIT license +# Copyright Peter Hinch 2018-2019 Released under the MIT license # Code based on extmod/utime_mphal.c # millisecs roll over on 7 days rather than 12.42757 days @@ -29,11 +29,8 @@ if mode is None: # USB is disabled use_utime = False # use RTC timebase elif 'VCP' in mode: # User has enabled VCP in boot.py - if d_series: # Detect an active connection to the PC - usb_conn = pyb.USB_VCP().isconnected() - else: - usb_conn = pyb.Pin.board.USB_VBUS.value() # USB physically connected to pyb V1.x - if usb_conn: + # Detect an active connection (not just a power source) + if pyb.USB_VCP().isconnected(): # USB will work normally print('USB connection: rtc_time disabled.') else: pyb.usb_mode(None) # Save power From 8812447fb1bebedb9d9da0efc9333aecfc4dee26 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 11 Apr 2019 18:13:32 +0100 Subject: [PATCH 125/472] Prior to Damiens low power fix for D series --- lowpower/README.md | 30 +++++++++++++++-------------- lowpower/rtc_time.py | 46 +++++++++++++++++++++++++++----------------- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/lowpower/README.md b/lowpower/README.md index 4f7b7c9..3ced654 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -1,10 +1,12 @@ # A low power usayncio adaptation -Release 0.11 9th April 2019 +Release 0.11 11th April 2019 -API change: low power applications must now import `rtc_time_cfg` and set its +API changes: low power applications must now import `rtc_time_cfg` and set its `enabled` flag. -This is specific to Pyboards including the D series. +`Latency` class: Constructor requires event loop arg. + +This module is specific to Pyboards including the D series. 1. [Introduction](./README.md#1-introduction) 2. [Installation](./README.md#2-installation) @@ -252,10 +254,10 @@ Horizontal 500μs/div ### 3.2.4 Pyboard D measurements -As of this release the two demo applications consume around 3.3mA. This is high -because the 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. +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) @@ -283,12 +285,12 @@ reason is explained in the code comments. It is recommended to use the `utime` method explicitly if needed. Latency Class: - * Constructor: Positional arg `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. 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. + * 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 @@ -298,7 +300,7 @@ 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 -rtc_time.Latency().value(t) +rtc_time.Latency(t) ``` ###### [Contents](./README.md#a-low-power-usayncio-adaptation) diff --git a/lowpower/rtc_time.py b/lowpower/rtc_time.py index 05e6457..e830716 100644 --- a/lowpower/rtc_time.py +++ b/lowpower/rtc_time.py @@ -14,12 +14,12 @@ from os import uname from rtc_time_cfg import enabled if not enabled: - print('rtc_time module has not been enabled.') - sys.exit(0) + raise ImportError('rtc_time is not enabled.') + +# sleep_ms is defined to stop things breaking if someone imports uasyncio.core +# Power won't be saved if this is done. +sleep_ms = utime.sleep_ms -_PERIOD = const(604800000) # ms in 7 days -_PERIOD_2 = const(302400000) # half period -_SS_TO_MS = 1000/256 # Subsecs to ms d_series = uname().machine[:5] == 'PYBD_' use_utime = True # Assume the normal utime timebase @@ -39,25 +39,37 @@ raise OSError('rtc_time.py is Pyboard-specific.') # For lowest power consumption set unused pins as inputs with pullups. -# Note the 4K7 I2C pullups on X9 X10 Y9 Y10. +# Note the 4K7 I2C pullups on X9 X10 Y9 Y10 (Pyboard 1.x). if d_series: print('Running on Pyboard D') # Investigate which pins we can do this to TODO - #for pin in [p for p in dir(pyb.Pin.board) if p[0] in 'XYW']: - #pin_x = pyb.Pin(pin, pyb.Pin.IN, pyb.Pin.PULL_UP) +# pinlist = [p for p in dir(pyb.Pin.board) if p.startswith('W') and p[1].isdigit() and p[-1].isdigit()] +# sorted(pinlist, key=lambda s: int(s[1:])) + #pinlist = ['W3', 'W5', 'W6', 'W7', 'W8', 'W9', 'W10', 'W11', 'W12', 'W14', 'W15', + #'W16', 'W17', 'W18', 'W19', 'W20', 'W22', 'W23', 'W24', 'W25', + #'W26', 'W27', 'W28', 'W29', 'W30', 'W32', 'W33', 'W34', 'W43', 'W45', + #'W46', 'W47', 'W49', 'W50', 'W51', 'W52', 'W53', 'W54', 'W55', 'W56', + #'W57', 'W58', 'W59', 'W60', 'W61', 'W62', 'W63', 'W64', 'W65', 'W66', + #'W67', 'W68', 'W70', 'W71', 'W72', 'W73', 'W74'] + # sorted([p for p in dir(pyb.Pin.board) if p[0] in 'XY' and p[-1].isdigit()], key=lambda x: int(x[1:]) if x[0]=='X' else int(x[1:])+100) + pinlist = ['X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8', 'X9', 'X10', 'X11', 'X12', + 'Y3', 'Y4', 'Y5', 'Y6', 'Y7', 'Y8', 'Y9', 'Y10', 'Y11', 'Y12'] + for pin in pinlist: + pin_x = pyb.Pin(pin, pyb.Pin.IN, pyb.Pin.PULL_UP) + pyb.Pin('EN_3V3').off() else: print('Running on Pyboard 1.x') 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) # User code redefines any pins in use -# sleep_ms is defined to stop things breaking if someone imports uasyncio.core -# Power won't be saved if this is done. -sleep_ms = utime.sleep_ms -if use_utime: # Run utime: Pyboard connected to PC via USB or alien platform +if use_utime: ticks_ms = utime.ticks_ms ticks_add = utime.ticks_add ticks_diff = utime.ticks_diff -else: +else: # All conditions met for low power operation + _PERIOD = const(604800000) # ms in 7 days + _PERIOD_2 = const(302400000) # half period + _SS_TO_MS = 1000/256 # Subsecs to ms rtc = pyb.RTC() # dt: (year, month, day, weekday, hours, minutes, seconds, subseconds) # weekday is 1-7 for Monday through Sunday. @@ -82,9 +94,6 @@ def ticks_diff(end, start): import uasyncio as asyncio -# Common version has a needless dict: https://www.python.org/dev/peps/pep-0318/#examples -# https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python -# Resolved: https://forum.micropython.org/viewtopic.php?f=2&t=5033&p=28824#p28824 def singleton(cls): instance = None def getinstance(*args, **kwargs): @@ -124,6 +133,7 @@ def _run(self): rtc.wakeup(None) def value(self, val=None): - if val is not None and not use_utime: + v = self._t_ms + if val is not None: self._t_ms = max(val, 0) - return self._t_ms + return v From f389189a0f7adf98f491829b06adf4de0be9747f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 14 Apr 2019 16:23:06 +0100 Subject: [PATCH 126/472] lowpower: add Pyboard D pin configuration. --- lowpower/rtc_time.py | 55 +++++++++++++++++++++++++++------------- lowpower/rtc_time_cfg.py | 2 ++ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/lowpower/rtc_time.py b/lowpower/rtc_time.py index e830716..c1036ff 100644 --- a/lowpower/rtc_time.py +++ b/lowpower/rtc_time.py @@ -12,8 +12,9 @@ import sys import utime from os import uname -from rtc_time_cfg import enabled -if not enabled: +from rtc_time_cfg import enabled, disable_3v3, disable_leds + +if not enabled: # uasyncio traps this and uses utime raise ImportError('rtc_time is not enabled.') # sleep_ms is defined to stop things breaking if someone imports uasyncio.core @@ -41,21 +42,41 @@ # For lowest power consumption set unused pins as inputs with pullups. # Note the 4K7 I2C pullups on X9 X10 Y9 Y10 (Pyboard 1.x). if d_series: - print('Running on Pyboard D') # Investigate which pins we can do this to TODO -# pinlist = [p for p in dir(pyb.Pin.board) if p.startswith('W') and p[1].isdigit() and p[-1].isdigit()] -# sorted(pinlist, key=lambda s: int(s[1:])) - #pinlist = ['W3', 'W5', 'W6', 'W7', 'W8', 'W9', 'W10', 'W11', 'W12', 'W14', 'W15', - #'W16', 'W17', 'W18', 'W19', 'W20', 'W22', 'W23', 'W24', 'W25', - #'W26', 'W27', 'W28', 'W29', 'W30', 'W32', 'W33', 'W34', 'W43', 'W45', - #'W46', 'W47', 'W49', 'W50', 'W51', 'W52', 'W53', 'W54', 'W55', 'W56', - #'W57', 'W58', 'W59', 'W60', 'W61', 'W62', 'W63', 'W64', 'W65', 'W66', - #'W67', 'W68', 'W70', 'W71', 'W72', 'W73', 'W74'] - # sorted([p for p in dir(pyb.Pin.board) if p[0] in 'XY' and p[-1].isdigit()], key=lambda x: int(x[1:]) if x[0]=='X' else int(x[1:])+100) - pinlist = ['X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8', 'X9', 'X10', 'X11', 'X12', - 'Y3', 'Y4', 'Y5', 'Y6', 'Y7', 'Y8', 'Y9', 'Y10', 'Y11', 'Y12'] - for pin in pinlist: - pin_x = pyb.Pin(pin, pyb.Pin.IN, pyb.Pin.PULL_UP) - pyb.Pin('EN_3V3').off() + print('Running on Pyboard D') + if not use_utime: + def low_power_pins(): + pins = [ + # user IO pins + 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'A10', 'A11', 'A12', 'A13', 'A14', 'A15', + 'B0', 'B1', 'B3', 'B4', 'B5', 'B7', 'B8', 'B9', 'B10', 'B11', 'B12', 'B13', + 'C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', + 'D0', 'D3', 'D8', 'D9', + 'E0', 'E1', 'E12', 'E14', 'E15', + 'F1', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F13', 'F14', 'F15', + 'H2', 'H3', 'H5', 'H6', 'H7', 'H8', + 'I0', 'I1', + + # internal pins + 'D1', 'D14', 'D15', + 'F0', 'F12', + 'G0', 'G1', 'G2', 'G3', 'G4', 'G5', #'G6', + 'H4', 'H9', 'H10', 'H11', 'H12', 'H13', 'H14', 'H15', + 'I2', 'I3', + ] + pins_led = ['F3', 'F4', 'F5',] + pins_sdmmc = ['D6', 'D7', 'G9', 'G10', 'G11', 'G12'] + pins_wlan = ['D2', 'D4', 'I7', 'I8', 'I9', 'I11'] + pins_bt = ['D5', 'D10', 'E3', 'E4', 'E5', 'E6', 'G8', 'G13', 'G14', 'G15', 'I4', 'I5', 'I6', 'I10'] + pins_qspi1 = ['B2', 'B6', 'D11', 'D12', 'D13', 'E2'] + pins_qspi2 = ['E7', 'E8', 'E9', 'E10', 'E11', 'E13'] + for p in pins: + pyb.Pin(p, pyb.Pin.IN, pyb.Pin.PULL_DOWN) + if disable_3v3: + pyb.Pin('EN_3V3', pyb.Pin.IN, None) + if disable_leds: + for p in pins_led: + pyb.Pin(p, pyb.Pin.IN, pyb.Pin.PULL_UP) + low_power_pins() else: print('Running on Pyboard 1.x') for pin in [p for p in dir(pyb.Pin.board) if p[0] in 'XY']: diff --git a/lowpower/rtc_time_cfg.py b/lowpower/rtc_time_cfg.py index d4484e3..be3e346 100644 --- a/lowpower/rtc_time_cfg.py +++ b/lowpower/rtc_time_cfg.py @@ -1,2 +1,4 @@ # rtc_time_cfg.py enabled = False +disable_3v3 = False +disable_leds = False From f6152e38e6bd0c4c4af1c4ed1044439da9fe344f Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 17 Apr 2019 08:53:50 +0100 Subject: [PATCH 127/472] lowpower Latency is functor. --- lowpower/README.md | 14 +++++++------- lowpower/lp_uart.py | 6 +++--- lowpower/lpdemo.py | 6 +++--- lowpower/rtc_time.py | 20 +++++++++++--------- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/lowpower/README.md b/lowpower/README.md index 3ced654..f865470 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -1,10 +1,10 @@ # A low power usayncio adaptation -Release 0.11 11th April 2019 +Release 0.12 15th April 2019 API changes: low power applications must now import `rtc_time_cfg` and set its `enabled` flag. -`Latency` class: Constructor requires event loop arg. +`Latency` is now a functor rather than a class. This module is specific to Pyboards including the D series. @@ -129,10 +129,10 @@ try: raise AttributeError except AttributeError: raise OSError('This requires fast_io fork of uasyncio.') -import rtc_time +from rtc_time import Latency # Instantiate event loop with any args before running code that uses it loop = asyncio.get_event_loop() -rtc_time.Latency(100) # Define latency in ms +Latency(100) # Define latency in ms ``` The `Latency` class has a continuously running loop that executes `pyb.stop` @@ -300,7 +300,7 @@ 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 -rtc_time.Latency(t) +Latency(t) ``` ###### [Contents](./README.md#a-low-power-usayncio-adaptation) @@ -321,10 +321,10 @@ try: except AttributeError: raise OSError('This requires fast_io fork of uasyncio.') # Do this import before configuring any pins or I/O: -import rtc_time +from rtc_time import Latency # Instantiate event loop with any args before running code that uses it: loop = asyncio.get_event_loop() -lp = rtc_time.Latency(100) # Define latency in ms +Latency(100) # Define latency in ms # Run application code ``` diff --git a/lowpower/lp_uart.py b/lowpower/lp_uart.py index ecf6a0c..42cc586 100644 --- a/lowpower/lp_uart.py +++ b/lowpower/lp_uart.py @@ -17,7 +17,7 @@ raise AttributeError except AttributeError: raise OSError('This requires fast_io fork of uasyncio.') -import rtc_time +from rtc_time import Latency, use_utime # Stop the test after a period async def killer(duration): @@ -39,13 +39,13 @@ async def receiver(uart_in, uart_out): await swriter.awrite(res) def test(duration): - if rtc_time.use_utime: # Not running in low power mode + if use_utime: # Not running in low power mode pyb.LED(3).on() uart2 = pyb.UART(1, 115200) uart4 = pyb.UART(4, 9600) # Instantiate event loop before using it in Latency class loop = asyncio.get_event_loop() - lp = rtc_time.Latency(50) # ms + lp = Latency(50) # ms loop.create_task(sender(uart4)) loop.create_task(receiver(uart4, uart2)) loop.run_until_complete(killer(duration)) diff --git a/lowpower/lpdemo.py b/lowpower/lpdemo.py index a18321f..1f68631 100644 --- a/lowpower/lpdemo.py +++ b/lowpower/lpdemo.py @@ -13,7 +13,7 @@ raise AttributeError except AttributeError: raise OSError('This requires fast_io fork of uasyncio.') -import rtc_time +from rtc_time import Latency class Button(aswitch.Switch): def __init__(self, pin): @@ -35,7 +35,7 @@ def start(loop, leds, tims): running = True coros = [] # Demo: assume app requires higher speed (not true in this instance) - rtc_time.Latency().value(50) + Latency(50) # Here you might apply power to external hardware for x, led in enumerate(leds): # Create a coroutine for each LED coros.append(toggle(led, tims[x])) @@ -50,7 +50,7 @@ def stop(leds, coros): # Remove power from external hardware for led in leds: led.off() - rtc_time.Latency().value(200) # Slow down scheduler to conserve power + Latency(200) # Slow down scheduler to conserve power async def monitor(loop, button): leds = [LED(x) for x in (1, 2, 3)] # Create list of LED's and times diff --git a/lowpower/rtc_time.py b/lowpower/rtc_time.py index c1036ff..dfc4a38 100644 --- a/lowpower/rtc_time.py +++ b/lowpower/rtc_time.py @@ -115,23 +115,24 @@ def ticks_diff(end, start): import uasyncio as asyncio -def singleton(cls): +def functor(cls): instance = None def getinstance(*args, **kwargs): nonlocal instance if instance is None: instance = cls(*args, **kwargs) - return instance + return instance + return instance(*args, **kwargs) return getinstance -@singleton -class Latency(): +@functor +class Latency: def __init__(self, t_ms=100): if use_utime: # Not in low power mode: t_ms stays zero self._t_ms = 0 else: if asyncio.got_event_loop(): - self._t_ms = t_ms + self._t_ms = max(t_ms, 0) loop = asyncio.get_event_loop() loop.create_task(self._run()) else: @@ -145,16 +146,17 @@ def _run(self): while True: if t_ms > 0: pyb.stop() + # Pending tasks run once, may change self._t_ms yield - if t_ms != self._t_ms: + 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 value(self, val=None): + def __call__(self, t_ms=None): v = self._t_ms - if val is not None: - self._t_ms = max(val, 0) + if t_ms is not None: + self._t_ms = max(t_ms, 0) return v From 7b802c919ef0cd55dda8d402d07a7735af8e7819 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 23 Apr 2019 18:19:34 +0100 Subject: [PATCH 128/472] lowpower: add mqtt_log.py, improve README. --- lowpower/README.md | 63 +++++++++++++++++++++++++++++++++---------- lowpower/mqtt_log.py | 64 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 lowpower/mqtt_log.py diff --git a/lowpower/README.md b/lowpower/README.md index f865470..3c95d30 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -1,6 +1,6 @@ # A low power usayncio adaptation -Release 0.12 15th April 2019 +Release 0.12 23rd April 2019 API changes: low power applications must now import `rtc_time_cfg` and set its `enabled` flag. @@ -75,12 +75,27 @@ tested. Copy the file `rtc_time.py` to the device so that it is on `sys.path`. 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. + 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. -The test program `lp_uart.py` requires a link between pins X1 and X2 to enable -UART 4 to receive data via a loopback. +`mqtt_log.py` requires the `umqtt.simple` library. This may be installed with +[upip_m](https://github.com/peterhinch/micropython-samples/tree/master/micropip). +``` +>>> upip_m.install('micropython-umqtt.simple') +``` +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) @@ -135,11 +150,14 @@ loop = asyncio.get_event_loop() Latency(100) # Define latency in ms ``` -The `Latency` class has a continuously running loop that executes `pyb.stop` -before yielding with a zero delay. The duration of the `stop` condition -(`latency`) can be dynamically varied. If zeroed the scheduler will run at -full speed. The `yield` allows each pending task to run once before the -scheduler is again paused (if `latency` > 0). +`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 @@ -392,16 +410,33 @@ async def bar(): the 10ms sleep will be >=200ms dependent on other application tasks. -Latency may be changed dynamically by using the `value` method of the `Latency` -instance. A typical application (as in `lpdemo.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. +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. diff --git a/lowpower/mqtt_log.py b/lowpower/mqtt_log.py new file mode 100644 index 0000000..fb2737d --- /dev/null +++ b/lowpower/mqtt_log.py @@ -0,0 +1,64 @@ +# mqtt_log.py Demo/test program for MicroPython asyncio low power operation +# Author: Peter Hinch +# Copyright Peter Hinch 2019 Released under the MIT license + +# MQTT Demo publishes an incremental count and the RTC time periodically. +# On my SF_2W board consumption while paused was 170μA. + +# Test reception e.g. with: +# mosquitto_sub -h 192.168.0.33 -t result + +SERVER = '192.168.0.33' # *** Adapt for local conditions *** +SSID = 'misspiggy' +PW = '6163VMiqSTyx' + +import rtc_time_cfg +rtc_time_cfg.enabled = True + +from pyb import LED, RTC +from umqtt.simple import MQTTClient +import network +import ujson + +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 + +def publish(s): + c = MQTTClient('umqtt_client', SERVER) + c.connect() + c.publish(b'result', s.encode('UTF8')) + c.disconnect() + +async def main(loop): + rtc = RTC() + red = LED(1) + red.on() + grn = LED(2) + sta_if = network.WLAN() + sta_if.active(True) + sta_if.connect(SSID, PW) + while sta_if.status() == 1: + await asyncio.sleep(1) + grn.toggle() + if sta_if.isconnected(): + red.off() + grn.on() + await asyncio.sleep(1) # 1s of green == success. + grn.off() # Conserve power + Latency(2000) + count = 0 + while True: + publish(ujson.dumps([count, rtc.datetime()])) + count += 1 + await asyncio.sleep(120) # 2 mins + else: # Fail to connect + red.on() + grn.off() + +loop = asyncio.get_event_loop() +loop.run_until_complete(main(loop)) From 16e66f717df2432588e7d8be6b0b8e4c54e1f876 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 16 Jun 2019 07:22:55 +0100 Subject: [PATCH 129/472] fast_io: StreamReader.readinto method added. --- fast_io/__init__.py | 7 +++++++ lowpower/mqtt_log.py | 9 +++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/fast_io/__init__.py b/fast_io/__init__.py index 298307b..8cdf198 100644 --- a/fast_io/__init__.py +++ b/fast_io/__init__.py @@ -164,6 +164,13 @@ def read(self, n=-1): # PollEventLoop._unregister return res # Next iteration raises StopIteration and returns result + def readinto(self, buf, n=-1): # Experimental and not yet tested TODO + yield IORead(self.polls) + res = self.ios.readinto(buf, n) + assert res, 'zero bytes returned' # Temporary + yield IOReadDone(self.polls) + return res + def readexactly(self, n): buf = b"" while n: diff --git a/lowpower/mqtt_log.py b/lowpower/mqtt_log.py index fb2737d..1337560 100644 --- a/lowpower/mqtt_log.py +++ b/lowpower/mqtt_log.py @@ -6,11 +6,7 @@ # On my SF_2W board consumption while paused was 170μA. # Test reception e.g. with: -# mosquitto_sub -h 192.168.0.33 -t result - -SERVER = '192.168.0.33' # *** Adapt for local conditions *** -SSID = 'misspiggy' -PW = '6163VMiqSTyx' +# mosquitto_sub -h 192.168.0.10 -t result import rtc_time_cfg rtc_time_cfg.enabled = True @@ -19,6 +15,7 @@ from umqtt.simple import MQTTClient import network import ujson +from local import SERVER, SSID, PW # Local configuration: change this file import uasyncio as asyncio try: @@ -42,7 +39,7 @@ async def main(loop): sta_if = network.WLAN() sta_if.active(True) sta_if.connect(SSID, PW) - while sta_if.status() == 1: + while sta_if.status() in (1, 2): # https://github.com/micropython/micropython/issues/4682 await asyncio.sleep(1) grn.toggle() if sta_if.isconnected(): From e3787f0e0bd75585203a2d5be0d784a8dbca36a8 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 17 Jun 2019 08:31:42 +0100 Subject: [PATCH 130/472] fast_io StreamReader.readinto improvement. --- fast_io/__init__.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/fast_io/__init__.py b/fast_io/__init__.py index 8cdf198..5f05dff 100644 --- a/fast_io/__init__.py +++ b/fast_io/__init__.py @@ -164,12 +164,29 @@ def read(self, n=-1): # PollEventLoop._unregister return res # Next iteration raises StopIteration and returns result - def readinto(self, buf, n=-1): # Experimental and not yet tested TODO - yield IORead(self.polls) - res = self.ios.readinto(buf, n) - assert res, 'zero bytes returned' # Temporary - yield IOReadDone(self.polls) - return res + def readinto(self, buf, n=0): # Experimental and not yet tested TODO + if DEBUG and __debug__: + log.debug("StreamReader.readinto() START") + + while True: + yield IORead(self.polls) + if DEBUG and __debug__: + log.debug("StreamReader.readinto() ... just after IORead") + if n: + res = self.ios.readinto(buf, n) # Call the device's readinto method + else: + res = self.ios.readinto(buf) + 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 + if DEBUG and __debug__: + log.debug("StreamReader.readinto() ... just after IOReadDone") + # This de-registers device as a read device with poll via + # PollEventLoop._unregister + return res # Next iteration raises StopIteration and returns result def readexactly(self, n): buf = b"" From 7580e7bfd22145a7b7a121d4bbef567434a8e111 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 18 Jun 2019 08:04:10 +0100 Subject: [PATCH 131/472] StreamReader.readinto: document, strip debug and repeated comments. --- FASTPOLL.md | 36 +++++++++++++++++++++++++++++++++++- fast_io/__init__.py | 19 ++++--------------- fast_io/core.py | 2 +- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 9452d9f..9aca084 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -30,6 +30,8 @@ This version has the following features relative to official V2.0: * `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 @@ -64,6 +66,9 @@ formerly provided by `asyncio_priority.py` is now implemented. 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 [got_event_loop](./FASTPOLL.md#332-got_event_loop) + 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) @@ -347,10 +352,14 @@ See [Low priority callbacks](./FASTPOLL.md#35-low-priority-callbacks) ## 3.3 Other Features +### 3.3.1 Version + Variable: - * `version` Returns a 2-tuple. Current contents ('fast_io', '0.24'). Enables + * `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 got_event_loop + Function: * `got_event_loop()` No arg. Returns a `bool`: `True` if the event loop has been instantiated. Enables code using the event loop to raise an exception if @@ -374,6 +383,31 @@ loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) This is mainly for retro-fitting to existing classes and functions. The preferred approach is to pass the event loop to classes as a constructor arg. +### 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. +available it will be placed in the buffer. The return value is the number of +bytes read. The default maximum is the buffer size, otherwise the value of `n`. + +The method will pause (allowing other coros to run) until data is available. + +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 diff --git a/fast_io/__init__.py b/fast_io/__init__.py index 5f05dff..f40968d 100644 --- a/fast_io/__init__.py +++ b/fast_io/__init__.py @@ -164,29 +164,18 @@ def read(self, n=-1): # PollEventLoop._unregister return res # Next iteration raises StopIteration and returns result - def readinto(self, buf, n=0): # Experimental and not yet tested TODO - if DEBUG and __debug__: - log.debug("StreamReader.readinto() START") - + def readinto(self, buf, n=0): # See comments in .read while True: yield IORead(self.polls) - if DEBUG and __debug__: - log.debug("StreamReader.readinto() ... just after IORead") if n: - res = self.ios.readinto(buf, n) # Call the device's readinto method + res = self.ios.readinto(buf, n) # Call device's readinto method else: res = self.ios.readinto(buf) 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 - if DEBUG and __debug__: - log.debug("StreamReader.readinto() ... just after IOReadDone") - # This de-registers device as a read device with poll via - # PollEventLoop._unregister - return res # Next iteration raises StopIteration and returns result + yield IOReadDone(self.polls) + return res def readexactly(self, n): buf = b"" diff --git a/fast_io/core.py b/fast_io/core.py index bfc6e92..747d6c9 100644 --- a/fast_io/core.py +++ b/fast_io/core.py @@ -8,7 +8,7 @@ # Code at https://github.com/peterhinch/micropython-async.git # fork: peterhinch/micropython-lib branch: uasyncio-io-fast-and-rw -version = ('fast_io', '0.24') +version = ('fast_io', '0.25') try: import rtc_time as time # Low power timebase using RTC except ImportError: From f5c4bca705428a7db4431ee9733a3a6691b22236 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 18 Jun 2019 08:10:01 +0100 Subject: [PATCH 132/472] StreamReader.readinto: document, strip debug and repeated comments. --- FASTPOLL.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 9aca084..986edf6 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -393,11 +393,13 @@ 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. -available it will be placed in the buffer. The return value is the number of -bytes read. The default maximum is the buffer size, otherwise the value of `n`. 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, From 45f51dd18de78caebb97c6445f47c178d00175a3 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 18 Jun 2019 09:45:02 +0100 Subject: [PATCH 133/472] Implement get_running_loop(). --- FASTPOLL.md | 39 ++++++++++++++++++++------------------- fast_io/core.py | 7 ++++++- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index 986edf6..b362c8f 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -67,7 +67,7 @@ formerly provided by `asyncio_priority.py` is now implemented. 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 [got_event_loop](./FASTPOLL.md#332-got_event_loop) + 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) @@ -314,7 +314,7 @@ Arguments to `get_event_loop()`: 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#54-writing-streaming-device-drivers). +[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 @@ -358,30 +358,31 @@ 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 got_event_loop +### 3.3.2 Check event loop status -Function: - * `got_event_loop()` No arg. Returns a `bool`: `True` if the event loop has - been instantiated. Enables code using the event loop to raise an exception if - the event loop was not instantiated: -```python -class Foo(): - def __init__(self): - if asyncio.got_event_loop(): - loop = asyncio.get_event_loop() - loop.create_task(self._run()) - else: - raise OSError('Foo class requires an event loop instance') -``` -This avoids subtle errors: +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) ``` -This is mainly for retro-fitting to existing classes and functions. The -preferred approach is to pass the event loop to classes as a constructor arg. +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 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 diff --git a/fast_io/core.py b/fast_io/core.py index 747d6c9..5861f80 100644 --- a/fast_io/core.py +++ b/fast_io/core.py @@ -342,7 +342,12 @@ def get_event_loop(runq_len=16, waitq_len=16, ioq_len=0, lp_len=0): return _event_loop # Allow user classes to determine prior event loop instantiation. -def got_event_loop(): +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): From 8239a3d8b9f19430a13606c1492d5bf9a18300eb Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 29 Jul 2019 18:47:21 +0100 Subject: [PATCH 134/472] Update installation instructions. --- FASTPOLL.md | 2 +- README.md | 17 +++++++++------ TUTORIAL.md | 62 +++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/FASTPOLL.md b/FASTPOLL.md index b362c8f..2439342 100644 --- a/FASTPOLL.md +++ b/FASTPOLL.md @@ -372,7 +372,7 @@ 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 call `get_running_loop` to avoid inadvertently instantiating an +constructor can call `get_running_loop` to avoid inadvertently instantiating an event loop with default args. Function: diff --git a/README.md b/README.md index b4eb45a..cd47f35 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,10 @@ This comprises two parts. # 2. Version and installation of uasyncio -Paul Sokolovsky (`uasyncio` author) has released `uasyncio` V2.2.1. This version -is on PyPi and requires his [Pycopy](https://github.com/pfalcon/micropython) -fork of MicroPython firmware. His `uasyncio` code may also be found in +Paul Sokolovsky (`uasyncio` author) has released versions of `uasyncio` which +supercede the official version. His latest version is that on PyPi and requires +his [Pycopy](https://github.com/pfalcon/micropython) fork of MicroPython +firmware. His `uasyncio` code may also be found in [his fork of micropython-lib](https://github.com/pfalcon/micropython-lib). I support only the official build of MicroPython. The library code guaranteed @@ -58,12 +59,14 @@ CPython). Most documentation and code in this repository assumes the current official version of `uasyncio`. This is V2.0 from [micropython-lib](https://github.com/micropython/micropython-lib). -If release build of MicroPython V1.10 or later is used, V2.0 is incorporated -and no installation is required. Some examples illustrate the features of the -`fast_io` fork and therefore require this version. +It is recommended to use MicroPython firmware V1.11 or later. On many platforms +`uasyncio` is incorporated and no installation is required. + +Some examples illustrate features of the `fast_io` fork and therefore require +this version. See [tutorial](./TUTORIAL.md#installing-uasyncio-on-bare-metal) for -installation instructions where a realease build is not used. +installation instructions where `uasyncio` is not pre-installed. # 3. uasyncio development state diff --git a/TUTORIAL.md b/TUTORIAL.md index c5d5df2..e0ba9ec 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -94,23 +94,60 @@ CPython V3.5 and above. ## 0.1 Installing uasyncio on bare metal -If a release build of firmware is used no installation is necessary as uasyncio -is compiled into the build. The current release build (V1.9.10) now supports -asynchronous stream I/O. +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 +``` -The following instructions cover the case where a release build is not used. -The instructions have changed as the version on PyPi is no longer compatible -with official MicroPython firmware. +#### Hardware without internet connectivity (copy 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 +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 -``` -git clone https://github.com/micropython/micropython-lib.git +```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: @@ -119,9 +156,6 @@ directory `lib`) and copy the following files to it: * `uasyncio.synchro/uasyncio/synchro.py` * `uasyncio.queues/uasyncio/queues.py` -The `queues` and `synchro` modules are optional, but are required to run all -the examples below. - 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. From 90bbd5abde6fcb0c6c6c96a0750fc9819c40f7c7 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 7 Sep 2019 13:39:58 +0100 Subject: [PATCH 135/472] PRIMITIVES.md add @asyn.cancellable syntax warning. --- PRIMITIVES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index bad474e..52a8f83 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -559,6 +559,9 @@ 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: From c5ed0c979bc166020b817b37aa7096d19b4c78c4 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 26 Sep 2019 11:51:03 +0100 Subject: [PATCH 136/472] Correct error in lowpower/README.md --- lowpower/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lowpower/README.md b/lowpower/README.md index 3c95d30..b7ed99b 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -64,7 +64,8 @@ Some general notes on low power Pyboard applications may be found # 2. Installation Ensure that the version of `uasyncio` in this repository is installed and -tested. Copy the file `rtc_time.py` to the device so that it is on `sys.path`. +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 From d778ff464e60591bbff8bf8e4adc5a819f90654a Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 27 Sep 2019 11:01:11 +0100 Subject: [PATCH 137/472] lowpower: warn about issue 5152 and WiFi. --- lowpower/README.md | 9 +++++++-- lowpower/mqtt_log.py | 2 ++ lowpower/rtc_time.py | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lowpower/README.md b/lowpower/README.md index b7ed99b..32b88ff 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -85,10 +85,15 @@ that they are on `sys.path`. * `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_m](https://github.com/peterhinch/micropython-samples/tree/master/micropip). +upip. See [Installing library modules](https://github.com/peterhinch/micropython-samples/tree/master/micropip). ``` ->>> upip_m.install('micropython-umqtt.simple') +>>> 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 diff --git a/lowpower/mqtt_log.py b/lowpower/mqtt_log.py index 1337560..21e50d9 100644 --- a/lowpower/mqtt_log.py +++ b/lowpower/mqtt_log.py @@ -50,8 +50,10 @@ async def main(loop): Latency(2000) count = 0 while True: + print('Publish') publish(ujson.dumps([count, rtc.datetime()])) count += 1 + print('Wait 2 mins') await asyncio.sleep(120) # 2 mins else: # Fail to connect red.on() diff --git a/lowpower/rtc_time.py b/lowpower/rtc_time.py index dfc4a38..986d7d9 100644 --- a/lowpower/rtc_time.py +++ b/lowpower/rtc_time.py @@ -21,7 +21,7 @@ # Power won't be saved if this is done. sleep_ms = utime.sleep_ms -d_series = uname().machine[:5] == 'PYBD_' +d_series = uname().machine[:4] == 'PYBD' use_utime = True # Assume the normal utime timebase if sys.platform == 'pyboard': @@ -41,6 +41,9 @@ # For lowest power consumption set unused pins as inputs with pullups. # Note the 4K7 I2C pullups on X9 X10 Y9 Y10 (Pyboard 1.x). + +# Pulling Pyboard D pins should be disabled if using WiFi as it now seems to +# interfere with it. Although until issue #5152 is fixed it's broken anyway. if d_series: print('Running on Pyboard D') if not use_utime: From 1578048db0e500dea33d9278e5ee21c79461625f Mon Sep 17 00:00:00 2001 From: Gordan Trevis Date: Mon, 30 Sep 2019 20:35:23 +0200 Subject: [PATCH 138/472] Minor Example Code correction Missing asyn. Modul prefix for NamedTask --- PRIMITIVES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PRIMITIVES.md b/PRIMITIVES.md index 52a8f83..ca147d4 100644 --- a/PRIMITIVES.md +++ b/PRIMITIVES.md @@ -707,15 +707,15 @@ 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 NamedTask('my foo', foo, 1, 2) # Pause until complete or killed +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(NamedTask('my nums', foo, 10, 11)()) # Note () syntax. +loop.create_task(asyn.NamedTask('my nums', foo, 10, 11)()) # Note () syntax. ``` Cancellation is performed with: ```python -await NamedTask.cancel('my foo') +await asyn.NamedTask.cancel('my foo') ``` When cancelling a task there is no need to check if the task is still running: From c7c788e8820a06cd28aa67f8531e86e7ca950ddb Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 9 Oct 2019 12:56:38 +0100 Subject: [PATCH 139/472] Temporary workround for MicroPython issue 5172. --- fast_io/__init__.py | 703 ++++++++++++++++++++++++-------------------- fast_io/core.py | 2 +- 2 files changed, 378 insertions(+), 327 deletions(-) diff --git a/fast_io/__init__.py b/fast_io/__init__.py index f40968d..5a2239c 100644 --- a/fast_io/__init__.py +++ b/fast_io/__init__.py @@ -1,326 +1,377 @@ -# 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 -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") - -# add_writer causes read failure if passed the same sock instance as was passed -# to add_reader. Cand we fix this by maintaining two object maps? -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)) - self._register(sock, select.POLLIN) - 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)) - self._register(sock, select.POLLOUT) - 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) - res = self.ios.read(n) - assert res is not None - if not res: - 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) - res = self.ios.readline() - assert res is not None - 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: - res = self.s.write(buf, off, sz) - # If we spooled everything, return immediately - if res == sz: - if DEBUG and __debug__: - log.debug("StreamWriter.awrite(): completed spooling %d bytes", res) - yield IOWriteDone(self.s) # remove_writer de-registers device as a writer - return - if res is None: - res = 0 - if DEBUG and __debug__: - log.debug("StreamWriter.awrite(): spooled partial %d bytes", res) - assert res < sz - off += res - sz -= res - yield IOWrite(self.s) - #assert s2.fileno() == self.s.fileno() - if DEBUG and __debug__: - log.debug("StreamWriter.awrite(): can write more") - - # 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) - while True: - 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} - yield client_coro(StreamReader(s2), StreamWriter(s2, extra)) - - -import uasyncio.core -uasyncio.core._event_loop_class = PollEventLoop +# 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/fast_io/core.py b/fast_io/core.py index 5861f80..7eadcfc 100644 --- a/fast_io/core.py +++ b/fast_io/core.py @@ -8,7 +8,7 @@ # Code at https://github.com/peterhinch/micropython-async.git # fork: peterhinch/micropython-lib branch: uasyncio-io-fast-and-rw -version = ('fast_io', '0.25') +version = ('fast_io', '0.26') try: import rtc_time as time # Low power timebase using RTC except ImportError: From a6a102d1f57faba1636cbb706617a22c359eacb7 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 17 Oct 2019 10:02:19 +0100 Subject: [PATCH 140/472] lowpower: disable_pins added and defaulted False. --- lowpower/README.md | 19 ++++++++++++++++++- lowpower/rtc_time.py | 7 ++++--- lowpower/rtc_time_cfg.py | 1 + 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lowpower/README.md b/lowpower/README.md index 32b88ff..acaedc8 100644 --- a/lowpower/README.md +++ b/lowpower/README.md @@ -1,6 +1,6 @@ # A low power usayncio adaptation -Release 0.12 23rd April 2019 +Release 0.13 17th Oct 2019 API changes: low power applications must now import `rtc_time_cfg` and set its `enabled` flag. @@ -21,6 +21,7 @@ This module is specific to Pyboards including the D series. 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) @@ -327,6 +328,22 @@ around or to make it global. Once instantiated, latency may be changed by 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 diff --git a/lowpower/rtc_time.py b/lowpower/rtc_time.py index 986d7d9..b6b7137 100644 --- a/lowpower/rtc_time.py +++ b/lowpower/rtc_time.py @@ -12,7 +12,7 @@ import sys import utime from os import uname -from rtc_time_cfg import enabled, disable_3v3, disable_leds +from rtc_time_cfg import enabled, disable_3v3, disable_leds, disable_pins if not enabled: # uasyncio traps this and uses utime raise ImportError('rtc_time is not enabled.') @@ -72,8 +72,9 @@ def low_power_pins(): pins_bt = ['D5', 'D10', 'E3', 'E4', 'E5', 'E6', 'G8', 'G13', 'G14', 'G15', 'I4', 'I5', 'I6', 'I10'] pins_qspi1 = ['B2', 'B6', 'D11', 'D12', 'D13', 'E2'] pins_qspi2 = ['E7', 'E8', 'E9', 'E10', 'E11', 'E13'] - for p in pins: - pyb.Pin(p, pyb.Pin.IN, pyb.Pin.PULL_DOWN) + if disable_pins: + for p in pins: + pyb.Pin(p, pyb.Pin.IN, pyb.Pin.PULL_DOWN) if disable_3v3: pyb.Pin('EN_3V3', pyb.Pin.IN, None) if disable_leds: diff --git a/lowpower/rtc_time_cfg.py b/lowpower/rtc_time_cfg.py index be3e346..c7c7d5e 100644 --- a/lowpower/rtc_time_cfg.py +++ b/lowpower/rtc_time_cfg.py @@ -2,3 +2,4 @@ enabled = False disable_3v3 = False disable_leds = False +disable_pins = False From 9a1939cec3d27babcf2de2522fe5cef20f10b0cc Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 21 Oct 2019 11:53:42 +0100 Subject: [PATCH 141/472] aswitch.py: fix bug where Delay_ms spawned needless coros. --- DRIVERS.md | 13 +++++++++++-- astests.py | 33 +++++++++++++++++++++++++++++++++ aswitch.py | 50 +++++++++++++++++++++++++++++++------------------- 3 files changed, 75 insertions(+), 21 deletions(-) diff --git a/DRIVERS.md b/DRIVERS.md index fd612f3..3359123 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -156,6 +156,9 @@ 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 @@ -207,12 +210,18 @@ 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. + 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 @@ -229,7 +238,7 @@ import uasyncio as asyncio from aswitch import Pushbutton, Delay_ms async def my_app(): - await asyncio.sleep(60) # Dummy + await asyncio.sleep(60) # Run for 1 minute pin = Pin('X1', Pin.IN, Pin.PULL_UP) # Pushbutton to gnd red = LED(1) diff --git a/astests.py b/astests.py index db87cc9..0120be5 100644 --- a/astests.py +++ b/astests.py @@ -13,7 +13,16 @@ 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): @@ -33,8 +42,13 @@ async def killer(): # 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) @@ -47,8 +61,13 @@ def test_sw(): # 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) @@ -62,8 +81,15 @@ def test_swcb(): # 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) @@ -83,8 +109,15 @@ def test_btn(suppress=False, lf=True, df=True): # 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) diff --git a/aswitch.py b/aswitch.py index e362af3..4269ce9 100644 --- a/aswitch.py +++ b/aswitch.py @@ -46,53 +46,63 @@ def launch(func, tup_args): loop.create_task(res) -class Delay_ms(object): +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 # Not running + 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 self.tstop is None: # Not running + if not self._running: # timer not running await asyncio.sleep_ms(0) else: - await self.killer() + await self._killer() def stop(self): - self.tstop = None + 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 - if self.can_alloc and self.tstop is None: # No killer task is running - self.tstop = time.ticks_add(time.ticks_ms(), duration) - # Start a task which stops the delay after its period has elapsed - self.loop.create_task(self.killer()) - self.tstop = time.ticks_add(time.ticks_ms(), 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.tstop is not None + return self._running __call__ = running - async def killer(self): - twait = time.ticks_diff(self.tstop, time.ticks_ms()) + 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: + if self._tstop is None: break # Return if stop() called during wait - twait = time.ticks_diff(self.tstop, time.ticks_ms()) - if self.tstop is not None and self.func is not None: + 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 # Not running + self._tstop = None # killer not running + self._running = False # timer is stopped -class Switch(object): +class Switch: debounce_ms = 50 def __init__(self, pin): self.pin = pin # Should be initialised for input with pullup @@ -127,7 +137,9 @@ async def switchcheck(self): # Ignore further state changes until switch has settled await asyncio.sleep_ms(Switch.debounce_ms) -class Pushbutton(object): +# 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 From a602aa89303b047939c1ba43be03321509a1ad72 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 17 Nov 2019 16:27:35 +0000 Subject: [PATCH 142/472] Update primitives for new uasyncio version. README provides introduction. --- README.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++-- asyn.py | 4 +-- asyntest.py | 50 +++++++++++++++++++++++---------- 3 files changed, 116 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index cd47f35..0277e22 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ code size and high performance on bare metal targets. This repository provides documentation, tutorial material and code to aid in its effective use. It also contains an optional `fast_io` variant of `uasyncio`. +Damien has completely rewritten `uasyncio`. Its release is likely to be +imminent, see +[PR5332](https://github.com/micropython/micropython/pull/5332) and [section 3.1](./README.md##31-the-new_version). + ## The fast_io variant This comprises two parts. @@ -70,6 +74,78 @@ installation instructions where `uasyncio` is not pre-installed. # 3. uasyncio development state +## 3.1 The new version + +This complete rewrite of `uasyncio` supports CPython 3.8 syntax. A design aim +is that it should be be a compatible subset of `asyncio`. Many applications +using the coding style advocated in the tutorial will work unchanged. The +following features will involve minor changes to application code: + + * 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 should be replaced by `await asyncio.sleep_ms(0)`: + this is in accord with CPython where `yield` will produce a syntax error. + * Awaitable classes: currently under discussion. The `__iter__` method works + but `yield` should be replaced by `await asyncio.sleep_ms(0)`. As yet I have + found no way to write an awaitable classes compatible with the new `uasyncio` + and which does not throw syntax errors under CPython 3.8/`asyncio`. + +### 3.1.1 Implications for this repository + +It is planned to retain V2 under a different name. The new version fixes bugs +which have been outstanding for a long time. In my view V2 is best viewed as +deprecated. I will retain V2-specific code and docs in a separate directory, +with the rest of this repo being adapted for the new version. + +#### 3.1.1.1 Tutorial + +This requires only minor changes. + +#### 3.1.1.2 Fast I/O + +The `fast_io` fork is incompatible and will be relegated to the V2 directory. + +The new version's design greatly simplifies the implementation of fast I/O: +I therefore hope the new `uasyncio` will include it. The other principal aims +were to provide workrounds for bugs now fixed. If `uasyncio` includes fast I/O +there is no reason to fork the new version; other `fast_io` features will be +lost unless Damien sees fit to implement them. The low priority task option is +little used and arguably is ill-conceived: I will not be advocating for its +inclusion. + +#### 3.1.1.3 Synchronisation Primitives + +The CPython `asyncio` library supports these synchronisation primitives: + * `Lock` - already incorporated in new `uasyncio`. + * `Event` - already incorporated. + * `gather` - already incorporated. + * `Semaphore` and `BoundedSemaphore`. My classes work under new version. + * `Condition`. Works under new version. + * `Queue`. This was implemented by Paul Sokolvsky in `uasyncio.queues`. + +Incorporating these will produce more efficient implementations; my solutions +were designed to work with stock `uasyncio` V2. + +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. I will support these by subclassing the native `Event`. + +The following work under new and old versions: + * `Barrier` (now adapted). + * `Delay_ms` (this and the following in aswitch.py) + * `Switch` + * `Pushbutton` + +The following were workrounds for bugs and omissions in V2 which are now fixed. +They will be removed. + * The cancellation decorators and classes (cancellation works as per CPython). + * The nonstandard support for `gather` (now properly supported). + +## 3.2 The current version V2.0 + These notes are intended for users familiar with `asyncio` under CPython. The MicroPython language is based on CPython 3.4. The `uasyncio` library @@ -99,13 +175,13 @@ It supports millisecond level timing with the following: Classes `Task` and `Future` are not supported. -## 3.1 Asynchronous I/O +## 3.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. -## 3.2 Time values +## 3.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 diff --git a/asyn.py b/asyn.py index 7496027..c87c175 100644 --- a/asyn.py +++ b/asyn.py @@ -154,7 +154,7 @@ def __await__(self): while True: # Wait until last waiting thread changes the direction if direction != self._down: return - yield + await asyncio.sleep_ms(0) __iter__ = __await__ @@ -201,7 +201,7 @@ async def __aexit__(self, *args): async def acquire(self): while self._count == 0: - yield + await asyncio.sleep_ms(0) self._count -= 1 def release(self): diff --git a/asyntest.py b/asyntest.py index 2c7e7c2..26c874c 100644 --- a/asyntest.py +++ b/asyntest.py @@ -40,7 +40,7 @@ def print_tests(): event_test() Test Event and Lock objects. barrier_test() Test the Barrier class. semaphore_test(bounded=False) Test Semaphore or BoundedSemaphore. -condition_test() Test the Condition class. +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. @@ -287,13 +287,6 @@ async def cond01(): with await cond: cond.notify(2) # Notify 2 tasks -async def cond02(n, barrier): - with await cond: - print('cond02', n, 'Awaiting notification.') - await cond.wait() - print('cond02', n, 'triggered. tim =', tim) - barrier.trigger() - @asyn.cancellable async def cond03(): # Maintain a count of seconds global tim @@ -302,6 +295,26 @@ async def cond03(): # Maintain a count of seconds 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 @@ -312,11 +325,15 @@ async def cond04(n, barrier): print('cond04', n, 'triggered. tim =', tim) barrier.trigger() -async def cond_go(loop): +async def cond_go(loop, new): ntasks = 7 barrier = asyn.Barrier(ntasks + 1) - loop.create_task(asyn.Cancellable(cond01)()) - loop.create_task(asyn.Cancellable(cond03)()) + 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 @@ -325,10 +342,15 @@ async def cond_go(loop): loop.create_task(cond04(99, barrier)) await barrier # cancel continuously running coros. - await asyn.Cancellable.cancel_all() + if new: + t1.cancel() + t3.cancel() + await asyncio.sleep_ms(0) + else: + await asyn.Cancellable.cancel_all() print('Done.') -def condition_test(): +def condition_test(new=False): printexp('''cond02 0 Awaiting notification. cond02 1 Awaiting notification. cond02 2 Awaiting notification. @@ -348,7 +370,7 @@ def condition_test(): Done. ''', 13) loop = asyncio.get_event_loop() - loop.run_until_complete(cond_go(loop)) + loop.run_until_complete(cond_go(loop, new)) # ************ Gather test ************ From d43ee7b759091c934138a3acfb2d988707b83cc2 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 17 Nov 2019 16:33:47 +0000 Subject: [PATCH 143/472] Update primitives for new uasyncio version. README provides introduction. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0277e22..d0098a1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ contains an optional `fast_io` variant of `uasyncio`. Damien has completely rewritten `uasyncio`. Its release is likely to be imminent, see -[PR5332](https://github.com/micropython/micropython/pull/5332) and [section 3.1](./README.md##31-the-new_version). +[PR5332](https://github.com/micropython/micropython/pull/5332) and [section 3.1](./README.md#31-the-new_version). ## The fast_io variant @@ -90,7 +90,7 @@ following features will involve minor changes to application code: this is in accord with CPython where `yield` will produce a syntax error. * Awaitable classes: currently under discussion. The `__iter__` method works but `yield` should be replaced by `await asyncio.sleep_ms(0)`. As yet I have - found no way to write an awaitable classes compatible with the new `uasyncio` + found no way to write an awaitable class compatible with the new `uasyncio` and which does not throw syntax errors under CPython 3.8/`asyncio`. ### 3.1.1 Implications for this repository From 83b134ccf03f963ffd30f6ae14e2f8552e1c66fe Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 17 Nov 2019 16:37:32 +0000 Subject: [PATCH 144/472] Update primitives for new uasyncio version. README provides introduction. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0098a1..bb567e8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ contains an optional `fast_io` variant of `uasyncio`. Damien has completely rewritten `uasyncio`. Its release is likely to be imminent, see -[PR5332](https://github.com/micropython/micropython/pull/5332) and [section 3.1](./README.md#31-the-new_version). +[PR5332](https://github.com/micropython/micropython/pull/5332) and [below](./README.md#31-the-new_version). ## The fast_io variant From c11d0ec7f738d410c458f859c6da1f13abf04b73 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 17 Nov 2019 16:39:00 +0000 Subject: [PATCH 145/472] Update primitives for new uasyncio version. README provides introduction. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb567e8..db305bb 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ contains an optional `fast_io` variant of `uasyncio`. Damien has completely rewritten `uasyncio`. Its release is likely to be imminent, see -[PR5332](https://github.com/micropython/micropython/pull/5332) and [below](./README.md#31-the-new_version). +[PR5332](https://github.com/micropython/micropython/pull/5332) and [below](./README.md#31-the-new-version). ## The fast_io variant From 67ae61318fbcc6595d525ab26a1b25cc089942c0 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 27 Jan 2020 17:47:49 +0000 Subject: [PATCH 146/472] GPS: tolerate empty float fields. Ignore 2nd char of prefix. --- gps/as_GPS.py | 56 ++++++++++++++++++++++++++------------------------- gps/ast_pb.py | 2 +- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/gps/as_GPS.py b/gps/as_GPS.py index fdbddb2..196069d 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -23,6 +23,9 @@ 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) @@ -113,14 +116,12 @@ def __init__(self, sreader, local_offset=0, fix_cb=lambda *_ : None, cb_mask=RMC self._mktime = time.mktime # Key: currently supported NMEA sentences. Value: parse method. - self.supported_sentences = {'GPRMC': self._gprmc, 'GLRMC': self._gprmc, - 'GPGGA': self._gpgga, 'GLGGA': self._gpgga, - 'GPVTG': self._gpvtg, 'GLVTG': self._gpvtg, - 'GPGSA': self._gpgsa, 'GLGSA': self._gpgsa, - 'GPGSV': self._gpgsv, 'GLGSV': self._gpgsv, - 'GPGLL': self._gpgll, 'GLGLL': self._gpgll, - 'GNGGA': self._gpgga, 'GNRMC': self._gprmc, - 'GNVTG': self._gpvtg, + self.supported_sentences = {'RMC': self._gprmc, + 'GGA': self._gpgga, + 'VTG': self._gpvtg, + 'GSA': self._gpgsa, + 'GSV': self._gpgsv, + 'GLL': self._gpgll, } ##################### @@ -212,11 +213,12 @@ async def _update(self, line): await asyncio.sleep(0) self.clean_sentences += 1 # Sentence is good but unparsed. - segs[0] = segs[0][1:] # discard $ + seg0 = segs[0][1:] # discard $ + segx = seg0[2:] # Discard 1st 2 chars segs = segs[:-1] # and checksum - if segs[0] in self.supported_sentences: + if seg0.startswith('G') and segx in self.supported_sentences: try: - s_type = self.supported_sentences[segs[0]](segs) # Parse + s_type = self.supported_sentences[segx](segs) # Parse except ValueError: s_type = False await asyncio.sleep(0) @@ -226,12 +228,12 @@ async def _update(self, line): if s_type: # Successfully parsed if self.reparse(segs): # Subclass hook self.parsed_sentences += 1 - return segs[0] # For test programs + return seg0 # For test programs else: if self.parse(segs): # Subclass hook self.parsed_sentences += 1 self.unsupported_sentences += 1 - return segs[0] # For test programs + return seg0 # For test programs # Optional hooks for subclass def parse(self, segs): # Parse unsupported sentences @@ -249,12 +251,12 @@ 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_mins = gfloat(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_mins = gfloat(l_string[3:]) lon_hemi = gps_segments[idx_long + 1] if lat_hemi not in 'NS'or lon_hemi not in 'EW': @@ -281,7 +283,7 @@ def _set_date_time(self, utc_string, date_string): # 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:])) + fss, fsecs = modf(gfloat(utc_string[4:])) secs = int(fsecs) self.msecs = int(fss * 1000) d = int(date_string[0:2]) # day @@ -322,12 +324,12 @@ def _gprmc(self, gps_segments): # Parse RMC sentence # Can raise ValueError. self._fix(gps_segments, 3, 5) # Speed - spd_knt = float(gps_segments[7]) + spd_knt = gfloat(gps_segments[7]) # Course - course = float(gps_segments[8]) + course = gfloat(gps_segments[8]) # Add Magnetic Variation if firmware supplies it if gps_segments[10]: - mv = float(gps_segments[10]) + mv = gfloat(gps_segments[10]) if gps_segments[11] not in ('EW'): raise ValueError self.magvar = mv if gps_segments[11] == 'E' else -mv @@ -352,8 +354,8 @@ def _gpgll(self, gps_segments): # Parse GLL sentence # 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]) + course = gfloat(gps_segments[1]) + spd_knt = gfloat(gps_segments[5]) self._speed = spd_knt self.course = course self._valid |= VTG @@ -364,7 +366,7 @@ def _gpgga(self, gps_segments): # Parse GGA sentence # Number of Satellites in Use satellites_in_use = int(gps_segments[7]) # Horizontal Dilution of Precision - hdop = float(gps_segments[8]) + hdop = gfloat(gps_segments[8]) # Get Fix Status fix_stat = int(gps_segments[6]) @@ -373,8 +375,8 @@ def _gpgga(self, gps_segments): # Parse GGA sentence # Longitude / Latitude self._fix(gps_segments, 2, 4) # Altitude / Height Above Geoid - altitude = float(gps_segments[9]) - geoid_height = float(gps_segments[11]) + altitude = gfloat(gps_segments[9]) + geoid_height = gfloat(gps_segments[11]) # Update Object Data self.altitude = altitude self.geoid_height = geoid_height @@ -399,9 +401,9 @@ def _gpgsa(self, gps_segments): # Parse GSA sentence else: break # PDOP,HDOP,VDOP - pdop = float(gps_segments[15]) - hdop = float(gps_segments[16]) - vdop = float(gps_segments[17]) + pdop = gfloat(gps_segments[15]) + hdop = gfloat(gps_segments[16]) + vdop = gfloat(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? diff --git a/gps/ast_pb.py b/gps/ast_pb.py index 502b065..b9498bf 100644 --- a/gps/ast_pb.py +++ b/gps/ast_pb.py @@ -1,7 +1,7 @@ # 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 +# Copyright (c) Peter Hinch 2018-2020 # Released under the MIT License (MIT) - see LICENSE file # Test asynchronous GPS device driver as_pyGPS From a49510d78e2a3c977b60131aa5a975b31886d5a1 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 27 Jan 2020 17:49:27 +0000 Subject: [PATCH 147/472] GPS: tolerate empty float fields. Ignore 2nd char of prefix. --- gps/as_GPS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gps/as_GPS.py b/gps/as_GPS.py index 196069d..d06241f 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -5,7 +5,7 @@ # 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 Peter Hinch +# 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 9572d139bcab7e62859040c9f86607315210bc8e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 28 Jan 2020 09:52:29 +0000 Subject: [PATCH 148/472] GPS: improve detection of malformed lines. --- gps/as_GPS.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gps/as_GPS.py b/gps/as_GPS.py index d06241f..d8353ff 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -192,6 +192,9 @@ async def _run(self, loop): # 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 if self.FULL_CHECK: # 9ms on Pyboard try: next(c for c in line if ord(c) < 10 or ord(c) > 126) @@ -199,8 +202,6 @@ async def _update(self, line): except StopIteration: pass # All good await asyncio.sleep(0) - if len(line) > self._SENTENCE_LIMIT or not '*' in line: - return # Too long or malformed a = line.split(',') segs = a[:-1] + a[-1].split('*') From 7e8bab05b33f2b7fcc540309b4419aadd113d986 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 28 Jan 2020 19:13:04 +0000 Subject: [PATCH 149/472] GPS: improve detection of malformed lines. --- gps/as_GPS.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/gps/as_GPS.py b/gps/as_GPS.py index d8353ff..510d51e 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -195,13 +195,9 @@ async def _update(self, 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 - if self.FULL_CHECK: # 9ms on Pyboard - try: - next(c for c in line if ord(c) < 10 or ord(c) > 126) - return # Bad character received - except StopIteration: - pass # All good - await asyncio.sleep(0) + # 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('*') @@ -214,9 +210,10 @@ async def _update(self, line): await asyncio.sleep(0) self.clean_sentences += 1 # Sentence is good but unparsed. - seg0 = segs[0][1:] # discard $ - segx = seg0[2:] # Discard 1st 2 chars + 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 From b03296ea11bd2cb4a1f73eb500152be065115ad1 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 31 Jan 2020 14:48:28 +0000 Subject: [PATCH 150/472] GPS Revert gfloat change. --- gps/as_GPS.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/gps/as_GPS.py b/gps/as_GPS.py index 510d51e..1a912d5 100644 --- a/gps/as_GPS.py +++ b/gps/as_GPS.py @@ -24,7 +24,7 @@ from math import modf # Float conversion tolerant of empty field -gfloat = lambda x : float(x) if x else 0.0 +# gfloat = lambda x : float(x) if x else 0.0 # Angle formats DD = const(1) @@ -249,12 +249,12 @@ 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 = gfloat(l_string[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 = gfloat(l_string[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': @@ -281,7 +281,7 @@ def _set_date_time(self, utc_string, date_string): # 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(gfloat(utc_string[4:])) + fss, fsecs = modf(float(utc_string[4:])) secs = int(fsecs) self.msecs = int(fss * 1000) d = int(date_string[0:2]) # day @@ -322,12 +322,12 @@ def _gprmc(self, gps_segments): # Parse RMC sentence # Can raise ValueError. self._fix(gps_segments, 3, 5) # Speed - spd_knt = gfloat(gps_segments[7]) + spd_knt = float(gps_segments[7]) # Course - course = gfloat(gps_segments[8]) + course = float(gps_segments[8]) # Add Magnetic Variation if firmware supplies it if gps_segments[10]: - mv = gfloat(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 @@ -352,8 +352,8 @@ def _gpgll(self, gps_segments): # Parse GLL sentence # Chip sends VTG messages with meaningless data before getting a fix. def _gpvtg(self, gps_segments): # Parse VTG sentence self._valid &= ~VTG - course = gfloat(gps_segments[1]) - spd_knt = gfloat(gps_segments[5]) + course = float(gps_segments[1]) + spd_knt = float(gps_segments[5]) self._speed = spd_knt self.course = course self._valid |= VTG @@ -364,7 +364,7 @@ def _gpgga(self, gps_segments): # Parse GGA sentence # Number of Satellites in Use satellites_in_use = int(gps_segments[7]) # Horizontal Dilution of Precision - hdop = gfloat(gps_segments[8]) + hdop = float(gps_segments[8]) # Get Fix Status fix_stat = int(gps_segments[6]) @@ -373,8 +373,8 @@ def _gpgga(self, gps_segments): # Parse GGA sentence # Longitude / Latitude self._fix(gps_segments, 2, 4) # Altitude / Height Above Geoid - altitude = gfloat(gps_segments[9]) - geoid_height = gfloat(gps_segments[11]) + altitude = float(gps_segments[9]) + geoid_height = float(gps_segments[11]) # Update Object Data self.altitude = altitude self.geoid_height = geoid_height @@ -399,9 +399,9 @@ def _gpgsa(self, gps_segments): # Parse GSA sentence else: break # PDOP,HDOP,VDOP - pdop = gfloat(gps_segments[15]) - hdop = gfloat(gps_segments[16]) - vdop = gfloat(gps_segments[17]) + 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? From 3d545ff9a8f880ead12a160b35965c0f63b8fccd Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Mon, 6 Apr 2020 10:36:13 +0100 Subject: [PATCH 151/472] Prior to v3 directory --- README.md | 67 ++++++++++++++++++++++-------------------------- nec_ir/README.md | 42 +++++++++++++++--------------- 2 files changed, 51 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index db305bb..f947a32 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,23 @@ code size and high performance on bare metal targets. This repository provides documentation, tutorial material and code to aid in its effective use. It also contains an optional `fast_io` variant of `uasyncio`. -Damien has completely rewritten `uasyncio`. Its release is likely to be -imminent, see +Damien has completely rewritten `uasyncio`. V3.0 now been released, see [PR5332](https://github.com/micropython/micropython/pull/5332) and [below](./README.md#31-the-new-version). -## The fast_io variant +#### NOTE ON NEW RELEASE -This comprises two parts. - 1. The [fast_io](./FASTPOLL.md) version of `uasyncio` is a "drop in" - replacement for the official version 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. +The material in this repo largely relates to the old version V2.0 and I intend +a substantial revision. I believe most of the material in the tutorial is still +valid as it aims to be CPython compatible. I also expect the example scripts to +work, based on testing with pre-release versions. + +There is currently no support for fast I/O scheduling: 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. In these applications there is still a use case for the fast_io +version. I hope that the new version acquires a facility to prioritise I/O. -## Resources for users of all versions +## Resources * [A tutorial](./TUTORIAL.md) An introductory tutorial on asynchronous programming and the use of the `uasyncio` library. @@ -44,33 +47,27 @@ This comprises two parts. * [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. - * [Under the hood](./UNDER_THE_HOOD.md) A guide to help understand the - `uasyncio` code. For scheduler geeks and those wishing to modify `uasyncio`. -# 2. Version and installation of uasyncio +## Resources specific to V2.0 -Paul Sokolovsky (`uasyncio` author) has released versions of `uasyncio` which -supercede the official version. His latest version is that on PyPi and requires -his [Pycopy](https://github.com/pfalcon/micropython) fork of MicroPython -firmware. His `uasyncio` code may also be found in -[his fork of micropython-lib](https://github.com/pfalcon/micropython-lib). +### The fast_io variant -I support only the official build of MicroPython. The library code guaranteed -to work with this build is in [micropython-lib](https://github.com/micropython/micropython-lib). -Most of the resources in here should work with Paul's forks (most work with -CPython). +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. -Most documentation and code in this repository assumes the current official -version of `uasyncio`. This is V2.0 from -[micropython-lib](https://github.com/micropython/micropython-lib). -It is recommended to use MicroPython firmware V1.11 or later. On many platforms -`uasyncio` is incorporated and no installation is required. +### Under the hood -Some examples illustrate features of the `fast_io` fork and therefore require -this version. +[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`. + +# 2. Version and installation of uasyncio -See [tutorial](./TUTORIAL.md#installing-uasyncio-on-bare-metal) for -installation instructions where `uasyncio` is not pre-installed. +The new release of `uasyncio` is pre-installed in current daily firmware +builds. # 3. uasyncio development state @@ -97,12 +94,8 @@ following features will involve minor changes to application code: It is planned to retain V2 under a different name. The new version fixes bugs which have been outstanding for a long time. In my view V2 is best viewed as -deprecated. I will retain V2-specific code and docs in a separate directory, -with the rest of this repo being adapted for the new version. - -#### 3.1.1.1 Tutorial - -This requires only minor changes. +deprecated. I will support V3 in a separate directory, the resources in this +directory being retained for existing applications and users of V2 and fast_io. #### 3.1.1.2 Fast I/O diff --git a/nec_ir/README.md b/nec_ir/README.md index fb4ad2c..33be026 100644 --- a/nec_ir/README.md +++ b/nec_ir/README.md @@ -10,14 +10,14 @@ 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 + 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 +The driver requires the `uasyncio` library and the file `asyn.py` from this repository. # Usage @@ -36,10 +36,10 @@ 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 +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 +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. @@ -48,7 +48,7 @@ not in use. 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``. +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 @@ -62,9 +62,9 @@ Users tend to press the key again if no acknowledgement is received. 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 + 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. @@ -72,11 +72,11 @@ 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. + 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. +The test program `art1.py` provides an example of a minimal application. # How it works @@ -103,7 +103,7 @@ 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 +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 @@ -115,19 +115,19 @@ in the interrupt service routine. Data values passed to the callback are normally positive. Negative values indicate a repeat code or an error. -``REPEAT`` A repeat code was received. +`REPEAT` A repeat code was received. -Any data value < ``REPEAT`` denotes an error. In general applications do not +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 +`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 +`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 +`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. From 2f797894b2b0129654644610eca2937e7cbffc53 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 10 Apr 2020 18:15:57 +0100 Subject: [PATCH 152/472] Add uasyncio V3 information. --- README.md | 207 ++- v3/README.md | 135 ++ v3/TUTORIAL.md | 2261 +++++++++++++++++++++++++++++++ v3/__init__.py | 0 v3/demos/aledflash.py | 30 + v3/demos/apoll.py | 61 + v3/demos/auart.py | 28 + v3/demos/auart_hd.py | 106 ++ v3/demos/gather.py | 49 + v3/demos/gps/LICENSE | 21 + v3/demos/gps/README.md | 907 +++++++++++++ v3/demos/gps/as_GPS.py | 615 +++++++++ v3/demos/gps/as_GPS_time.py | 166 +++ v3/demos/gps/as_GPS_utils.py | 48 + v3/demos/gps/as_rwGPS.py | 118 ++ v3/demos/gps/as_rwGPS_time.py | 230 ++++ v3/demos/gps/as_tGPS.py | 240 ++++ v3/demos/gps/ast_pb.py | 98 ++ v3/demos/gps/ast_pbrw.py | 171 +++ v3/demos/gps/astests.py | 177 +++ v3/demos/gps/astests_pyb.py | 151 +++ v3/demos/gps/baud.py | 55 + v3/demos/gps/log.kml | 128 ++ v3/demos/gps/log_kml.py | 75 + v3/demos/iorw.py | 118 ++ v3/demos/rate.py | 46 + v3/demos/roundrobin.py | 34 + v3/primitives/__init__.py | 19 + v3/primitives/barrier.py | 68 + v3/primitives/condition.py | 63 + v3/primitives/delay_ms.py | 60 + v3/primitives/message.py | 64 + v3/primitives/pushbutton.py | 97 ++ v3/primitives/queue.py | 66 + v3/primitives/semaphore.py | 37 + v3/primitives/switch.py | 37 + v3/primitives/tests/__init__.py | 0 v3/primitives/tests/asyntest.py | 404 ++++++ v3/primitives/tests/switches.py | 137 ++ 39 files changed, 7203 insertions(+), 124 deletions(-) create mode 100644 v3/README.md create mode 100644 v3/TUTORIAL.md create mode 100644 v3/__init__.py create mode 100644 v3/demos/aledflash.py create mode 100644 v3/demos/apoll.py create mode 100644 v3/demos/auart.py create mode 100644 v3/demos/auart_hd.py create mode 100644 v3/demos/gather.py create mode 100644 v3/demos/gps/LICENSE create mode 100644 v3/demos/gps/README.md create mode 100644 v3/demos/gps/as_GPS.py create mode 100644 v3/demos/gps/as_GPS_time.py create mode 100644 v3/demos/gps/as_GPS_utils.py create mode 100644 v3/demos/gps/as_rwGPS.py create mode 100644 v3/demos/gps/as_rwGPS_time.py create mode 100644 v3/demos/gps/as_tGPS.py create mode 100644 v3/demos/gps/ast_pb.py create mode 100644 v3/demos/gps/ast_pbrw.py create mode 100755 v3/demos/gps/astests.py create mode 100755 v3/demos/gps/astests_pyb.py create mode 100644 v3/demos/gps/baud.py create mode 100644 v3/demos/gps/log.kml create mode 100644 v3/demos/gps/log_kml.py create mode 100644 v3/demos/iorw.py create mode 100644 v3/demos/rate.py create mode 100644 v3/demos/roundrobin.py create mode 100644 v3/primitives/__init__.py create mode 100644 v3/primitives/barrier.py create mode 100644 v3/primitives/condition.py create mode 100644 v3/primitives/delay_ms.py create mode 100644 v3/primitives/message.py create mode 100644 v3/primitives/pushbutton.py create mode 100644 v3/primitives/queue.py create mode 100644 v3/primitives/semaphore.py create mode 100644 v3/primitives/switch.py create mode 100644 v3/primitives/tests/__init__.py create mode 100644 v3/primitives/tests/asyntest.py create mode 100644 v3/primitives/tests/switches.py diff --git a/README.md b/README.md index f947a32..ef04c99 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,43 @@ -# 1. Asynchronous programming in MicroPython +# Asynchronous programming in MicroPython CPython supports asynchronous programming via the `asyncio` library. 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. It also -contains an optional `fast_io` variant of `uasyncio`. +documentation, tutorial material and code to aid in its effective use. -Damien has completely rewritten `uasyncio`. V3.0 now been released, see -[PR5332](https://github.com/micropython/micropython/pull/5332) and [below](./README.md#31-the-new-version). +## uasyncio versions -#### NOTE ON NEW RELEASE +Damien has completely rewritten `uasyncio` which has been released as V3.0. See +[PR5332](https://github.com/micropython/micropython/pull/5332). -The material in this repo largely relates to the old version V2.0 and I intend -a substantial revision. I believe most of the material in the tutorial is still -valid as it aims to be CPython compatible. I also expect the example scripts to -work, based on testing with pre-release versions. +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`. -There is currently no support for fast I/O scheduling: 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. In these applications there is still a use case for the fast_io -version. I hope that the new version acquires a facility to prioritise I/O. +Resources for V3 and an updated tutorial may be found in the v3 directory. -## Resources +### [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. @@ -48,9 +63,7 @@ version. I hope that the new version acquires a facility to prioritise I/O. boards to communicate without using a UART. This is hardware agnostic but slower than the I2C version. -## Resources specific to V2.0 - -### The fast_io variant +## 1.2 The fast_io variant This comprises two parts. 1. The [fast_io](./FASTPOLL.md) version of `uasyncio` is a "drop in" @@ -59,85 +72,73 @@ This comprises two parts. 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. -### 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`. - -# 2. Version and installation of uasyncio - -The new release of `uasyncio` is pre-installed in current daily firmware -builds. - -# 3. uasyncio development state - -## 3.1 The new version +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. -This complete rewrite of `uasyncio` supports CPython 3.8 syntax. A design aim -is that it should be be a compatible subset of `asyncio`. Many applications -using the coding style advocated in the tutorial will work unchanged. The -following features will involve minor changes to application code: +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. - * 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 should be replaced by `await asyncio.sleep_ms(0)`: - this is in accord with CPython where `yield` will produce a syntax error. - * Awaitable classes: currently under discussion. The `__iter__` method works - but `yield` should be replaced by `await asyncio.sleep_ms(0)`. As yet I have - found no way to write an awaitable class compatible with the new `uasyncio` - and which does not throw syntax errors under CPython 3.8/`asyncio`. +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. -### 3.1.1 Implications for this repository +## 1.2.1 A Pyboard-only low power module -It is planned to retain V2 under a different name. The new version fixes bugs -which have been outstanding for a long time. In my view V2 is best viewed as -deprecated. I will support V3 in a separate directory, the resources in this -directory being retained for existing applications and users of V2 and fast_io. +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. -#### 3.1.1.2 Fast I/O +## 1.3 Under the hood -The `fast_io` fork is incompatible and will be relegated to the V2 directory. +[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`. -The new version's design greatly simplifies the implementation of fast I/O: -I therefore hope the new `uasyncio` will include it. The other principal aims -were to provide workrounds for bugs now fixed. If `uasyncio` includes fast I/O -there is no reason to fork the new version; other `fast_io` features will be -lost unless Damien sees fit to implement them. The low priority task option is -little used and arguably is ill-conceived: I will not be advocating for its -inclusion. +## 1.4 Synchronisation Primitives -#### 3.1.1.3 Synchronisation Primitives +All solutions listed below work with stock `uasyncio` V2 or `fast_io`. The CPython `asyncio` library supports these synchronisation primitives: - * `Lock` - already incorporated in new `uasyncio`. - * `Event` - already incorporated. - * `gather` - already incorporated. - * `Semaphore` and `BoundedSemaphore`. My classes work under new version. - * `Condition`. Works under new version. + * `Lock` + * `Event` + * `gather` + * `Semaphore` and `BoundedSemaphore`. + * `Condition`. * `Queue`. This was implemented by Paul Sokolvsky in `uasyncio.queues`. - -Incorporating these will produce more efficient implementations; my solutions -were designed to work with stock `uasyncio` V2. + +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. I will support these by subclassing the native `Event`. +awaitable class. + +#### These are documented [here](./PRIMITIVES.md) + +## 1.5 Switches, Pushbuttons and Timeouts -The following work under new and old versions: - * `Barrier` (now adapted). - * `Delay_ms` (this and the following in aswitch.py) - * `Switch` +The file `aswitch.py` provides support for: + * `Delay_ms` A software retriggerable monostable or watchdog. + * `Switch` Debounced switch and pushbutton classes with callbacks. * `Pushbutton` -The following were workrounds for bugs and omissions in V2 which are now fixed. -They will be removed. - * The cancellation decorators and classes (cancellation works as per CPython). - * The nonstandard support for `gather` (now properly supported). +#### It is documented [here](./DRIVERS.md) -## 3.2 The current version V2.0 +# 2. Version 2.0 usage notes These notes are intended for users familiar with `asyncio` under CPython. @@ -168,13 +169,13 @@ It supports millisecond level timing with the following: Classes `Task` and `Future` are not supported. -## 3.2.1 Asynchronous I/O +## 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. -## 3.2.2 Time values +## 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 @@ -186,45 +187,3 @@ 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. - -# 4. The "fast_io" version. - -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. This issue is also addressed in Paul Sokolovsky's fork. - -## 4.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. - -# 5. The asyn.py library - -This library ([docs](./PRIMITIVES.md)) provides 'micro' implementations of the -`asyncio` synchronisation primitives. -[CPython docs](https://docs.python.org/3/library/asyncio-sync.html) - -It also supports a `Barrier` class to facilitate coroutine synchronisation. - -Coroutine cancellation is performed in an efficient manner in `uasyncio`. The -`asyn` library uses this, further enabling the cancelling coro to pause until -cancellation is complete. It also provides a means of checking the 'running' -status of individual coroutines. - -A lightweight implementation of `asyncio.gather` is provided. diff --git a/v3/README.md b/v3/README.md new file mode 100644 index 0000000..391faf0 --- /dev/null +++ b/v3/README.md @@ -0,0 +1,135 @@ +# 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`. + +These notes and the tutorial should be read in conjunction with +[the official docs](http://docs.micropython.org/en/latest/library/uasyncio.html) + +There is a new tutorial for V3. + +#### [V3 Tutorial](./TUTORIAL.md) + +# 2. Overview + +These notes are intended for users familiar with `asyncio` under CPython. + +The MicroPython language is based on CPython 3.4. The `uasyncio` 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: + + * `async def` and `await` syntax. + * Awaitable classes (using `__iter__` rather than `__await__`). + * Asynchronous context managers. + * Asynchronous iterators. + * `uasyncio.sleep(seconds)`. + * Timeouts (`uasyncio.wait_for`). + * Task cancellation (`Task.cancel`). + * Gather. + +It supports millisecond level timing with the following: + * `uasyncio.sleep_ms(time)` + +It includes the followiing CPython compatible synchronisation primitives: + * `Event`. + * `Lock`. + * `gather`. + +This repo includes code for the CPython primitives which are not yet officially +supported. + +The `Future` class is not supported, nor are the `event_loop` methods +`call_soon`, `call_later`, `call_at`. + +# 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` syntax and secondly +related to modules in this repository. + +## 3.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](./TUTORIAL.md#412-portable-code). + +## 3.2 Modules from this repository + +Modules `asyn.py` and `aswitch.py` are deprecated for V3 applications. See +[the tutorial](./TUTORIAL.md) for V3 replacements. + +### 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. + +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. + +### 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. + +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](./TUTORIAL.md). + +### 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. + +# 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/TUTORIAL.md b/v3/TUTORIAL.md new file mode 100644 index 0000000..9441079 --- /dev/null +++ b/v3/TUTORIAL.md @@ -0,0 +1,2261 @@ +# 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. + +#### WARNING currently this is a work in progress. + +This is a work in progress, with some sections being so marked. There will be +typos and maybe errors - please report errors of fact! Most code samples are +now complete scripts which can be cut and pasted at the REPL. + +# 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](./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.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.2.1 [The event's value](./TUTORIAL.md#321-the-events-value) + 3.3 [gather](./TUTORIAL.md#33-gather) + 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.7 [Barrier](./TUTORIAL.md#37-barrier) + 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.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-tasks-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 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.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 [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) + 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 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. +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. + +This tutorial aims to present a consistent programming style compatible with +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: +```python +import uasyncio +print(uasyncio.__version__) +``` +Version 3 will print a version number. Older versions will throw an exception. + +###### [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 + +**Primitives** + +The directory `primitives` contains a collection of synchronisation primitives +and classes for debouncing switches and pushbuttons, along with a software +retriggerable delay class. Pushbuttons are a generalisation of switches with +logical rather than physical status along with double-clicked and long pressed +events. + +These are implemented as a Python package: copy the `primitives` directory tree +to your hardware. + +**Demo Programs** + +The first two are the most immediately rewarding as they produce visible +results by accessing Pyboard hardware. + + 1. [aledflash.py](./demos/aledflash.py) Flashes three Pyboard LEDs + asynchronously for 10s. The simplest uasyncio demo. Import it to run. + 2. [apoll.py](./demos/apoll.py) A device driver for the Pyboard accelerometer. + Demonstrates the use of a task to poll a device. Runs for 20s. Import it to + run. Requires a Pyboard V1.x. + 3. [roundrobin.py](./demos/roundrobin.py) Demo of round-robin scheduling. Also + a benchmark of scheduling performance. + 4. [auart.py](./demos/auart.py) Demo of streaming I/O via a Pyboard UART. + 5. [auart_hd.py](./demos/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. + 6. [iorw.py](./demos/iorw.py) Demo of a read/write device driver using the + stream I/O mechanism. Requires a Pyboard. + 7. [A driver for GPS modules](./demos/gps/README.md) Runs a background task to + read and decode NMEA sentences, providing constantly updated position, course, + altitude and time/date information. + +###### [Contents](./TUTORIAL.md#contents) + +# 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 +with other coros. The illusion of concurrency is achieved by periodically +yielding to the scheduler, enabling other coros to be scheduled. + +## 2.1 Program structure + +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 + +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 +`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, +the scheduler would schedule them in periods when `bar` was paused: + +```python +import uasyncio as 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)) + await asyncio.sleep(10) + +asyncio.run(main()) +``` +In this example, three instances of `bar` run concurrently. The +`asyncio.create_task` method returns immediately but schedules the passed coro +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 +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 +`async def` and usually contains at least one `await` statement. This minimal +example waits 1 second before printing a message: + +```python +async def bar(): + await asyncio.sleep(1) + print('Done') +``` + +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 +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: + +```python +import uasyncio as asyncio +async def bar(t): + print('Bar started: waiting {}secs'.format(t)) + await asyncio.sleep(t) + print('Bar done') + +async def main(): + await bar(1) # Pauses here until bar is complete + task = asyncio.create_task(bar(5)) + await asyncio.sleep(0) # bar has now started + print('Got here: bar running') # Can run code here + await task # Now we wait for the bar task to complete + print('All done') +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 +`await` causes the passed `Task` or coro to run to completion before the next +line executes. 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 +tasks being scheduled for the 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. + +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. + +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 +concurrency: + +```python +import uasyncio as 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)) + print('Tasks are running') + await asyncio.sleep(10) + +asyncio.run(main()) +``` + +###### [Contents](./TUTORIAL.md#contents) + +### 2.2.1 Queueing a task for scheduling + + * `asyncio.create_task` Arg: the coro to run. The scheduler converts the coro + to a `Task` and queues the task to run ASAP. Return value: the `Task` + instance. It returns immediately. The coro arg is specified with function call + syntax with any required arguments passed. + * `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. + * `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. + +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 + +Callbacks should be Python functions designed to complete in a short period of +time. This is because tasks will have no opportunity to run for the +duration. If it is necessary to schedule a callback to run after `t` seconds, +it may be done as follows: +```python +async def schedule(cb, t, *args, **kwargs): + await asyncio.sleep(t) + cb(*args, **kwargs) +``` +In this example the callback runs after three seconds: +```python +import uasyncio as asyncio + +async def schedule(cbk, t, *args, **kwargs): + await asyncio.sleep(t) + cbk(*args, **kwargs) + +def callback(x, y): + print('x={} y={}'.format(x, y)) + +async def bar(): + asyncio.create_task(schedule(callback, 3, 42, 100)) + for count in range(6): + print(count) + await asyncio.sleep(1) # Pause 1s + +asyncio.run(bar()) +``` + +###### [Contents](./TUTORIAL.md#contents) + +### 2.2.3 Notes + +Coros may be bound methods. A coro usually contains at least one `await` +statement, but nothing will break (in MicroPython or CPython 3.8) if it has +none. + +Similarly to a function or method, a coro can contain a `return` statement. To +retrieve the returned data issue: + +```python +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: +```python +import uasyncio as asyncio + +async def bar(n): + for count in range(n): + await asyncio.sleep_ms(200 * n) # Pause by varying amounts + print('Instance {} stops with count = {}'.format(n, count)) + return count * count + +async def main(): + tasks = (bar(2), bar(3), bar(4)) + print('Waiting for gather...') + res = await asyncio.gather(*tasks) + print(res) + +asyncio.run(main()) +``` + +###### [Contents](./TUTORIAL.md#contents) + +## 2.3 Delays + +Where a delay is required in a task 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 tasks. +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 tasks while the delay is in progress. + +###### [Contents](./TUTORIAL.md#contents) + +# 3 Synchronisation + +There is often a need to provide synchronisation between tasks. A common +example is to avoid what are known as "race conditions" where multiple tasks +compete to access a single resource. These are discussed +[in section 7.8](./TUTORIAL.md#78-race-conditions). Another hazard is the +"deadly embrace" where two tasks 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. +CPython provides the following classes: + * `Lock` - already incorporated in new `uasyncio`. + * `Event` - already incorporated. + * `ayncio.gather` - already incorporated. + * `Semaphore` In this repository. + * `BoundedSemaphore`. In this repository. + * `Condition`. In this repository. + * `Queue`. In this repository. + +As the table above indicates, not all are yet officially supported. In the +interim, implementations may be found in the `primitives` directory, along with +the following classes: + * `Message` An ISR-friendly `Event` with an optional data payload. + * `Barrier` Based on a Microsoft class, enables multiple coros to synchronise + in a similar (but not identical) way to `gather`. + * `Delay_ms` A useful software-retriggerable monostable, akin to a watchdog. + Calls a user callback if not cancelled or regularly retriggered. + * `Switch` A debounced switch with open and close user callbacks. + * `Pushbutton` Debounced pushbutton with callbacks for pressed, released, long + press or double-press. + +To install these priitives, 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.pushbutton import Pushbutton +``` +When `uasyncio` acquires an official version (which will be more efficient) the +invocation line alone should be changed: +```python +from uasyncio import Semaphore, BoundedSemaphore +``` + +Another synchronisation issue arises with producer and consumer tasks. 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 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 +until the consumer is ready to access the data. + +The following provides a discussion of the primitives. + +###### [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 tasks +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 + +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 main(): + lock = asyncio.Lock() # The Lock instance + for n in range(1, 4): + asyncio.create_task(task(n, lock)) + await asyncio.sleep(10) + +asyncio.run(main()) # Run for 10s +``` + +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()`. + +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 + +async def task(i, lock): + while 1: + async with lock: + print("Acquired lock in task", i) + await asyncio.sleep(0.5) + +async def main(): + lock = asyncio.Lock() # The Lock instance + for n in range(1, 4): + asyncio.create_task(task(n, lock)) + await asyncio.sleep(10) + +asyncio.run(main()) # Run for 10s +``` + +###### [Contents](./TUTORIAL.md#contents) + +## 3.2 Event + +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 +continue. An `Event` object is instantiated and made accessible to all tasks +using it: + +```python +import uasyncio as asyncio +from uasyncio import Event + +event = Event() +async def waiter(): + 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()) + await asyncio.sleep(2) + print('Setting event') + event.set() + await asyncio.sleep(1) + # Caller can check if event has been cleared + print('Event is {}'.format('set' if event.is_set() else 'clear')) + +asyncio.run(main()) +``` +Constructor: no args. +Synchronous Methods: + * `set` Initiates the event. Currently may not be called in an interrupt + context. + * `clear` No args. Clears the event. + * `is_set` No args. Returns `True` if the event is set. + +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. + +**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. + +###### [Contents](./TUTORIAL.md#contents) + +## 3.3 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 +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) +``` +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 +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 + +```python +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +async def barking(n): + print('Start barking') + for _ in range(6): + await asyncio.sleep(1) + print('Done barking.') + return 2 * n + +async def foo(n): + print('Start timeout coro foo()') + while True: + await asyncio.sleep(1) + n += 1 + return n + +async def bar(n): + print('Start cancellable bar()') + while True: + await asyncio.sleep(1) + n += 1 + return n + +async def do_cancel(task): + await asyncio.sleep(5) + print('About to cancel bar') + task.cancel() + +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])) + res = None + try: + res = await asyncio.gather(*tasks, return_exceptions=True) + except asyncio.TimeoutError: # These only happen if return_exceptions is False + print('Timeout') # With the default times, cancellation occurs first + except asyncio.CancelledError: + print('Cancelled') + print('Result: ', res) + +asyncio.run(main()) +``` + +###### [Contents](./TUTORIAL.md#contents) + +## 3.4 Semaphore + +This is currently an unofficial implementation. Its API is as per CPython +asyncio. + +A semaphore limits the number of tasks which can access a resource. It can be +used to limit the number of instances of a particular task which can run +concurrently. It performs this using an access counter which is initialised by +the constructor and decremented each time a task 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 an asynchronous context manager. The +following illustrates tasks accessing a resource one at a time: + +```python +import uasyncio as asyncio +from primitives.semaphore import Semaphore + +async def foo(n, sema): + print('foo {} waiting for semaphore'.format(n)) + async with sema: + print('foo {} got semaphore'.format(n)) + await asyncio.sleep_ms(200) + +async def main(): + sema = Semaphore() + for num in range(3): + asyncio.create_task(foo(num, sema)) + await asyncio.sleep(2) + +asyncio.run(main()) +``` + +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. + +###### [Contents](./TUTORIAL.md#contents) + +### 3.4.1 BoundedSemaphore + +This is currently an unofficial implementation. Its API is as per CPython +asyncio. + +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](./TUTORIAL.md#contents) + +## 3.5 Queue + +This is currently an unofficial implementation. Its API is as per CPython +asyncio. + +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. + +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. + +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. + * `put_nowait` Arg: the object to put on the queue. Raises an exception if the + queue is full. + * `get_nowait` No arg. Returns an object from the queue. Raises an exception + if the queue is empty. + +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. + +```python +import uasyncio as asyncio +from primitives.queue import Queue + +async def slow_process(): + await asyncio.sleep(2) + return 42 + +async def produce(queue): + print('Waiting for slow process.') + result = await slow_process() + print('Putting result onto queue') + await queue.put(result) # Put result on queue + +async def consume(queue): + print("Running consume()") + result = await queue.get() # Blocks until data is ready + print('Result was {}'.format(result)) + +async def queue_go(delay): + queue = Queue() + asyncio.create_task(consume(queue)) + asyncio.create_task(produce(queue)) + await asyncio.sleep(delay) + print("Done") + +asyncio.run(queue_go(4)) +``` + +###### [Contents](./TUTORIAL.md#contents) + +## 3.6 Message + +This is an unofficial primitive and has no analog 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`. + * It is an awaitable class. + +The `.set()` method can accept an optional data value of any type. A 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 +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: + +```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. + +###### [Contents](./TUTORIAL.md#contents) + +## 3.7 Barrier + +I implemented this unofficial primitive before `uasyncio` had support for +`gather`. It is based on a Microsoft primitive. I doubt there is a role for it +in new applications but I will leave it in place to avoid breaking code. + +It two uses. Firstly it can cause 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 +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 +barrier to allow 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. + +```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) + +# 4 Designing classes for asyncio + +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 +on a UART or a user pressing a button should allow other tasks to be scheduled +until the event occurs. + +###### [Contents](./TUTORIAL.md#contents) + +## 4.1 Awaitable classes + +A task can pause execution by waiting on an `awaitable` object. There is a +difference between CPython and MicroPython in the way an `awaitable` class is +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. +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. + +```python +import uasyncio as asyncio + +class Foo(): + def __iter__(self): + for n in range(5): + print('__iter__ called') + yield from asyncio.sleep(1) # Other tasks get scheduled here + return 42 + +async def bar(): + foo = Foo() # Foo is an awaitable class + print('waiting for foo') + res = await foo # Retrieve result + print('done', res) + +asyncio.run(bar()) +``` + +### 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. + +###### [Contents](./TUTORIAL.md#contents) + +### 4.1.2 Portable code + +The Python language requires that `__await__` is a generator function. In +MicroPython generators and tasks are identical, so the solution is to use +`yield from task(args)`. + +This tutorial aims to offer code portable to CPython 3.8 or above. In CPython +tasks and generators are distinct. CPython tasks have an `__await__` special +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 + +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) + +asyncio.run(bar()) +``` + +In `__await__`, `yield from asyncio.sleep(1)` was allowed in CPython 3.6. In +V3.8 it produces a syntax error. It must now be put in the task as in the above +example. + +###### [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 task - 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 +import uasyncio as asyncio +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 tasks 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) +asyncio.run(run()) +``` + +###### [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 tasks. An example is the `Lock` +class. Such a class has an `__aenter__` task which is logically required to run +asynchronously. To support the asynchronous CM protocol its `__aexit__` method +also must be a task. Such classes are accessed from within a task with the +following syntax: +```python +async def bar(lock): + async with lock as obj: # "as" clause is optional, no real point for a lock + print('In context manager') +``` +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 tasks waiting on a task 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 synchronous method +``` +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 + +class Foo: + def __init__(self): + self.data = 0 + + async def acquire(self): + await asyncio.sleep(1) + return 42 + + async def __aenter__(self): + print('Waiting for data') + self.data = await self.acquire() + return self + + def close(self): + print('Exit') + + async def __aexit__(self, *args): + print('Waiting to quit') + await asyncio.sleep(1) # Can run asynchronous + self.close() # or synchronous methods + +async def bar(): + foo = Foo() + async with foo as f: + print('In context manager') + res = f.data + print('Done', res) + +asyncio.run(bar()) +``` + +###### [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 task, it should be trapped either in that task +or in a task 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, tasks launched with +`asyncio.create_task()` must trap any exceptions internally. + +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 +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') + +try: + asyncio.run(bar()) +except ZeroDivisionError: + asyncio.run(shutdown()) +except KeyboardInterrupt: + print('Keyboard interrupt at loop level.') + asyncio.run(shutdown()) +``` + +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 +in response to a keyboard interrupt should trap the exception at the outermost +scope. + +###### [Contents](./TUTORIAL.md#contents) + +## 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. However it is possible to trap the exception, for example to +perform cleanup code in a `finally` clause. The exception thrown to the coro +cannot be explicitly trapped (in CPython or MicroPython): `try-finally` must be +used. + +## 5.2.1 Task cancellation + +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 +async def printit(): + print('Got here') + await asyncio.sleep(1) + +async def foo(): + while True: + await printit() + print('In foo') + +async def bar(): + foo_task = asyncio.create_task(foo()) # Create task from task + await asyncio.sleep(4) # Show it running + foo_task.cancel() + await asyncio.sleep(0) + print('foo is now cancelled.') + await asyncio.sleep(4) # Proof! +asyncio.run(bar()) +``` +The exception may be trapped as follows +```python +import uasyncio as asyncio +async def printit(): + print('Got here') + await asyncio.sleep(1) + +async def foo(): + try: + while True: + await printit() + finally: + print('Cancelled') + +async def bar(): + foo_task = asyncio.create_task(foo()) + await asyncio.sleep(4) + foo_task.cancel() + await asyncio.sleep(0) + print('Task is now cancelled') +asyncio.run(bar()) +``` + +**Note** It is bad practice to issue the `close` or `throw` methods of a +de-scheduled task. This subverts the scheduler by causing the task 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 task and a timeout in seconds or ms +respectively. If the timeout expires an exception 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. The exception thrown to the coro cannot be +explicitly trapped (in CPython or MicroPython): `try-finally` must be used. + +```python +import uasyncio as asyncio + +async def forever(): + try: + print('Starting') + while True: + await asyncio.sleep_ms(300) + print('Got here') + finally: # Optional error trapping + print('forever timed out') # And return + +async def foo(): + try: + await asyncio.wait_for(forever(), 3) + except asyncio.TimeoutError: # Mandatory error trapping + print('foo got timeout') + await asyncio.sleep(2) + +asyncio.run(foo()) +``` + +###### [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 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. + +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-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 +[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 tasks each of +which yields with a zero delay. When I/O has been serviced it will next be +polled once all user tasks 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 tasks 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 task occurs more than once before the task is +actually scheduled. + +Another timing issue is the accuracy of delays. If a task 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 tasks are waiting on nonzero delays, the next line +will immediately be scheduled. But if other tasks 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 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 + +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 task 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 tasks are scheduled while we wait + rx_data = foo.read_record() + print('Got: {}'.format(rx_data)) + await foo.send_record(rx_data) + rx_data = b'' + +asyncio.run(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) + +async def main(): + rx = asyncio.create_task(receiver()) + tx = asyncio.create_task(sender()) + await asyncio.sleep(10) + print('Quitting') + tx.cancel() + rx.cancel() + await asyncio.sleep(1) + print('Done') + +asyncio.run(main()) +``` + +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 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. + +### 6.3.1 A UART driver example + +The program [auart_hd.py](./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. + +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 tasks each polling a device, partly because `select` is written in C +but also because the task 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 + +async def timer_test(n): + timer = MillisecTimer() + for x in range(n): + await timer(100) # Pause 100ms + print(x) + +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 +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. + +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 task 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) + asyncio.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 latency can be high: if implemented fast I/O scheduling will improve +this. + +The demo program [iorw.py](./demos/iorw.py) illustrates a complete example. + +###### [Contents](./TUTORIAL.md#contents) + +## 6.5 A complete example: aremote.py + +**TODO** Not yet ported to V3. + +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 task 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 task to compensate for +any `asyncio` latency when setting its delay period. + +###### [Contents](./TUTORIAL.md#contents) + +## 6.6 HTU21D environment sensor + +**TODO** Not yet ported to V3. + +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 tasks 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 task 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 task 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 task 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). +A coro instance is created and discarded, typically leading to a program +silently failing to run correctly: + +```python +import uasyncio as asyncio +async def foo(): + await asyncio.sleep(1) + print('done') + +async def main(): + foo() # Should read: await foo + +asyncio.run(main()) +``` + +###### [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 CPython compatibility and the event loop + +The samples in this tutorial are compatible with CPython 3.8. If you need +compatibility with versions 3.5 or above, the `asyncio.run()` method is absent. +Replace: +```python +asyncio.run(my_task()) +``` +with: +```python +loop = asyncio.get_event_loop() +loop.run_forever(my_task()) +``` +The `create_task` method is a member of the `event_loop` instance. Replace +```python +asyncio.create_task(my_task()) +``` +with +```python +loop = asyncio.get_event_loop() +loop.create_task(my_task()) +``` +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 +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). + +This doc offers better alternatives to `get_event_loop` if you can confine +support to CPython V3.8+. + +## 7.8 Race conditions + +These occur when coroutines compete for access to a resource, each using the +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 +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 +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. + +###### [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 + asyncio.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 main(): + leds = [LED_async(n) for n in range(1, 4)] + for n, led in enumerate(leds): + led.flash(0.7 + n/4) + sw = pyb.Switch() + while not sw.value(): + await asyncio.sleep_ms(100) + +asyncio.run(main()) +``` + +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 +asyncio.run(main()) # Execution passes to tasks. + # It only continues here once main() 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, +known as a task. A task normally includes 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 task 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 +tasks 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 task +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 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 +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 task 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 task 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 task, you can be +sure that variables won't suddenly be changed by another task: your task 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 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. + +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 task 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/v3/__init__.py b/v3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v3/demos/aledflash.py b/v3/demos/aledflash.py new file mode 100644 index 0000000..c621f97 --- /dev/null +++ b/v3/demos/aledflash.py @@ -0,0 +1,30 @@ +# aledflash.py Demo/test program for MicroPython asyncio +# Author: Peter Hinch +# Copyright Peter Hinch 2020 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): + duration = int(duration) + if duration > 0: + print("Flash LED's for {:3d} seconds".format(duration)) + leds = [pyb.LED(x) for x in range(1,4)] # Initialise three on board LED's + for x, led in enumerate(leds): # Create a coroutine for each LED + t = int((0.2 + x/2) * 1000) + asyncio.create_task(toggle(leds[x], t)) + asyncio.run(killer(duration)) + +test(10) diff --git a/v3/demos/apoll.py b/v3/demos/apoll.py new file mode 100644 index 0000000..54d218f --- /dev/null +++ b/v3/demos/apoll.py @@ -0,0 +1,61 @@ +# Demonstration of a device driver using a coroutine to poll a dvice. +# 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.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 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 = time.ticks_ms() + return 0 + return time.ticks_diff(time.ticks_ms(), 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(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 + 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.') + asyncio.create_task(accel_coro()) + await asyncio.sleep(delay) + print('Test complete!') + +asyncio.run(main(20)) diff --git a/v3/demos/auart.py b/v3/demos/auart.py new file mode 100644 index 0000000..c685c59 --- /dev/null +++ b/v3/demos/auart.py @@ -0,0 +1,28 @@ +# Test of uasyncio stream I/O using UART +# Author: Peter Hinch +# Copyright Peter Hinch 2017-2020 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) + +async def main(): + asyncio.create_task(sender()) + asyncio.create_task(receiver()) + while True: + await asyncio.sleep(1) + +asyncio.run(main()) diff --git a/v3/demos/auart_hd.py b/v3/demos/auart_hd.py new file mode 100644 index 0000000..10393ef --- /dev/null +++ b/v3/demos/auart_hd.py @@ -0,0 +1,106 @@ +# auart_hd.py +# Author: Peter Hinch +# Copyright Peter Hinch 2018-2020 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 +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): + 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'] + 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.swriter = asyncio.StreamWriter(self.uart, {}) + self.sreader = asyncio.StreamReader(self.uart) + self.delay = Delay_ms() + self.response = [] + asyncio.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.') + master = Master() + device = Device() + 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.') + +def printexp(): + st = '''Expected output: +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. +''' + print('\x1b[32m') + print(st) + print('\x1b[39m') + +printexp() +asyncio.run(test()) + diff --git a/v3/demos/gather.py b/v3/demos/gather.py new file mode 100644 index 0000000..c422888 --- /dev/null +++ b/v3/demos/gather.py @@ -0,0 +1,49 @@ +# gather.py Demo of Gatherable coroutines. Includes 3 cases: +# 1. A normal coro +# 2. A coro with a timeout +# 3. A cancellable coro + +import uasyncio as asyncio + +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 Exception as e: #asyncio.TimeoutError: + print('foo timeout.', e) + return n + +async def bar(n): + print('Start cancellable bar()') + try: + while True: + await asyncio.sleep(1) + n += 1 + except Exception as e: + print('bar stopped.', e) + return n + +async def do_cancel(task): + await asyncio.sleep(5) + print('About to cancel bar') + task.cancel() + +async def main(): + bar_task = asyncio.create_task(bar(70)) # Note args here + tasks = [] + tasks.append(barking(21)) + tasks.append(asyncio.wait_for(foo(10), 7)) + asyncio.create_task(do_cancel(bar_task)) + res = await asyncio.gather(*tasks) + print('Result: ', res) + +asyncio.run(main()) diff --git a/v3/demos/gps/LICENSE b/v3/demos/gps/LICENSE new file mode 100644 index 0000000..798b35f --- /dev/null +++ b/v3/demos/gps/LICENSE @@ -0,0 +1,21 @@ +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/v3/demos/gps/README.md b/v3/demos/gps/README.md new file mode 100644 index 0000000..d858051 --- /dev/null +++ b/v3/demos/gps/README.md @@ -0,0 +1,907 @@ +# 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]. + +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. + +## 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.8 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.8+. + * 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) | Y | +| Rx | X1 (U4 tx) | | + +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. + +## 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 + +asyncio.run(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) + +asyncio.run(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.8 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.8 +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.8+ (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) + +asyncio.run(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])) + +asyncio.run(test()) +``` + +## 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.8+ or MicroPython. + * `astests_pyb.py` Test with synthetic data on UART. GPS hardware replaced by + a loopback on UART 4. Requires a Pyboard. + +# 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/v3/demos/gps/as_GPS.py b/v3/demos/gps/as_GPS.py new file mode 100644 index 0000000..64974a5 --- /dev/null +++ b/v3/demos/gps/as_GPS.py @@ -0,0 +1,615 @@ +# 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 + +# Ported to uasyncio V3 OK. + +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 + asyncio.create_task(self._run()) + + ########################################## + # Data Stream Handler Functions + ########################################## + + async def _run(self): + while True: + res = await self._sreader.readline() + try: + res = res.decode('utf8') + except UnicodeError: # Garbage: can happen e.g. on baudrate change + continue + asyncio.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/v3/demos/gps/as_GPS_time.py b/v3/demos/gps/as_GPS_time.py new file mode 100644 index 0000000..60be132 --- /dev/null +++ b/v3/demos/gps/as_GPS_time.py @@ -0,0 +1,166 @@ +# 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): + 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)) + 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() + asyncio.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): + 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.') + gps = await setup() + print('Waiting for time data.') + await gps.ready() + print('Setting RTC.') + await gps.set_rtc() + terminate = asyn.Event() + asyncio.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): + 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). +# 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() + asyncio.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): + asyncio.run(do_usec(minutes)) diff --git a/v3/demos/gps/as_GPS_utils.py b/v3/demos/gps/as_GPS_utils.py new file mode 100644 index 0000000..7deb5d6 --- /dev/null +++ b/v3/demos/gps/as_GPS_utils.py @@ -0,0 +1,48 @@ +# 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/v3/demos/gps/as_rwGPS.py b/v3/demos/gps/as_rwGPS.py new file mode 100644 index 0000000..2cb5540 --- /dev/null +++ b/v3/demos/gps/as_rwGPS.py @@ -0,0 +1,118 @@ +# 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/v3/demos/gps/as_rwGPS_time.py b/v3/demos/gps/as_rwGPS_time.py new file mode 100644 index 0000000..27c0bca --- /dev/null +++ b/v3/demos/gps/as_rwGPS_time.py @@ -0,0 +1,230 @@ +# 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. + +# Copyright (c) 2018-2020 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 +from uasyncio import Event +from primitives.message import Message +import pyb +import utime +import math +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): + 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)) + 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 = Event() + asyncio.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): + try: + asyncio.run(do_drift(minutes)) + 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.') + gps = await setup() + print('Waiting for time data.') + await gps.ready() + print('Setting RTC.') + await gps.set_rtc() + print('RTC is set.') + terminate = Event() + asyncio.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): + 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). +# 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 = Message() + 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 = Event() + asyncio.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): + try: + asyncio.run(do_usec(minutes)) + finally: + asyncio.run(shutdown()) diff --git a/v3/demos/gps/as_tGPS.py b/v3/demos/gps/as_tGPS.py new file mode 100644 index 0000000..e68d20c --- /dev/null +++ b/v3/demos/gps/as_tGPS.py @@ -0,0 +1,240 @@ +# 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. + 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) + + 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/v3/demos/gps/ast_pb.py b/v3/demos/gps/ast_pb.py new file mode 100644 index 0000000..b029b83 --- /dev/null +++ b/v3/demos/gps/ast_pb.py @@ -0,0 +1,98 @@ +# 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,)) + print('awaiting first fix') + asyncio.create_task(sat_test(gps)) + asyncio.create_task(stats(gps)) + asyncio.create_task(navigation(gps)) + asyncio.create_task(course(gps)) + asyncio.create_task(date(gps)) + + +asyncio.run(gps_test()) diff --git a/v3/demos/gps/ast_pbrw.py b/v3/demos/gps/ast_pbrw.py new file mode 100644 index 0000000..d38b814 --- /dev/null +++ b/v3/demos/gps/ast_pbrw.py @@ -0,0 +1,171 @@ +# 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, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) +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) + blue.toggle() + 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') + asyncio.create_task(sat_test(gps)) + asyncio.create_task(stats(gps)) + asyncio.create_task(navigation(gps)) + asyncio.create_task(course(gps)) + asyncio.create_task(date(gps)) + await gps.data_received(True, True, True, True) # all messages + asyncio.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) + +try: + asyncio.run(gps_test()) +finally: + asyncio.run(shutdown()) diff --git a/v3/demos/gps/astests.py b/v3/demos/gps/astests.py new file mode 100755 index 0000000..e59cff8 --- /dev/null +++ b/v3/demos/gps/astests.py @@ -0,0 +1,177 @@ +#!/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(): + asyncio.run(run()) + +if __name__ == "__main__": + run_tests() diff --git a/v3/demos/gps/astests_pyb.py b/v3/demos/gps/astests_pyb.py new file mode 100755 index 0000000..d7b914d --- /dev/null +++ b/v3/demos/gps/astests_pyb.py @@ -0,0 +1,151 @@ +# 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-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +# Ported to uasyncio V3 OK. + +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) + +asyncio.run(run_tests()) diff --git a/v3/demos/gps/baud.py b/v3/demos/gps/baud.py new file mode 100644 index 0000000..e3c2eb7 --- /dev/null +++ b/v3/demos/gps/baud.py @@ -0,0 +1,55 @@ +# baud.py Test uasyncio at high baudrate +import pyb +import uasyncio as asyncio +import utime +import as_rwGPS +# Outcome +# Sleep Buffer +# 0 None OK, length limit 74 +# 10 None Bad: length 111 also short weird RMC sentences +# 10 1000 OK, length 74, 37 +# 10 200 Bad: 100, 37 overruns +# 10 400 OK, 74,24 Short GSV sentence looked OK +# 4 200 OK, 74,35 Emulate parse time + +# as_GPS.py +# As written update blocks for 23.5ms parse for 3.8ms max +# with CRC check removed update blocks 17.3ms max +# CRC, bad char and line length removed update blocks 8.1ms max + +# At 10Hz update rate I doubt there's enough time to process the data +BAUDRATE = 115200 +red, green, yellow, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) + +async def setup(): + print('Initialising') + uart = pyb.UART(4, 9600) + sreader = asyncio.StreamReader(uart) + swriter = asyncio.StreamWriter(uart, {}) + gps = as_rwGPS.GPS(sreader, swriter, local_offset=1) + await asyncio.sleep(2) + await gps.baudrate(BAUDRATE) + uart.init(BAUDRATE) + +def setbaud(): + asyncio.run(setup()) + print('Baudrate set to 115200.') + +async def gps_test(): + print('Initialising') + uart = pyb.UART(4, BAUDRATE, read_buf_len=400) + sreader = asyncio.StreamReader(uart) + swriter = asyncio.StreamWriter(uart, {}) + maxlen = 0 + minlen = 100 + while True: + res = await sreader.readline() + l = len(res) + maxlen = max(maxlen, l) + minlen = min(minlen, l) + print(l, maxlen, minlen, res) + red.toggle() + utime.sleep_ms(10) + +def test(): + asyncio.run(gps_test()) diff --git a/v3/demos/gps/log.kml b/v3/demos/gps/log.kml new file mode 100644 index 0000000..31d1076 --- /dev/null +++ b/v3/demos/gps/log.kml @@ -0,0 +1,128 @@ + + + + +#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/v3/demos/gps/log_kml.py b/v3/demos/gps/log_kml.py new file mode 100644 index 0000000..ee7338d --- /dev/null +++ b/v3/demos/gps/log_kml.py @@ -0,0 +1,75 @@ +# log_kml.py Log GPS data to a kml file for display on Google Earth + +# Copyright (c) Peter Hinch 2018 +# 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/ + +# 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, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) +sw = pyb.Switch() + +# Toggle the red LED +def toggle_led(gps): + 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') + blue.toggle() + for _ in range(interval * 10): + await asyncio.sleep_ms(100) + if sw.value(): + break + + f.write(str_end) + red.off() + green.on() + +asyncio.run(log_kml()) diff --git a/v3/demos/iorw.py b/v3/demos/iorw.py new file mode 100644 index 0000000..be53965 --- /dev/null +++ b/v3/demos/iorw.py @@ -0,0 +1,118 @@ +# iorw.py Emulate a device which can read and write one character at a time. + +# 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=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) + +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) + +def printexp(): + st = '''Received b'ready\\n' +Received b'ready\\n' +Received b'ready\\n' +Received b'ready\\n' +Received b'ready\\n' +Wrote Hello MyIO 1 +Received b'ready\\n' +Received b'ready\\n' +Received b'ready\\n' +Wrote Hello MyIO 2 +Received b'ready\\n' +... +Runs until interrupted (ctrl-c). +''' + print('\x1b[32m') + print(st) + print('\x1b[39m') + +printexp() +myio = MyIO() +asyncio.create_task(receiver(myio)) +asyncio.run(sender(myio)) diff --git a/v3/demos/rate.py b/v3/demos/rate.py new file mode 100644 index 0000000..3e0c301 --- /dev/null +++ b/v3/demos/rate.py @@ -0,0 +1,46 @@ +# rate.py Benchmark for uasyncio. Author Peter Hinch Feb 2018-Apr 2020. +# Benchmark uasyncio round-robin scheduling performance +# This measures the rate at which uasyncio can schedule a minimal coro which +# mereley increments a global. + +# Outcome on a Pyboard 1.1 +# 100 minimal coros are scheduled at an interval of 195μs on uasyncio V3 +# Compares with ~156μs on official uasyncio V2. + +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 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)) + count = 0 + for _ in range(n_coros - old_n): + asyncio.create_task(foo()) + old_n = n_coros + await asyncio.sleep(duration) + 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]))) + +asyncio.run(report()) + diff --git a/v3/demos/roundrobin.py b/v3/demos/roundrobin.py new file mode 100644 index 0000000..35a79cd --- /dev/null +++ b/v3/demos/roundrobin.py @@ -0,0 +1,34 @@ +# roundrobin.py Test/demo of round-robin scheduling +# Author: Peter Hinch +# Copyright Peter Hinch 2017-2020 Released under the MIT license + +# Result on Pyboard 1.1 with print('Foo', n) commented out +# executions/second 5575.6 on uasyncio V3 + +# uasyncio V2 produced the following results +# 4249 - with a hack where sleep_ms(0) was replaced with yield +# Using sleep_ms(0) 2750 + +import uasyncio as asyncio + +count = 0 +period = 5 + + +async def foo(n): + global count + while True: + await asyncio.sleep_ms(0) + count += 1 + print('Foo', n) + + +async def main(delay): + for n in range(1, 4): + asyncio.create_task(foo(n)) + print('Testing for {} seconds'.format(period)) + await asyncio.sleep(delay) + + +asyncio.run(main(period)) +print('Coro executions per sec =', count/period) diff --git a/v3/primitives/__init__.py b/v3/primitives/__init__.py new file mode 100644 index 0000000..fb4bd3c --- /dev/null +++ b/v3/primitives/__init__.py @@ -0,0 +1,19 @@ +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) + diff --git a/v3/primitives/barrier.py b/v3/primitives/barrier.py new file mode 100644 index 0000000..5aa73a9 --- /dev/null +++ b/v3/primitives/barrier.py @@ -0,0 +1,68 @@ +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +from . import launch + +# 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') diff --git a/v3/primitives/condition.py b/v3/primitives/condition.py new file mode 100644 index 0000000..3172642 --- /dev/null +++ b/v3/primitives/condition.py @@ -0,0 +1,63 @@ +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +# Condition class +# from primitives.condition import Condition + +class Condition(): + def __init__(self, lock=None): + self.lock = asyncio.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): + await 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 = asyncio.Event() + self.events.append(ev) + self.lock.release() + await ev.wait() + 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 diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py new file mode 100644 index 0000000..f7593b9 --- /dev/null +++ b/v3/primitives/delay_ms.py @@ -0,0 +1,60 @@ +import uasyncio as asyncio +import utime as time +from . import launch +# Usage: +# from primitives.delay_ms import Delay_ms + +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 + 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() + + 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 + asyncio.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 diff --git a/v3/primitives/message.py b/v3/primitives/message.py new file mode 100644 index 0000000..d685758 --- /dev/null +++ b/v3/primitives/message.py @@ -0,0 +1,64 @@ +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. +#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) + + __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 diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py new file mode 100644 index 0000000..a22d231 --- /dev/null +++ b/v3/primitives/pushbutton.py @@ -0,0 +1,97 @@ +import uasyncio as asyncio +import utime as time +from . import launch +from primitives.delay_ms import Delay_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 + asyncio.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/v3/primitives/queue.py b/v3/primitives/queue.py new file mode 100644 index 0000000..1166bb4 --- /dev/null +++ b/v3/primitives/queue.py @@ -0,0 +1,66 @@ +# queue.py: adapted from uasyncio V2 +# Code is based on Paul Sokolovsky's work. +# This is a temporary solution until uasyncio V3 gets an efficient official version + +import uasyncio as asyncio + + +# Exception raised by get_nowait(). +class QueueEmpty(Exception): + pass + + +# Exception raised by put_nowait(). +class QueueFull(Exception): + pass + +class Queue: + + def __init__(self, maxsize=0): + self.maxsize = maxsize + self._queue = [] + + def _get(self): + return self._queue.pop(0) + + async def get(self): # Usage: item = await queue.get() + while self.empty(): + # Queue is empty, put the calling Task on the waiting queue + await asyncio.sleep_ms(0) + return self._get() + + 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() + return self._get() + + def _put(self, val): + self._queue.append(val) + + async def put(self, val): # Usage: await queue.put(item) + while self.qsize() >= self.maxsize and self.maxsize: + # Queue full + await asyncio.sleep_ms(0) + # Task(s) waiting to get from queue, schedule first Task + self._put(val) + + def put_nowait(self, val): # Put an item into the queue without blocking. + if self.qsize() >= self.maxsize and self.maxsize: + raise QueueFull() + self._put(val) + + def qsize(self): # Number of items in the queue. + return len(self._queue) + + 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 diff --git a/v3/primitives/semaphore.py b/v3/primitives/semaphore.py new file mode 100644 index 0000000..99f3a66 --- /dev/null +++ b/v3/primitives/semaphore.py @@ -0,0 +1,37 @@ +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +# 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') diff --git a/v3/primitives/switch.py b/v3/primitives/switch.py new file mode 100644 index 0000000..e25d65b --- /dev/null +++ b/v3/primitives/switch.py @@ -0,0 +1,37 @@ +import uasyncio as asyncio +import utime as time +from . import launch + +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 + asyncio.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) diff --git a/v3/primitives/tests/__init__.py b/v3/primitives/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v3/primitives/tests/asyntest.py b/v3/primitives/tests/asyntest.py new file mode 100644 index 0000000..e6fcb17 --- /dev/null +++ b/v3/primitives/tests/asyntest.py @@ -0,0 +1,404 @@ +# 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) +# To run: +# from primitives.tests.asyntest import test + +try: + import uasyncio as asyncio +except ImportError: + import asyncio + +from primitives.message import Message +from primitives.barrier import Barrier +from primitives.semaphore import Semaphore, BoundedSemaphore +from primitives.condition import Condition + +def print_tests(): + st = '''Available functions: +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. + +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 Message class ************ +# 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() + +async def run_ack(): + 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)) + message.set(count) + count += 1 + print('message was set') + await ack1 + ack1.clear() + print('Cleared ack1') + await ack2 + ack2.clear() + print('Cleared ack2') + message.clear() + print('Cleared message') + await asyncio.sleep(1) + +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...") + +def ack_test(): + printexp('''message was set +message_wait 1 got message with value 0 +message_wait 2 got message with value 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 +Cleared ack1 +Cleared ack2 +Cleared 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)) + +# ************ Test Lock and Message 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 messageset(message): + print('Waiting 5 secs before setting message') + await asyncio.sleep(5) + message.set() + print('message was set') + +async def messagewait(message): + print('waiting for message') + await message + print('got message') + message.clear() + +async def run_message_test(): + print('Test Lock class') + lock = asyncio.Lock() + asyncio.create_task(run_lock(1, lock)) + asyncio.create_task(run_lock(2, lock)) + asyncio.create_task(run_lock(3, lock)) + print('Test Message class') + message = Message() + asyncio.create_task(messageset(message)) + await messagewait(message) # run_message_test runs fast until this point + print('Message status {}'.format('Incorrect' if message.is_set() else 'OK')) + print('Tasks complete') + +def msg_test(): + printexp('''Test Lock class +Test Message class +waiting for message +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 message +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 +message was set +got message +Message status OK +Tasks complete +''', 5) + asyncio.run(run_message_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 = Barrier(3, callback, ('Synch',)) + for _ in range(3): + asyncio.create_task(report(barrier)) + asyncio.run(killer(2)) + +# ************ 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 + barrier = Barrier(num_coros + 1) + if bounded: + semaphore = BoundedSemaphore(3) + else: + semaphore = Semaphore(3) + for n in range(num_coros): + asyncio.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) + asyncio.run(run_sema_test(bounded)) + +# ************ Condition test ************ + +cond = Condition() +tim = 0 + +async def cond01(): + while True: + await asyncio.sleep(2) + with await cond: + cond.notify(2) # Notify 2 tasks + +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 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(): + ntasks = 7 + barrier = Barrier(ntasks + 1) + t1 = asyncio.create_task(cond01()) + t3 = asyncio.create_task(cond03()) + for n in range(ntasks): + asyncio.create_task(cond02(n, barrier)) + await barrier # All instances of cond02 have completed + # Test wait_for + barrier = Barrier(2) + asyncio.create_task(cond04(99, barrier)) + await barrier + # cancel continuously running coros. + t1.cancel() + t3.cancel() + await asyncio.sleep_ms(0) + print('Done.') + +def condition_test(): + 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) + asyncio.run(cond_go()) + +# ************ Queue test ************ + +from primitives.queue 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 queue_go(delay): + asyncio.create_task(foo()) + asyncio.create_task(bar()) + await asyncio.sleep(delay) + print("I've seen starships burn off the shoulder of Orion...") + print("Time to die...") + +def queue_test(): + printexp('''Running (runtime = 3s): +Running foo() +Waiting for slow process. +Putting result onto queue +I've seen starships burn off the shoulder of Orion... +Time to die... +''', 3) + asyncio.run(queue_go(3)) + +def test(n): + if n == 0: + print_tests() # Print this list. + elif n == 1: + ack_test() # Test message acknowledge. + elif n == 2: + msg_test() # Test Messge and Lock objects. + elif n == 3: + barrier_test() # Test the Barrier class. + elif n == 4: + semaphore_test(False) # Test Semaphore + elif n == 5: + semaphore_test(True) # Test BoundedSemaphore. + elif n == 6: + condition_test() # Test the Condition class. + elif n == 7: + queue_test() # Test the Queue class. diff --git a/v3/primitives/tests/switches.py b/v3/primitives/tests/switches.py new file mode 100644 index 0000000..b90f92a --- /dev/null +++ b/v3/primitives/tests/switches.py @@ -0,0 +1,137 @@ +# Test/demo programs for Switch and Pushbutton classes +# Tested on Pyboard but should run on other microcontroller platforms +# running MicroPython with uasyncio library. +# Author: Peter Hinch. +# Copyright Peter Hinch 2017-2020 Released under the MIT license. + +# To run: +# from primitives.tests.switches import * +# test_sw() # For example + +from machine import Pin +from pyb import LED +from primitives.switch import Switch +from primitives.pushbutton import 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()) From e1e8c2bd87bb2da35c43bdca0c996266b187813e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 11 Apr 2020 08:43:42 +0100 Subject: [PATCH 153/472] v3/TUTORIAL.md Fix error in section 5.1. --- v3/TUTORIAL.md | 58 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/v3/TUTORIAL.md b/v3/TUTORIAL.md index 9441079..bc4afd3 100644 --- a/v3/TUTORIAL.md +++ b/v3/TUTORIAL.md @@ -1171,24 +1171,52 @@ asyncio.run(bar()) ###### [Contents](./TUTORIAL.md#contents) -# 5 Exceptions timeouts and cancellation +# 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. +application of a timeout to a task, by throwing an exception to the task. ## 5.1 Exceptions -Where an exception occurs in a task, it should be trapped either in that task -or in a task 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, tasks launched with -`asyncio.create_task()` must trap any exceptions internally. +Consider a task `foo` created with `asyncio.create_task(foo())`. This task +might `await` other tasks, with potential nesting. If an exception occurs, it +will propagate up the chain until it reaches `foo`. This behaviour is as per +function calls: the exception propagates up the call chain until trapped. If +the exception is not trapped, the `foo` task stops with a traceback. Crucially +other tasks continue to run. -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 -queued for execution. +This does not apply to the main task started with `asyncio.run`. If an +exception propagates to that task, the scheduler will stop. This can be +demonstrated as follows: + +```python +import uasyncio as asyncio + +async def bar(): + await asyncio.sleep(0) + 1/0 + +async def foo(): + await asyncio.sleep(0) + print('Running bar') + await bar() + print('Does not print') # Because bar() raised an exception + +async def main(): + asyncio.create_task(foo()) + for _ in range(5): + print('Working') # Carries on after the exception + await asyncio.sleep(0.5) + 1/0 # Stops the scheduler + await asyncio.sleep(0) + print('This never happens') + await asyncio.sleep(0) + +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 stop. There is a "gotcha" illustrated by this code sample. If allowed to run to completion it works as expected. @@ -1230,6 +1258,12 @@ transferred to the scheduler. Consequently applications requiring cleanup code in response to a keyboard interrupt should trap the exception at the outermost scope. +#### Warning + +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. + ###### [Contents](./TUTORIAL.md#contents) ## 5.2 Cancellation and Timeouts From 82e0fb24dc169f136349420da21ed0414b80832d Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 11 Apr 2020 14:44:38 +0100 Subject: [PATCH 154/472] v3/TUTORIAL.md Fix errors re exception handling. --- v3/TUTORIAL.md | 101 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 30 deletions(-) diff --git a/v3/TUTORIAL.md b/v3/TUTORIAL.md index bc4afd3..cc0d092 100644 --- a/v3/TUTORIAL.md +++ b/v3/TUTORIAL.md @@ -41,9 +41,11 @@ now complete scripts which can be cut and pasted at the REPL. 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.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-tasks-with-timeouts) + 5.2.2 [Tasks with timeouts](./TUTORIAL.md#522-tasks-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 task](./TUTORIAL.md#62-polling-hardware-with-a-task) @@ -1171,7 +1173,7 @@ asyncio.run(bar()) ###### [Contents](./TUTORIAL.md#contents) -# 5. Exceptions timeouts and cancellation +# 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. @@ -1194,7 +1196,7 @@ import uasyncio as asyncio async def bar(): await asyncio.sleep(0) - 1/0 + 1/0 # Crash async def foo(): await asyncio.sleep(0) @@ -1218,8 +1220,47 @@ 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 stop. -There is a "gotcha" illustrated by this code sample. If allowed to run to -completion it works as expected. +#### 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 +queued for execution. + +### 5.1.1 Global exception handler + +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. +```python +import uasyncio as asyncio +import sys + +def _handle_exception(loop, context): + print('Global handler') + sys.print_exception(context["exception"]) + #loop.stop() + sys.exit() # Drastic, but loop.stop() does not currently work + +loop = asyncio.get_event_loop() +loop.set_exception_handler(_handle_exception) + +async def bar(): + await asyncio.sleep(0) + 1/0 # Crash + +async def main(): + asyncio.create_task(bar()) + for _ in range(5): + print('Working') + await asyncio.sleep(0.5) + +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. ```python import uasyncio as asyncio @@ -1258,22 +1299,20 @@ transferred to the scheduler. Consequently applications requiring cleanup code in response to a keyboard interrupt should trap the exception at the outermost scope. -#### Warning - -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. - ###### [Contents](./TUTORIAL.md#contents) ## 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. However it is possible to trap the exception, for example to -perform cleanup code in a `finally` clause. The exception thrown to the coro -cannot be explicitly trapped (in CPython or MicroPython): `try-finally` must be -used. +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. ## 5.2.1 Task cancellation @@ -1297,6 +1336,7 @@ async def bar(): await asyncio.sleep(0) print('foo is now cancelled.') await asyncio.sleep(4) # Proof! + asyncio.run(bar()) ``` The exception may be trapped as follows @@ -1310,8 +1350,11 @@ async def foo(): try: while True: await printit() - finally: - print('Cancelled') + except asyncio.CancelledError: + print('Trapped cancelled error.') + raise # Enable check in outer scope + finally: # Usual way to do cleanup + print('Cancelled - finally') async def bar(): foo_task = asyncio.create_task(foo()) @@ -1322,21 +1365,16 @@ async def bar(): asyncio.run(bar()) ``` -**Note** It is bad practice to issue the `close` or `throw` methods of a -de-scheduled task. This subverts the scheduler by causing the task to execute -code even though descheduled. This is likely to have unwanted consequences. - ###### [Contents](./TUTORIAL.md#contents) -## 5.2.2 Coroutines with timeouts +## 5.2.2 Tasks with timeouts 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 an exception 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. The exception thrown to the coro cannot be -explicitly trapped (in CPython or MicroPython): `try-finally` must be used. +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 +exception will interrupt program execution. ```python import uasyncio as asyncio @@ -1347,14 +1385,17 @@ async def forever(): while True: await asyncio.sleep_ms(300) print('Got here') - finally: # Optional error trapping - print('forever timed out') # And return + except asyncio.CancelledError: # Task sees CancelledError + print('Trapped cancelled error.') + raise + finally: # Usual way to do cleanup + print('forever timed out') async def foo(): try: await asyncio.wait_for(forever(), 3) except asyncio.TimeoutError: # Mandatory error trapping - print('foo got timeout') + print('foo got timeout') # Caller sees TimeoutError await asyncio.sleep(2) asyncio.run(foo()) From 9770d62939d8f1ac2fb7a6bc6e31c918728447a7 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 11 Apr 2020 16:24:37 +0100 Subject: [PATCH 155/472] v3/TUTORIAL.md Add note re global exception handler. --- v3/TUTORIAL.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/v3/TUTORIAL.md b/v3/TUTORIAL.md index cc0d092..6e2e570 100644 --- a/v3/TUTORIAL.md +++ b/v3/TUTORIAL.md @@ -1230,7 +1230,7 @@ queued for execution. 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. +global exception handler. This debug aid is not CPython compatible: ```python import uasyncio as asyncio import sys @@ -1239,16 +1239,15 @@ def _handle_exception(loop, context): print('Global handler') sys.print_exception(context["exception"]) #loop.stop() - sys.exit() # Drastic, but loop.stop() does not currently work - -loop = asyncio.get_event_loop() -loop.set_exception_handler(_handle_exception) + sys.exit() # Drastic - loop.stop() does not work when used this way async def bar(): await asyncio.sleep(0) 1/0 # Crash async def main(): + loop = asyncio.get_event_loop() + loop.set_exception_handler(_handle_exception) asyncio.create_task(bar()) for _ in range(5): print('Working') From d184e6e944950decb12a9ed9b845853964a25d76 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 14 Apr 2020 09:26:07 +0100 Subject: [PATCH 156/472] Initial GPS changes. --- gps/README.md | 14 +++++++++++--- gps/ast_pbrw.py | 3 +-- gps/log_kml.py | 9 +++++---- v3/demos/gps/README.md | 21 +++++++++++++++------ v3/demos/gps/as_GPS_time.py | 27 +++++++++++++-------------- v3/demos/gps/ast_pb.py | 6 +++--- v3/demos/gps/ast_pbrw.py | 18 +++++++++--------- v3/demos/gps/log_kml.py | 5 ++--- 8 files changed, 59 insertions(+), 44 deletions(-) diff --git a/gps/README.md b/gps/README.md index 8530be0..2803187 100644 --- a/gps/README.md +++ b/gps/README.md @@ -64,14 +64,22 @@ the Pyboard is run from a voltage >5V the Pyboard 3V3 pin should be used. | Vin | V+ or 3V3 | | | Gnd | Gnd | | | PPS | X3 | Y | -| Tx | X2 (U4 rx) | Y | -| Rx | X1 (U4 tx) | | +| 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. @@ -906,4 +914,4 @@ duration over which it is measured. [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 \ No newline at end of file +[micropyGPS]:https://github.com/inmcm/micropyGPS.git diff --git a/gps/ast_pbrw.py b/gps/ast_pbrw.py index ec5a760..2fdda30 100644 --- a/gps/ast_pbrw.py +++ b/gps/ast_pbrw.py @@ -20,7 +20,7 @@ # Avoid multiple baudrates. Tests use 9600 or 19200 only. BAUDRATE = 19200 -red, green, yellow, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) +red, green, yellow = pyb.LED(1), pyb.LED(2), pyb.LED(3) ntimeouts = 0 def callback(gps, _, timer): @@ -89,7 +89,6 @@ async def date(gps): while True: await asyncio.sleep(4) await gps.data_received(date=True) - blue.toggle() print('***** DATE AND TIME *****') print('Data is Valid:', hex(gps._valid)) print('UTC Time:', gps.utc) diff --git a/gps/log_kml.py b/gps/log_kml.py index 8fed4c6..3d13548 100644 --- a/gps/log_kml.py +++ b/gps/log_kml.py @@ -1,11 +1,13 @@ # log_kml.py Log GPS data to a kml file for display on Google Earth -# Copyright (c) Peter Hinch 2018 +# 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 @@ -39,11 +41,11 @@ ''' -red, green, yellow, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) +red, green, yellow = pyb.LED(1), pyb.LED(2), pyb.LED(3) sw = pyb.Switch() # Toggle the red LED -def toggle_led(gps): +def toggle_led(*_): red.toggle() async def log_kml(fn='/sd/log.kml', interval=10): @@ -62,7 +64,6 @@ async def log_kml(fn='/sd/log.kml', interval=10): f.write(',') f.write(str(gps.altitude)) f.write('\r\n') - blue.toggle() for _ in range(interval * 10): await asyncio.sleep_ms(100) if sw.value(): diff --git a/v3/demos/gps/README.md b/v3/demos/gps/README.md index d858051..b12eea1 100644 --- a/v3/demos/gps/README.md +++ b/v3/demos/gps/README.md @@ -7,6 +7,8 @@ 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. +**UNDER REVIEW: API MAY CHANGE** + ## 1.1 Driver characteristics * Asynchronous: UART messaging is handled as a background task allowing the @@ -67,14 +69,22 @@ the Pyboard is run from a voltage >5V the Pyboard 3V3 pin should be used. | Vin | V+ or 3V3 | | | Gnd | Gnd | | | PPS | X3 | Y | -| Tx | X2 (U4 rx) | Y | -| Rx | X1 (U4 tx) | | +| 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. @@ -160,7 +170,7 @@ 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. +`as_GPS_time.py` requires the `primitives` package. ### 1.4.2 Python 3.8 or later @@ -436,7 +446,6 @@ 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 @@ -635,13 +644,13 @@ import as_tGPS async def test(): fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}' red = pyb.LED(1) - blue = pyb.LED(4) + green = pyb.LED(2) 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()) + pps_cb=lambda *_: green.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 diff --git a/v3/demos/gps/as_GPS_time.py b/v3/demos/gps/as_GPS_time.py index 60be132..6adeed6 100644 --- a/v3/demos/gps/as_GPS_time.py +++ b/v3/demos/gps/as_GPS_time.py @@ -2,17 +2,16 @@ # 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 +# Copyright (c) 2018-2020 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 +from primitives.message import Message # Hardware assumptions. Change as required. PPS_PIN = pyb.Pin.board.X3 @@ -25,16 +24,16 @@ 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. +# Setup for tests. Red LED toggles on fix, green on PPS interrupt. async def setup(): red = pyb.LED(1) - blue = pyb.LED(4) + green = pyb.LED(2) 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()) + pps_cb=lambda *_: green.toggle()) # Test terminator: task sets the passed event after the passed time. async def killer(end_event, minutes): @@ -66,7 +65,7 @@ async def do_drift(minutes): gps = await setup() print('Waiting for time data.') await gps.ready() - terminate = asyn.Event() + terminate = asyncio.Event() asyncio.create_task(killer(terminate, minutes)) print('Setting RTC.') await gps.set_rtc() @@ -90,7 +89,7 @@ async def do_time(minutes): await gps.ready() print('Setting RTC.') await gps.set_rtc() - terminate = asyn.Event() + terminate = asyncio.Event() asyncio.create_task(killer(terminate, minutes)) while not terminate.is_set(): await asyncio.sleep(1) @@ -115,7 +114,7 @@ def time(minutes=1): 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() + # Trigger Message. 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() @@ -123,16 +122,16 @@ def us_cb(my_gps, tick, led): # Setup initialises with above callback async def us_setup(tick): red = pyb.LED(1) - blue = pyb.LED(4) + yellow = pyb.LED(3) 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)) + pps_cb=us_cb, pps_cb_args=(tick, yellow)) async def do_usec(minutes): - tick = asyn.Event() + tick = Message() print('Setting up GPS.') gps = await us_setup(tick) print('Waiting for time data.') @@ -142,10 +141,10 @@ async def do_usec(minutes): sd = 0 nsamples = 0 count = 0 - terminate = asyn.Event() + terminate = asyncio.Event() asyncio.create_task(killer(terminate, minutes)) while not terminate.is_set(): - await tick + await tick.wait() usecs = tick.value() tick.clear() err = 1000000 - usecs diff --git a/v3/demos/gps/ast_pb.py b/v3/demos/gps/ast_pb.py index b029b83..bf8831e 100644 --- a/v3/demos/gps/ast_pb.py +++ b/v3/demos/gps/ast_pb.py @@ -7,7 +7,7 @@ import pyb import uasyncio as asyncio -import aswitch +from primitives.delay_ms import Delay_ms import as_GPS red = pyb.LED(1) @@ -84,7 +84,7 @@ async def gps_test(): 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) + timer = Delay_ms(timeout) sentence_count = 0 gps = as_GPS.AS_GPS(sreader, local_offset=1, fix_cb=callback, fix_cb_args=(timer,)) print('awaiting first fix') @@ -92,7 +92,7 @@ async def gps_test(): asyncio.create_task(stats(gps)) asyncio.create_task(navigation(gps)) asyncio.create_task(course(gps)) - asyncio.create_task(date(gps)) + await date(gps) asyncio.run(gps_test()) diff --git a/v3/demos/gps/ast_pbrw.py b/v3/demos/gps/ast_pbrw.py index d38b814..0344a61 100644 --- a/v3/demos/gps/ast_pbrw.py +++ b/v3/demos/gps/ast_pbrw.py @@ -1,26 +1,25 @@ # 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 +# Copyright (c) Peter Hinch 2018-2020 # 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: coroutine has 4s loop delay. # Yellow toggles on position reading. -# Blue toggles on date valid. import pyb import uasyncio as asyncio -import aswitch +from primitives.delay_ms import Delay_ms import as_GPS import as_rwGPS # Avoid multiple baudrates. Tests use 9600 or 19200 only. BAUDRATE = 19200 -red, green, yellow, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) +red, green, yellow = pyb.LED(1), pyb.LED(2), pyb.LED(3) ntimeouts = 0 def callback(gps, _, timer): @@ -58,7 +57,7 @@ async def stats(gps): print('Sentences Parsed:', gps.parsed_sentences) print('CRC_Fails:', gps.crc_fails) print('Antenna status:', gps.antenna) - print('Firmware vesrion:', gps.version) + print('Firmware version:', gps.version) print('Enabled sentences:', gps.enabled) print() @@ -89,7 +88,6 @@ async def date(gps): while True: await asyncio.sleep(4) await gps.data_received(date=True) - blue.toggle() print('***** DATE AND TIME *****') print('Data is Valid:', hex(gps._valid)) print('UTC Time:', gps.utc) @@ -136,7 +134,7 @@ async def gps_test(): # read_buf_len is precautionary: code runs reliably without it. sreader = asyncio.StreamReader(uart) swriter = asyncio.StreamWriter(uart, {}) - timer = aswitch.Delay_ms(cb_timeout) + timer = 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) @@ -152,7 +150,7 @@ async def gps_test(): asyncio.create_task(course(gps)) asyncio.create_task(date(gps)) await gps.data_received(True, True, True, True) # all messages - asyncio.create_task(change_status(gps, uart)) + await change_status(gps, uart) async def shutdown(): # Normally UART is already at BAUDRATE. But if last session didn't restore @@ -167,5 +165,7 @@ async def shutdown(): try: asyncio.run(gps_test()) +except KeyboardInterrupt: + print('Interrupted') finally: asyncio.run(shutdown()) diff --git a/v3/demos/gps/log_kml.py b/v3/demos/gps/log_kml.py index ee7338d..8e7035e 100644 --- a/v3/demos/gps/log_kml.py +++ b/v3/demos/gps/log_kml.py @@ -39,11 +39,11 @@ ''' -red, green, yellow, blue = pyb.LED(1), pyb.LED(2), pyb.LED(3), pyb.LED(4) +red, green, yellow = pyb.LED(1), pyb.LED(2), pyb.LED(3) sw = pyb.Switch() # Toggle the red LED -def toggle_led(gps): +def toggle_led(*_): red.toggle() async def log_kml(fn='/sd/log.kml', interval=10): @@ -62,7 +62,6 @@ async def log_kml(fn='/sd/log.kml', interval=10): f.write(',') f.write(str(gps.altitude)) f.write('\r\n') - blue.toggle() for _ in range(interval * 10): await asyncio.sleep_ms(100) if sw.value(): From db6e4e3a9c0068dab47bcd3942112ba717558b97 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 15 Apr 2020 16:46:01 +0100 Subject: [PATCH 157/472] GPS and HTU21D ported to V3 --- v3/README.md | 33 +- v3/{demos => as_demos}/aledflash.py | 0 v3/{demos => as_demos}/apoll.py | 0 v3/{demos => as_demos}/auart.py | 0 v3/{demos => as_demos}/auart_hd.py | 0 v3/{demos => as_demos}/gather.py | 0 v3/{demos => as_demos}/iorw.py | 0 v3/{demos => as_demos}/rate.py | 0 v3/{demos => as_demos}/roundrobin.py | 0 v3/as_drivers/as_GPS/__init__.py | 1 + v3/{demos/gps => as_drivers/as_GPS}/as_GPS.py | 4 +- .../gps => as_drivers/as_GPS}/as_GPS_time.py | 10 +- .../gps => as_drivers/as_GPS}/as_GPS_utils.py | 2 +- .../gps => as_drivers/as_GPS}/as_rwGPS.py | 2 +- .../as_GPS}/as_rwGPS_time.py | 10 +- .../gps => as_drivers/as_GPS}/as_tGPS.py | 18 +- v3/{demos/gps => as_drivers/as_GPS}/ast_pb.py | 12 +- .../gps => as_drivers/as_GPS}/ast_pbrw.py | 32 +- .../gps => as_drivers/as_GPS}/astests.py | 30 +- .../gps => as_drivers/as_GPS}/astests_pyb.py | 28 +- v3/{demos/gps => as_drivers/as_GPS}/baud.py | 2 +- v3/{demos/gps => as_drivers/as_GPS}/log.kml | 0 .../gps => as_drivers/as_GPS}/log_kml.py | 8 +- v3/as_drivers/htu21d/__init__.py | 1 + v3/as_drivers/htu21d/htu21d_mc.py | 63 ++++ v3/as_drivers/htu21d/htu_test.py | 32 ++ v3/as_drivers/nec_ir/__init__.py | 1 + v3/as_drivers/nec_ir/aremote.py | 122 +++++++ v3/as_drivers/nec_ir/art.py | 47 +++ v3/as_drivers/nec_ir/art1.py | 60 ++++ v3/demos/gps/LICENSE | 21 -- v3/{demos/gps/README.md => docs/GPS.md} | 327 +++++++++++------- v3/docs/HTU21D.md | 50 +++ v3/docs/NEC_IR.md | 133 +++++++ v3/{ => docs}/TUTORIAL.md | 87 +++-- 35 files changed, 872 insertions(+), 264 deletions(-) rename v3/{demos => as_demos}/aledflash.py (100%) rename v3/{demos => as_demos}/apoll.py (100%) rename v3/{demos => as_demos}/auart.py (100%) rename v3/{demos => as_demos}/auart_hd.py (100%) rename v3/{demos => as_demos}/gather.py (100%) rename v3/{demos => as_demos}/iorw.py (100%) rename v3/{demos => as_demos}/rate.py (100%) rename v3/{demos => as_demos}/roundrobin.py (100%) create mode 100644 v3/as_drivers/as_GPS/__init__.py rename v3/{demos/gps => as_drivers/as_GPS}/as_GPS.py (99%) rename v3/{demos/gps => as_drivers/as_GPS}/as_GPS_time.py (95%) rename v3/{demos/gps => as_drivers/as_GPS}/as_GPS_utils.py (97%) rename v3/{demos/gps => as_drivers/as_GPS}/as_rwGPS.py (99%) rename v3/{demos/gps => as_drivers/as_GPS}/as_rwGPS_time.py (96%) rename v3/{demos/gps => as_drivers/as_GPS}/as_tGPS.py (94%) rename v3/{demos/gps => as_drivers/as_GPS}/ast_pb.py (89%) rename v3/{demos/gps => as_drivers/as_GPS}/ast_pbrw.py (88%) rename v3/{demos/gps => as_drivers/as_GPS}/astests.py (89%) rename v3/{demos/gps => as_drivers/as_GPS}/astests_pyb.py (87%) rename v3/{demos/gps => as_drivers/as_GPS}/baud.py (97%) rename v3/{demos/gps => as_drivers/as_GPS}/log.kml (100%) rename v3/{demos/gps => as_drivers/as_GPS}/log_kml.py (91%) create mode 100644 v3/as_drivers/htu21d/__init__.py create mode 100644 v3/as_drivers/htu21d/htu21d_mc.py create mode 100644 v3/as_drivers/htu21d/htu_test.py create mode 100644 v3/as_drivers/nec_ir/__init__.py create mode 100644 v3/as_drivers/nec_ir/aremote.py create mode 100644 v3/as_drivers/nec_ir/art.py create mode 100644 v3/as_drivers/nec_ir/art1.py delete mode 100644 v3/demos/gps/LICENSE rename v3/{demos/gps/README.md => docs/GPS.md} (86%) create mode 100644 v3/docs/HTU21D.md create mode 100644 v3/docs/NEC_IR.md rename v3/{ => docs}/TUTORIAL.md (96%) diff --git a/v3/README.md b/v3/README.md index 391faf0..f1e0b3c 100644 --- a/v3/README.md +++ b/v3/README.md @@ -1,4 +1,4 @@ -# 1, Guide to uasyncio V3 +# 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 @@ -7,11 +7,24 @@ 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) -There is a new tutorial for V3. +## 1.1 Resources for V3 -#### [V3 Tutorial](./TUTORIAL.md) +This repo contains the following: -# 2. Overview +#### [V3 Tutorial](./docs/TUTORIAL.md) +#### Test/demo scripts + +Documented in the tutorial. + +#### Synchronisation primitives + +Documented in the tutorial. + +#### Asynchronous device drivers +The device drivers are in the process of being ported. Currently there is a +[GPS driver](./docs/GPS.md). + +# 2 V3 Overview These notes are intended for users familiar with `asyncio` under CPython. @@ -34,7 +47,7 @@ The `uasyncio` library supports the following features: It supports millisecond level timing with the following: * `uasyncio.sleep_ms(time)` -It includes the followiing CPython compatible synchronisation primitives: +It includes the following CPython compatible synchronisation primitives: * `Event`. * `Lock`. * `gather`. @@ -45,7 +58,7 @@ supported. The `Future` class is not supported, nor are the `event_loop` methods `call_soon`, `call_later`, `call_at`. -# 3. Porting applications from V2 +# 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` syntax and secondly @@ -65,12 +78,12 @@ related to modules in this repository. It is possible to write an awaitable class with code portable between MicroPython and CPython 3.8. This is discussed -[in the tutorial](./TUTORIAL.md#412-portable-code). +[in the tutorial](./docs/TUTORIAL.md#412-portable-code). ## 3.2 Modules from this repository Modules `asyn.py` and `aswitch.py` are deprecated for V3 applications. See -[the tutorial](./TUTORIAL.md) for V3 replacements. +[the tutorial](./docs/TUTORIAL.md#3-synchronisation) for V3 replacements. ### 3.2.1 Synchronisation primitives @@ -101,7 +114,7 @@ used with V3: * 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](./TUTORIAL.md). +in [the tutorial](./docs/TUTORIAL.md#36-message). ### 3.2.3 Switches, Pushbuttons and delays (old aswitch.py) @@ -114,7 +127,7 @@ New versions are provided in this repository. Classes: * `Switch` Debounced switch with close and open callbacks. * `Pushbutton` Pushbutton with double-click and long press callbacks. -# 4. Outstanding issues with V3 +# 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. diff --git a/v3/demos/aledflash.py b/v3/as_demos/aledflash.py similarity index 100% rename from v3/demos/aledflash.py rename to v3/as_demos/aledflash.py diff --git a/v3/demos/apoll.py b/v3/as_demos/apoll.py similarity index 100% rename from v3/demos/apoll.py rename to v3/as_demos/apoll.py diff --git a/v3/demos/auart.py b/v3/as_demos/auart.py similarity index 100% rename from v3/demos/auart.py rename to v3/as_demos/auart.py diff --git a/v3/demos/auart_hd.py b/v3/as_demos/auart_hd.py similarity index 100% rename from v3/demos/auart_hd.py rename to v3/as_demos/auart_hd.py diff --git a/v3/demos/gather.py b/v3/as_demos/gather.py similarity index 100% rename from v3/demos/gather.py rename to v3/as_demos/gather.py diff --git a/v3/demos/iorw.py b/v3/as_demos/iorw.py similarity index 100% rename from v3/demos/iorw.py rename to v3/as_demos/iorw.py diff --git a/v3/demos/rate.py b/v3/as_demos/rate.py similarity index 100% rename from v3/demos/rate.py rename to v3/as_demos/rate.py diff --git a/v3/demos/roundrobin.py b/v3/as_demos/roundrobin.py similarity index 100% rename from v3/demos/roundrobin.py rename to v3/as_demos/roundrobin.py diff --git a/v3/as_drivers/as_GPS/__init__.py b/v3/as_drivers/as_GPS/__init__.py new file mode 100644 index 0000000..e7979ed --- /dev/null +++ b/v3/as_drivers/as_GPS/__init__.py @@ -0,0 +1 @@ +from .as_GPS import * diff --git a/v3/demos/gps/as_GPS.py b/v3/as_drivers/as_GPS/as_GPS.py similarity index 99% rename from v3/demos/gps/as_GPS.py rename to v3/as_drivers/as_GPS/as_GPS.py index 64974a5..5e4b6dc 100644 --- a/v3/demos/gps/as_GPS.py +++ b/v3/as_drivers/as_GPS/as_GPS.py @@ -555,7 +555,7 @@ def time_since_fix(self): # ms since last valid fix 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 + from .as_GPS_utils import compass_direction return compass_direction(self) def latitude_string(self, coord_format=DM): @@ -611,5 +611,5 @@ def time_string(self, local=True): return '{:02d}:{:02d}:{:02d}'.format(hrs, mins, secs) def date_string(self, formatting=MDY): - from as_GPS_utils import date_string + from .as_GPS_utils import date_string return date_string(self, formatting) diff --git a/v3/demos/gps/as_GPS_time.py b/v3/as_drivers/as_GPS/as_GPS_time.py similarity index 95% rename from v3/demos/gps/as_GPS_time.py rename to v3/as_drivers/as_GPS/as_GPS_time.py index 6adeed6..b26a86b 100644 --- a/v3/demos/gps/as_GPS_time.py +++ b/v3/as_drivers/as_GPS/as_GPS_time.py @@ -10,7 +10,7 @@ import pyb import utime import math -import as_tGPS +from .as_tGPS import GPS_Timer from primitives.message import Message # Hardware assumptions. Change as required. @@ -31,7 +31,7 @@ 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 as_tGPS.GPS_Timer(sreader, pps_pin, local_offset=1, + return GPS_Timer(sreader, pps_pin, local_offset=1, fix_cb=lambda *_: red.toggle(), pps_cb=lambda *_: green.toggle()) @@ -126,9 +126,9 @@ 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 as_tGPS.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() diff --git a/v3/demos/gps/as_GPS_utils.py b/v3/as_drivers/as_GPS/as_GPS_utils.py similarity index 97% rename from v3/demos/gps/as_GPS_utils.py rename to v3/as_drivers/as_GPS/as_GPS_utils.py index 7deb5d6..6993baf 100644 --- a/v3/demos/gps/as_GPS_utils.py +++ b/v3/as_drivers/as_GPS/as_GPS_utils.py @@ -4,7 +4,7 @@ # Copyright (c) 2018 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -from as_GPS import MDY, DMY, LONG +from .as_GPS import MDY, DMY, LONG _DIRECTIONS = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW') diff --git a/v3/demos/gps/as_rwGPS.py b/v3/as_drivers/as_GPS/as_rwGPS.py similarity index 99% rename from v3/demos/gps/as_rwGPS.py rename to v3/as_drivers/as_GPS/as_rwGPS.py index 2cb5540..3fb4b8b 100644 --- a/v3/demos/gps/as_rwGPS.py +++ b/v3/as_drivers/as_GPS/as_rwGPS.py @@ -7,7 +7,7 @@ # Copyright (c) 2018 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file -import as_GPS +import as_drivers.as_GPS as as_GPS try: from micropython import const except ImportError: diff --git a/v3/demos/gps/as_rwGPS_time.py b/v3/as_drivers/as_GPS/as_rwGPS_time.py similarity index 96% rename from v3/demos/gps/as_rwGPS_time.py rename to v3/as_drivers/as_GPS/as_rwGPS_time.py index 27c0bca..b5fa239 100644 --- a/v3/demos/gps/as_rwGPS_time.py +++ b/v3/as_drivers/as_GPS/as_rwGPS_time.py @@ -18,8 +18,8 @@ import pyb import utime import math -import as_tGPS -import as_rwGPS +from .as_tGPS import GPS_RWTimer +from .as_rwGPS import FULL_COLD_START # Hardware assumptions. Change as required. PPS_PIN = pyb.Pin.board.X3 @@ -46,7 +46,7 @@ async def shutdown(): # session with ctrl-c. uart.init(BAUDRATE) await asyncio.sleep(0.5) - await gps.command(as_rwGPS.FULL_COLD_START) + await gps.command(FULL_COLD_START) print('Factory reset') gps.close() # Stop ISR #print('Restoring default baudrate (9600).') @@ -67,7 +67,7 @@ async def setup(): 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, + gps = GPS_RWTimer(sreader, swriter, pps_pin, local_offset=1, fix_cb=lambda *_: red.toggle(), pps_cb=lambda *_: blue.toggle()) gps.FULL_CHECK = False @@ -179,7 +179,7 @@ async def us_setup(tick): 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, + 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 diff --git a/v3/demos/gps/as_tGPS.py b/v3/as_drivers/as_GPS/as_tGPS.py similarity index 94% rename from v3/demos/gps/as_tGPS.py rename to v3/as_drivers/as_GPS/as_tGPS.py index e68d20c..542d3bf 100644 --- a/v3/demos/gps/as_tGPS.py +++ b/v3/as_drivers/as_GPS/as_tGPS.py @@ -2,7 +2,7 @@ # This is STM-specific: requires pyb module. # Hence not as RAM-critical as as_GPS -# Copyright (c) 2018 Peter Hinch +# Copyright (c) 2018-2020 Peter Hinch # Released under the MIT License (MIT) - see LICENSE file # TODO Test machine version. Replace LED with callback. Update tests and doc. @@ -17,8 +17,8 @@ import utime import micropython import gc -import as_GPS -import as_rwGPS +from .as_GPS import RMC, AS_GPS +from .as_rwGPS import GPS micropython.alloc_emergency_exception_buf(100) @@ -31,17 +31,17 @@ def rtc_secs(): # 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=(), + fix_cb=lambda *_ : None, cb_mask=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) + 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=(), + fix_cb=lambda *_ : None, cb_mask=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, + 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) @@ -236,5 +236,5 @@ def get_t_split(self): 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}) +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/demos/gps/ast_pb.py b/v3/as_drivers/as_GPS/ast_pb.py similarity index 89% rename from v3/demos/gps/ast_pb.py rename to v3/as_drivers/as_GPS/ast_pb.py index bf8831e..a4a2a6f 100644 --- a/v3/demos/gps/ast_pb.py +++ b/v3/as_drivers/as_GPS/ast_pb.py @@ -8,7 +8,7 @@ import pyb import uasyncio as asyncio from primitives.delay_ms import Delay_ms -import as_GPS +from .as_GPS import DD, MPH, LONG, AS_GPS red = pyb.LED(1) green = pyb.LED(2) @@ -52,8 +52,8 @@ async def navigation(gps): 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('Longitude:', gps.longitude(DD)) + print('Latitude', gps.latitude(DD)) print() async def course(gps): @@ -62,7 +62,7 @@ async def course(gps): await gps.data_received(course=True) print('***** COURSE DATA *****') print('Data is Valid:', gps._valid) - print('Speed:', gps.speed_string(as_GPS.MPH)) + print('Speed:', gps.speed_string(MPH)) print('Course', gps.course) print('Compass Direction:', gps.compass_direction()) print() @@ -75,7 +75,7 @@ async def date(gps): 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('Date:', gps.date_string(LONG)) print() async def gps_test(): @@ -86,7 +86,7 @@ async def gps_test(): sreader = asyncio.StreamReader(uart) timer = Delay_ms(timeout) sentence_count = 0 - gps = as_GPS.AS_GPS(sreader, local_offset=1, fix_cb=callback, fix_cb_args=(timer,)) + gps = AS_GPS(sreader, local_offset=1, fix_cb=callback, fix_cb_args=(timer,)) print('awaiting first fix') asyncio.create_task(sat_test(gps)) asyncio.create_task(stats(gps)) diff --git a/v3/demos/gps/ast_pbrw.py b/v3/as_drivers/as_GPS/ast_pbrw.py similarity index 88% rename from v3/demos/gps/ast_pbrw.py rename to v3/as_drivers/as_GPS/ast_pbrw.py index 0344a61..6c994b8 100644 --- a/v3/demos/gps/ast_pbrw.py +++ b/v3/as_drivers/as_GPS/ast_pbrw.py @@ -1,4 +1,4 @@ -# ast_pb.py +# ast_pbrw.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 @@ -14,8 +14,8 @@ import pyb import uasyncio as asyncio from primitives.delay_ms import Delay_ms -import as_GPS -import as_rwGPS +from .as_GPS import DD, LONG, MPH +from .as_rwGPS import * # Avoid multiple baudrates. Tests use 9600 or 19200 only. BAUDRATE = 19200 @@ -69,8 +69,8 @@ async def navigation(gps): 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('Longitude:', gps.longitude(DD)) + print('Latitude', gps.latitude(DD)) print() async def course(gps): @@ -79,7 +79,7 @@ async def course(gps): 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('Speed:', gps.speed_string(MPH)) print('Course', gps.course) print('Compass Direction:', gps.compass_direction()) print() @@ -92,7 +92,7 @@ async def date(gps): 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('Date:', gps.date_string(LONG)) print() async def change_status(gps, uart): @@ -103,10 +103,10 @@ async def change_status(gps, uart): print('***** baudrate 19200 *****') await asyncio.sleep(5) # Ensure baudrate is sorted print('***** Query VERSION *****') - await gps.command(as_rwGPS.VERSION) + await gps.command(VERSION) await asyncio.sleep(10) print('***** Query ENABLE *****') - await gps.command(as_rwGPS.ENABLE) + await gps.command(ENABLE) await asyncio.sleep(10) # Allow time for 1st report await gps.update_interval(2000) print('***** Update interval 2s *****') @@ -115,14 +115,14 @@ async def change_status(gps, uart): print('***** Disable satellite in view and channel messages *****') await asyncio.sleep(10) print('***** Query ENABLE *****') - await gps.command(as_rwGPS.ENABLE) + await gps.command(ENABLE) # See README.md re antenna commands # await asyncio.sleep(10) -# await gps.command(as_rwGPS.ANTENNA) +# await gps.command(ANTENNA) # print('***** Antenna reports requested *****') # await asyncio.sleep(60) -# await gps.command(as_rwGPS.NO_ANTENNA) +# await gps.command(NO_ANTENNA) # print('***** Antenna reports turned off *****') # await asyncio.sleep(10) @@ -136,12 +136,12 @@ async def gps_test(): swriter = asyncio.StreamWriter(uart, {}) timer = Delay_ms(cb_timeout) sentence_count = 0 - gps = as_rwGPS.GPS(sreader, swriter, local_offset=1, fix_cb=callback, + 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(as_rwGPS.DEFAULT_SENTENCES) + await gps.command(DEFAULT_SENTENCES) print('Set sentence frequencies to default') - #await gps.command(as_rwGPS.FULL_COLD_START) + #await gps.command(FULL_COLD_START) #print('Performed FULL_COLD_START') print('awaiting first fix') asyncio.create_task(sat_test(gps)) @@ -158,7 +158,7 @@ async def shutdown(): # session with ctrl-c. uart.init(BAUDRATE) await asyncio.sleep(1) - await gps.command(as_rwGPS.FULL_COLD_START) + await gps.command(FULL_COLD_START) print('Factory reset') #print('Restoring default baudrate.') #await gps.baudrate(9600) diff --git a/v3/demos/gps/astests.py b/v3/as_drivers/as_GPS/astests.py similarity index 89% rename from v3/demos/gps/astests.py rename to v3/as_drivers/as_GPS/astests.py index e59cff8..59dba46 100755 --- a/v3/demos/gps/astests.py +++ b/v3/as_drivers/as_GPS/astests.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3.5 +#!/usr/bin/env python3.8 # -*- coding: utf-8 -*- # astests.py @@ -10,7 +10,7 @@ # Released under the MIT License (MIT) - see LICENSE file # Run under CPython 3.5+ or MicroPython -import as_GPS +from .as_GPS import * try: import uasyncio as asyncio except ImportError: @@ -45,7 +45,7 @@ async def run(): '$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) + my_gps = AS_GPS(None) sentence = '' for sentence in test_RMC: my_gps._valid = 0 @@ -146,20 +146,20 @@ async def run(): 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):', 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(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('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() diff --git a/v3/demos/gps/astests_pyb.py b/v3/as_drivers/as_GPS/astests_pyb.py similarity index 87% rename from v3/demos/gps/astests_pyb.py rename to v3/as_drivers/as_GPS/astests_pyb.py index d7b914d..b2e4b12 100755 --- a/v3/demos/gps/astests_pyb.py +++ b/v3/as_drivers/as_GPS/astests_pyb.py @@ -11,7 +11,7 @@ # Ported to uasyncio V3 OK. -import as_GPS +from .as_GPS import * from machine import UART import uasyncio as asyncio @@ -49,7 +49,7 @@ async def run_tests(): # '$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,)) + my_gps = AS_GPS(sreader, fix_cb=callback, fix_cb_args=(42,)) sentence = '' for sentence in test_RMC: sentence_count += 1 @@ -124,20 +124,20 @@ async def run_tests(): 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):', 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(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('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() diff --git a/v3/demos/gps/baud.py b/v3/as_drivers/as_GPS/baud.py similarity index 97% rename from v3/demos/gps/baud.py rename to v3/as_drivers/as_GPS/baud.py index e3c2eb7..6832f6d 100644 --- a/v3/demos/gps/baud.py +++ b/v3/as_drivers/as_GPS/baud.py @@ -2,7 +2,7 @@ import pyb import uasyncio as asyncio import utime -import as_rwGPS +import as_drivers.as_rwGPS as as_rwGPS # Outcome # Sleep Buffer # 0 None OK, length limit 74 diff --git a/v3/demos/gps/log.kml b/v3/as_drivers/as_GPS/log.kml similarity index 100% rename from v3/demos/gps/log.kml rename to v3/as_drivers/as_GPS/log.kml diff --git a/v3/demos/gps/log_kml.py b/v3/as_drivers/as_GPS/log_kml.py similarity index 91% rename from v3/demos/gps/log_kml.py rename to v3/as_drivers/as_GPS/log_kml.py index 8e7035e..22279bf 100644 --- a/v3/demos/gps/log_kml.py +++ b/v3/as_drivers/as_GPS/log_kml.py @@ -8,7 +8,7 @@ # Logging stops and the file is closed when the user switch is pressed. -import as_GPS +from .as_GPS import KML, AS_GPS import uasyncio as asyncio import pyb @@ -50,15 +50,15 @@ 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) + 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(gps.longitude_string(KML)) f.write(',') - f.write(gps.latitude_string(as_GPS.KML)) + f.write(gps.latitude_string(KML)) f.write(',') f.write(str(gps.altitude)) f.write('\r\n') diff --git a/v3/as_drivers/htu21d/__init__.py b/v3/as_drivers/htu21d/__init__.py new file mode 100644 index 0000000..ca5a992 --- /dev/null +++ b/v3/as_drivers/htu21d/__init__.py @@ -0,0 +1 @@ +from .htu21d_mc import * diff --git a/v3/as_drivers/htu21d/htu21d_mc.py b/v3/as_drivers/htu21d/htu21d_mc.py new file mode 100644 index 0000000..2d74a95 --- /dev/null +++ b/v3/as_drivers/htu21d/htu21d_mc.py @@ -0,0 +1,63 @@ +# 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-2020 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 + asyncio.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 from asyncio.sleep(0) + + 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/v3/as_drivers/htu21d/htu_test.py b/v3/as_drivers/htu21d/htu_test.py new file mode 100644 index 0000000..ee820b9 --- /dev/null +++ b/v3/as_drivers/htu21d/htu_test.py @@ -0,0 +1,32 @@ +# 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 +from .htu21d_mc import HTU21D + +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(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) + +asyncio.run(main()) diff --git a/v3/as_drivers/nec_ir/__init__.py b/v3/as_drivers/nec_ir/__init__.py new file mode 100644 index 0000000..e7979ed --- /dev/null +++ b/v3/as_drivers/nec_ir/__init__.py @@ -0,0 +1 @@ +from .as_GPS import * diff --git a/v3/as_drivers/nec_ir/aremote.py b/v3/as_drivers/nec_ir/aremote.py new file mode 100644 index 0000000..59c00c2 --- /dev/null +++ b/v3/as_drivers/nec_ir/aremote.py @@ -0,0 +1,122 @@ +# 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) + 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/v3/as_drivers/nec_ir/art.py b/v3/as_drivers/nec_ir/art.py new file mode 100644 index 0000000..c861a50 --- /dev/null +++ b/v3/as_drivers/nec_ir/art.py @@ -0,0 +1,47 @@ +# 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/v3/as_drivers/nec_ir/art1.py b/v3/as_drivers/nec_ir/art1.py new file mode 100644 index 0000000..ae1978d --- /dev/null +++ b/v3/as_drivers/nec_ir/art1.py @@ -0,0 +1,60 @@ +# 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/v3/demos/gps/LICENSE b/v3/demos/gps/LICENSE deleted file mode 100644 index 798b35f..0000000 --- a/v3/demos/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/v3/demos/gps/README.md b/v3/docs/GPS.md similarity index 86% rename from v3/demos/gps/README.md rename to v3/docs/GPS.md index b12eea1..dfd3172 100644 --- a/v3/demos/gps/README.md +++ b/v3/docs/GPS.md @@ -1,4 +1,4 @@ -# 1. as_GPS +# 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 @@ -7,7 +7,8 @@ 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. -**UNDER REVIEW: API MAY CHANGE** +###### [Tutorial](./TUTORIAL.md#contents) +###### [Main V3 README](../README.md) ## 1.1 Driver characteristics @@ -51,14 +52,18 @@ changes. ###### [Main README](../README.md) -## 1.1 Overview +## 1.3 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 +###### [Top](./GPS.md#1-as_gps) + +# 2 Installation + +## 2.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 @@ -85,9 +90,25 @@ machine.Pin.board.EN_3V3.value(1) time.sleep(1) ``` -## 1.2 Basic Usage +## 2.2 Library installation + +The library is implemented as a Python package and is in `as_drivers/as_GPS`. +To install copy the following directories and their contents to the target +hardware: + 1. `as_drivers` + 2. `primitives` + +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. +Code samples will need adaptation for the serial port. + +## 2.3 Dependency -If running on a [MicroPython] target the `uasyncio` library must be installed. +The library requires `uasyncio` V3 on MicroPython and `asyncio` on CPython. + +###### [Top](./GPS.md#1-as_gps) + +# 3 Basic Usage 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. @@ -95,7 +116,7 @@ The test runs for 60 seconds once data has been received. ```python import uasyncio as asyncio -import as_GPS +import as_drivers.as_GPS as as_GPS from machine import UART def callback(gps, *_): # Runs for each valid fix print(gps.latitude(), gps.longitude(), gps.altitude) @@ -116,7 +137,7 @@ This example achieves the same thing without using a callback: ```python import uasyncio as asyncio -import as_GPS +import as_drivers.as_GPS as as_GPS from machine import UART uart = UART(4, 9600) @@ -133,52 +154,24 @@ async def test(): asyncio.run(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 +## 3.1 Demo programs -### 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]. +This assumes a Pyboard 1.x or Pyboard D with GPS connected to UART 4 and prints +received data: +```python +import as_drivers.as_gps.ast_pb +``` -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 the `primitives` package. +A simple demo which logs a route travelled to a .kml file which may be +displayed on Google Earth. Logging stops when the user switch is pressed. +Data is logged to `/sd/log.kml` at 10s intervals. +```python +import as_drivers.as_gps.log_kml +``` -### 1.4.2 Python 3.8 or later +###### [Top](./GPS.md#1-as_gps) -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.8 -or later. - -# 2. The AS_GPS Class read-only driver +# 4. 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 @@ -186,12 +179,12 @@ 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). + * Check the `time_since_fix` method [section 2.2.3](./GPS.md#423-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. + [section 4.3.1](./GPS.md#431-data-validity). This ensures current data. -## 2.1 Constructor +## 4.1 Constructor Mandatory positional arg: * `sreader` This is a `StreamReader` instance associated with the UART. @@ -208,7 +201,7 @@ 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 +### 4.1.1 The fix callback This receives the following positional args: 1. The GPS instance. @@ -226,9 +219,9 @@ of RMC and VTG messages: gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG) ``` -## 2.2 Public Methods +## 4.2 Public Methods -### 2.2.1 Location +### 4.2.1 Location * `latitude` Optional arg `coord_format=as_GPS.DD`. Returns the most recent latitude. @@ -264,7 +257,7 @@ gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG) `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 +### 4.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`. @@ -275,7 +268,7 @@ gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG) * `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 +### 4.2.3 Time and date * `time_since_fix` No args. Returns time in milliseconds since last valid fix. @@ -289,9 +282,9 @@ gps = as_GPS.AS_GPS(sreader, fix_cb=callback, cb_mask= as_GPS.RMC | as_GPS.VTG) `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 +## 4.3 Public coroutines -### 2.3.1 Data validity +### 4.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 @@ -311,7 +304,7 @@ while True: No option is provided for satellite data: this functionality is provided by the `get_satellite_data` coroutine. -### 2.3.2 Satellite Data +### 4.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 @@ -334,7 +327,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. -## 2.4 Public bound variables/properties +## 4.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` @@ -342,7 +335,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). -### 2.4.1 Position/course +### 4.4.1 Position/course * `course` Track angle in degrees. (VTG). * `altitude` Metres above mean sea level. (GGA). @@ -351,7 +344,7 @@ The sentence type which updates a value is shown in brackets e.g. (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 +### 4.4.2 Statistics and status The following are counts since instantiation. * `crc_fails` Usually 0 but can occur on baudrate change. @@ -360,14 +353,14 @@ The following are counts since instantiation. * `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). + [section 8](./GPS.md#8-developer-notes). -### 2.4.3 Date and time +### 4.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). + [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] * `local_offset` Local time offset in hrs as specified to constructor. @@ -379,7 +372,7 @@ 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 +### 4.4.4 Satellite data * `satellites_in_view` No. of satellites in view. (GSV). * `satellites_in_use` No. of satellites in use. (GGA). @@ -391,7 +384,7 @@ update when local time passes midnight (local time and date are computed from Dilution of Precision (DOP) values close to 1.0 indicate excellent quality position data. Increasing values indicate decreasing precision. -## 2.5 Subclass hooks +## 4.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. @@ -409,13 +402,15 @@ and subsequent characters are stripped from the last. Thus if the string 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 +## 4.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 +###### [Top](./GPS.md#1-as_gps) + +# 5. 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 @@ -429,37 +424,36 @@ GPS modules based on the MTK3329/MTK3339 chip. These include: A subset of the PMTK packet types is supported but this may be extended by subclassing. -## 3.1 Files +## 5.1 Test script + +This assumes a Pyboard 1.x or Pyboard D with GPS on UART 4. To run issue: +```python +import as_drivers.as_gps.ast_pbrw +``` - * `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. +are made for about 1 minute reporting data at the REPL and on the LEDs. On +completion (or `ctrl-c`) a factory reset is performed to restore the default +baudrate. 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. -### 3.1.1 Usage example +## 5.2 Usage example This reduces to 2s the interval at which the GPS sends messages: ```python import uasyncio as asyncio -import as_rwGPS +from as_drivers.as_GPS.as_rwGPS import GPS 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 +gps = GPS(sreader, swriter) # Instantiate GPS async def test(): print('waiting for GPS data') @@ -472,7 +466,7 @@ async def test(): asyncio.run(test()) ``` -## 3.2 GPS class Constructor +## 5.3 GPS class Constructor This takes two mandatory positional args: * `sreader` This is a `StreamReader` instance associated with the UART. @@ -498,18 +492,18 @@ 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). +[section 4.5](./GPS.md#45-subclass-hooks). The args presented to the fix callback are as described in -[section 2.1](./README.md#21-constructor). +[section 4.1](./GPS.md#41-constructor). -## 3.3 Public coroutines +## 5.4 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). + [notes on timing](GPS.md#9-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. @@ -546,17 +540,16 @@ 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 +### 5.4.1 Changing baudrate I have experienced failures on a Pyboard V1.1 at baudrates higher than 19200. -Under investigation. **TODO UPDATE THIS** +This may be a problem with my GPS hardware (see below). -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. +Further, there are problems (at least with my GPS firmware build) where setting +baudrates only works for certain rates. This is clearly an issue with the GPS +unit; rates of 19200, 38400, 57600 and 115200 work. Setting 4800 sets 115200. +Importantly 9600 does nothing. Hence 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: @@ -567,18 +560,19 @@ async def change_status(gps, uart): 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. +At risk of stating the obvious to seasoned programmers, say your application +changes the GPS unit's baudrate. If interrupted (with a bug or `ctrl-c`) the +GPS will still be running at the new baudrate. The 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). +See also [notes on timing](./GPS.md#9-notes-on-timing). -## 3.4 Public bound variables +## 5.5 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 @@ -594,7 +588,7 @@ measured in seconds) polls the value, returning it when it changes. 2 Internal antenna. 3 External antenna. -## 3.5 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 @@ -604,9 +598,11 @@ 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). +[in section 5.3](GPS.md#53-gps-class-constructor). -# 4. Using GPS for accurate timing +###### [Top](./GPS.md#1-as_gps) + +# 6. 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. @@ -618,28 +614,30 @@ 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 +See [Absolute accuracy](GPS.md#91-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 +## 6.1 Test scripts - * `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 +On import, these will list available tests. Example usage: +```python +import as_drivers.as_GPS.as_GPS_time as test +test.usec() +``` + +## 6.2 Usage example ```python import uasyncio as asyncio import pyb -import as_tGPS +import as_drivers.as_GPS.as_tGPS as as_tGPS async def test(): fstr = '{}ms Time: {:02d}:{:02d}:{:02d}:{:06d}' @@ -663,20 +661,20 @@ async def test(): asyncio.run(test()) ``` -## 4.2 GPS_Timer and GPS_RWTimer classes +## 6.3 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 +### 6.3.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 + * `local_offset` See [base class](GPS.md#41-constructor) for details of these args. * `fix_cb` * `cb_mask` @@ -688,7 +686,7 @@ Optional positional args: receives the `GPS_Timer` instance as the first arg, followed by any args in the tuple. -### 4.2.2 GPS_RWTimer class Constructor +### 6.3.2 GPS_RWTimer class Constructor This takes three mandatory positional args: * `sreader` The `StreamReader` instance associated with the UART. @@ -696,7 +694,7 @@ This takes three mandatory positional args: * `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 + * `local_offset` See [base class](GPS.md#41-constructor) for details of these args. * `fix_cb` * `cb_mask` @@ -710,7 +708,7 @@ Optional positional args: receives the `GPS_RWTimer` instance as the first arg, followed by any args in the tuple. -## 4.3 Public methods +## 6.4 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 @@ -728,10 +726,10 @@ target supporting `machine.ticks_us`. * `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 +See [Absolute accuracy](GPS.md#91-absolute-accuracy) for a discussion of the accuracy of these methods. -## 4.4 Public coroutines +## 6.5 Public coroutines All MicroPython targets: * `ready` No args. Pauses until a valid time/date message and PPS signal have @@ -760,7 +758,7 @@ 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 +## 6.6 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. @@ -768,9 +766,15 @@ 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). +[section 9](./GPS.md#9-notes-on-timing). -## 4.6 Test/demo program as_GPS_time.py +## 6.7 Test/demo program as_GPS_time.py + +Run by issuing +```python +import as_drivers.as_GPS.as_GPS_time as test +test.time() # e.g. +``` This comprises the following test functions. Reset the chip with ctrl-d between runs. @@ -784,7 +788,7 @@ runs. some limits to the absolute accuracy of the `get_t_split` method as discussed above. -# 5. Supported Sentences +# 7. Supported Sentences * GPRMC GP indicates NMEA sentence (US GPS system). * GLRMC GL indicates GLONASS (Russian system). @@ -802,11 +806,13 @@ runs. * GPGSV * GLGSV -# 6 Developer notes +###### [Top](./GPS.md#1-as_gps) + +# 8 Developer notes These notes are for those wishing to adapt these drivers. -## 6.1 Subclassing +## 8.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, @@ -818,7 +824,7 @@ 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. +[section 4.5](GPS.md#45-subclass-hooks) for details. The `parse` method should return `True` if the sentence was successfully parsed, otherwise `False`. @@ -827,15 +833,23 @@ 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 +## 8.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.8+ or MicroPython. * `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: -# 7. Notes on timing +```python +from as_drivers.as_GPS.astests import run_tests +run_tests() +``` + +###### [Top](./GPS.md#1-as_gps) + +# 9. 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 @@ -860,7 +874,7 @@ 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 +## 9.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 @@ -906,6 +920,49 @@ 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) + +# 10 Files + +If space on the filesystem is limited, unneccessary files may be deleted. Many +applications will not need the read/write or timing files. + +## 10.1 Basic 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. + * `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. + +## 10.3 Files for timing applications + + * `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. + +## 10.4 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 PC under CPython 3.8+ or MicroPython. + * `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 [NMEA-0183]:http://aprs.gids.nl/nmea/ @@ -914,3 +971,5 @@ duration over which it is measured. [MTK_command]:https://github.com/inmcm/MTK_commands [Ultimate GPS Breakout]:http://www.adafruit.com/product/746 [micropyGPS]:https://github.com/inmcm/micropyGPS.git + +###### [Top](./GPS.md#1-as_gps) diff --git a/v3/docs/HTU21D.md b/v3/docs/HTU21D.md new file mode 100644 index 0000000..947a679 --- /dev/null +++ b/v3/docs/HTU21D.md @@ -0,0 +1,50 @@ +# 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/v3/docs/NEC_IR.md b/v3/docs/NEC_IR.md new file mode 100644 index 0000000..33be026 --- /dev/null +++ b/v3/docs/NEC_IR.md @@ -0,0 +1,133 @@ +# 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/v3/TUTORIAL.md b/v3/docs/TUTORIAL.md similarity index 96% rename from v3/TUTORIAL.md rename to v3/docs/TUTORIAL.md index 6e2e570..0d7f1f8 100644 --- a/v3/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -46,6 +46,7 @@ now complete scripts which can be cut and pasted at the REPL. 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". 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) @@ -75,7 +76,7 @@ now complete scripts which can be cut and pasted at the REPL. 8.6 [Communication](./TUTORIAL.md#86-communication) 8.7 [Polling](./TUTORIAL.md#87-polling) -###### [Main README](./README.md) +###### [Main README](../README.md) # 0. Introduction @@ -137,23 +138,46 @@ to your hardware. **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. + The first two are the most immediately rewarding as they produce visible -results by accessing Pyboard hardware. - - 1. [aledflash.py](./demos/aledflash.py) Flashes three Pyboard LEDs - asynchronously for 10s. The simplest uasyncio demo. Import it to run. - 2. [apoll.py](./demos/apoll.py) A device driver for the Pyboard accelerometer. - Demonstrates the use of a task to poll a device. Runs for 20s. Import it to - run. Requires a Pyboard V1.x. - 3. [roundrobin.py](./demos/roundrobin.py) Demo of round-robin scheduling. Also - a benchmark of scheduling performance. - 4. [auart.py](./demos/auart.py) Demo of streaming I/O via a Pyboard UART. - 5. [auart_hd.py](./demos/auart_hd.py) Use of the Pyboard UART to communicate +results by accessing Pyboard hardware. With all demos, issue ctrl-d between +runs to soft reset the hardware. + +#### aledflash.py + +Flashes three Pyboard LEDs asynchronously for 10s. The simplest uasyncio demo. +```python +import as_demos.aledflash +``` + +#### 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. +```python +import as_demos.apoll +``` + +#### roundrobin.py + +Demo of round-robin scheduling. Also a benchmark of scheduling performance. +Runs for 5s. +```python +import as_demos.roundrobin +``` + + 1. [aledflash.py](./as_demos/aledflash.py) + 2. [apoll.py](./as_demos/apoll.py) + 3. [roundrobin.py](./as_demos/roundrobin.py) + 4. [auart.py](./as_demos/auart.py) Demo of streaming I/O via a Pyboard UART. + 5. [auart_hd.py](./as_demos/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. - 6. [iorw.py](./demos/iorw.py) Demo of a read/write device driver using the + 6. [iorw.py](./as_demos/iorw.py) Demo of a read/write device driver using the stream I/O mechanism. Requires a Pyboard. - 7. [A driver for GPS modules](./demos/gps/README.md) Runs a background task to + 7. [A driver for GPS modules](./GPS.md) Runs a background task to read and decode NMEA sentences, providing constantly updated position, course, altitude and time/date information. @@ -1400,6 +1424,27 @@ async def foo(): asyncio.run(foo()) ``` +## 5.2.3 Cancelling running tasks + +This useful technique can provoke counter intuitive behaviour. Consider a task +`foo` created using `create_task`. Then tasks `bar`, `cancel_me` (and possibly +others) are created with code like: +```python +async def bar(): + await foo + # more code +``` +All will pause waiting for `foo` to terminate. If any one of the waiting tasks +is cancelled, the cancellation will propagate to `foo`. This would be expected +behaviour if `foo` were a coro. The fact that it is a running task means that +the cancellation impacts the tasks waiting on it; it actually causes their +cancellation. Again, if `foo` were a coro and a task or coro was waiting on it, +cancelling `foo` would be expected to propagate to the caller. In the context +of running tasks, this may be unwelcome. + +The behaviour is "correct": CPython `asyncio` behaves identically. Ref +[this forum thread](https://forum.micropython.org/viewtopic.php?f=2&t=8158). + ###### [Contents](./TUTORIAL.md#contents) # 6 Interfacing hardware @@ -1608,7 +1653,7 @@ provide a solution if the data source supports it. ### 6.3.1 A UART driver example -The program [auart_hd.py](./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. @@ -1787,7 +1832,7 @@ class PinCall(io.IOBase): Once again latency can be high: if implemented fast I/O scheduling will improve this. -The demo program [iorw.py](./demos/iorw.py) illustrates a complete example. +The demo program [iorw.py](./as_demos/iorw.py) illustrates a complete example. ###### [Contents](./TUTORIAL.md#contents) @@ -1795,7 +1840,7 @@ The demo program [iorw.py](./demos/iorw.py) illustrates a complete example. **TODO** Not yet ported to V3. -See [aremote.py](./nec_ir/aremote.py) documented [here](./nec_ir/README.md). +See [aremote.py](./nec_ir/aremote.py) documented [here](./NEC_IR.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. @@ -1812,10 +1857,8 @@ any `asyncio` latency when setting its delay period. ## 6.6 HTU21D environment sensor -**TODO** Not yet ported to V3. - This chip provides accurate measurements of temperature and humidity. The -driver is documented [here](./htu21d/README.md). It has a continuously running +driver is documented [here](./HTU21D.md). It has a continuously running task which updates `temperature` and `humidity` bound variables which may be accessed "instantly". @@ -1824,6 +1867,10 @@ works asynchronously by triggering the acquisition and using `await asyncio.sleep(t)` prior to reading the data. This allows other tasks to run while acquisition is in progress. +```python +import as_drivers.htu21d.htu_test +``` + # 7 Hints and tips ###### [Contents](./TUTORIAL.md#contents) From 626d0a6e4cf01fcb7dca7e84d1d15daa4c9bb0f8 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 16 Apr 2020 15:32:55 +0100 Subject: [PATCH 158/472] v3 Add NEC_IR. Tutorial corrections and improvements. --- v3/README.md | 8 ++- v3/as_demos/__init__.py | 0 v3/as_demos/aledflash.py | 19 +++++-- v3/as_demos/auart.py | 11 +++- v3/as_demos/auart_hd.py | 14 ++++- v3/as_demos/gather.py | 65 ++++++++++++++++++--- v3/as_drivers/nec_ir/__init__.py | 2 +- v3/as_drivers/nec_ir/aremote.py | 15 ++--- v3/as_drivers/nec_ir/art.py | 11 +++- v3/as_drivers/nec_ir/art1.py | 7 ++- v3/docs/NEC_IR.md | 35 +++++++++--- v3/docs/TUTORIAL.md | 98 ++++++++++++++++++-------------- v3/primitives/tests/asyntest.py | 41 ++++++------- v3/primitives/tests/switches.py | 23 +++++--- 14 files changed, 239 insertions(+), 110 deletions(-) create mode 100644 v3/as_demos/__init__.py diff --git a/v3/README.md b/v3/README.md index f1e0b3c..954033b 100644 --- a/v3/README.md +++ b/v3/README.md @@ -21,8 +21,12 @@ Documented in the tutorial. Documented in the tutorial. #### Asynchronous device drivers -The device drivers are in the process of being ported. Currently there is a -[GPS driver](./docs/GPS.md). + +The device drivers are in the process of being ported. These currently +comprise: + + * [GPS driver](./docs/GPS.md) Includes various GPS utilities. + * [HTU21D](./docs/HTU21D.md) Temperature and humidity sensor. # 2 V3 Overview diff --git a/v3/as_demos/__init__.py b/v3/as_demos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v3/as_demos/aledflash.py b/v3/as_demos/aledflash.py index c621f97..2d478c4 100644 --- a/v3/as_demos/aledflash.py +++ b/v3/as_demos/aledflash.py @@ -17,14 +17,21 @@ async def toggle(objLED, time_ms): # TEST FUNCTION -def test(duration): - duration = int(duration) - if duration > 0: - print("Flash LED's for {:3d} seconds".format(duration)) +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 - for x, led in enumerate(leds): # Create a coroutine for each LED + 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)) -test(10) +def test(duration=10): + try: + asyncio.run(main(duration)) + except KeyboardInterrupt: + print('Interrupted') + finally: + asyncio.new_event_loop() + print('as_demos.aledflash.test() to run again.') + +test() diff --git a/v3/as_demos/auart.py b/v3/as_demos/auart.py index c685c59..13fea5e 100644 --- a/v3/as_demos/auart.py +++ b/v3/as_demos/auart.py @@ -25,4 +25,13 @@ async def main(): while True: await asyncio.sleep(1) -asyncio.run(main()) +def test(): + try: + asyncio.run(main()) + except KeyboardInterrupt: + print('Interrupted') + finally: + asyncio.new_event_loop() + print('as_demos.auart.test() to run again.') + +test() diff --git a/v3/as_demos/auart_hd.py b/v3/as_demos/auart_hd.py index 10393ef..82e544e 100644 --- a/v3/as_demos/auart_hd.py +++ b/v3/as_demos/auart_hd.py @@ -68,7 +68,7 @@ async def send_command(self, command): await asyncio.sleep(1) # Wait for 4s after last msg received return self.response -async def test(): +async def main(): print('This test takes 10s to complete.') master = Master() device = Device() @@ -101,6 +101,14 @@ def printexp(): print(st) print('\x1b[39m') -printexp() -asyncio.run(test()) +def test(): + printexp() + try: + asyncio.run(main()) + except KeyboardInterrupt: + print('Interrupted') + finally: + asyncio.new_event_loop() + 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 c422888..86a9ba1 100644 --- a/v3/as_demos/gather.py +++ b/v3/as_demos/gather.py @@ -18,8 +18,9 @@ async def foo(n): while True: await asyncio.sleep(1) n += 1 - except Exception as e: #asyncio.TimeoutError: - print('foo timeout.', e) + except asyncio.CancelledError: + print('Trapped foo timeout.') + raise return n async def bar(n): @@ -28,8 +29,9 @@ async def bar(n): while True: await asyncio.sleep(1) n += 1 - except Exception as e: - print('bar stopped.', e) + except asyncio.CancelledError: # Demo of trapping + print('Trapped bar cancellation.') + raise return n async def do_cancel(task): @@ -37,13 +39,62 @@ async def do_cancel(task): print('About to cancel bar') task.cancel() -async def main(): +async def main(rex): bar_task = asyncio.create_task(bar(70)) # Note args here tasks = [] tasks.append(barking(21)) tasks.append(asyncio.wait_for(foo(10), 7)) asyncio.create_task(do_cancel(bar_task)) - res = await asyncio.gather(*tasks) + try: + res = await asyncio.gather(*tasks, return_exceptions=rex) + except asyncio.TimeoutError: + print('foo timed out.') + res = 'No result' print('Result: ', res) -asyncio.run(main()) + +exp_false = '''Test runs for 10s. Expected output: + +Start cancellable bar() +Start normal coro barking() +Start timeout coro foo() +About to cancel bar +Trapped bar cancellation. +Done barking. +Trapped foo timeout. +foo timed out. +Result: No result + +''' +exp_true = '''Test runs for 10s. Expected output: + +Start cancellable bar() +Start normal coro barking() +Start timeout coro foo() +About to cancel bar +Trapped bar cancellation. +Done barking. +Trapped foo timeout. +Result: [42, TimeoutError()] + +''' + +def printexp(st): + print('\x1b[32m') + print(st) + print('\x1b[39m') + +def test(rex): + st = exp_true if rex else exp_false + printexp(st) + try: + asyncio.run(main(rex)) + except KeyboardInterrupt: + 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.') + +test(rex=False) diff --git a/v3/as_drivers/nec_ir/__init__.py b/v3/as_drivers/nec_ir/__init__.py index e7979ed..54209f0 100644 --- a/v3/as_drivers/nec_ir/__init__.py +++ b/v3/as_drivers/nec_ir/__init__.py @@ -1 +1 @@ -from .as_GPS import * +from .aremote import * diff --git a/v3/as_drivers/nec_ir/aremote.py b/v3/as_drivers/nec_ir/aremote.py index 59c00c2..3448e6a 100644 --- a/v3/as_drivers/nec_ir/aremote.py +++ b/v3/as_drivers/nec_ir/aremote.py @@ -6,10 +6,10 @@ from sys import platform import uasyncio as asyncio -from asyn import Event +from primitives.message import Message from micropython import const from array import array -from utime import ticks_us, ticks_diff +from utime import ticks_ms, ticks_us, ticks_diff if platform == 'pyboard': from pyb import Pin, ExtInt else: @@ -41,7 +41,7 @@ # 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._ev_start = Message() self._callback = callback self._extended = extended self._addr = 0 @@ -56,15 +56,13 @@ def __init__(self, pin, callback, extended, *args): # Optional args for callbac 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()) + asyncio.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()) + latency = ticks_diff(ticks_ms(), 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 @@ -74,8 +72,7 @@ def _cb_pin(self, line): # 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._ev_start.set(ticks_ms()) # asyncio latency compensation self._times[self._edge] = t self._edge += 1 diff --git a/v3/as_drivers/nec_ir/art.py b/v3/as_drivers/nec_ir/art.py index c861a50..7cbff42 100644 --- a/v3/as_drivers/nec_ir/art.py +++ b/v3/as_drivers/nec_ir/art.py @@ -5,6 +5,7 @@ # Copyright Peter Hinch 2017 Released under the MIT license # Run this to characterise a remote. +# import as_drivers.nec_ir.art from sys import platform import uasyncio as asyncio @@ -17,7 +18,7 @@ else: print('Unsupported platform', platform) -from aremote import * +from .aremote import * errors = {BADSTART : 'Invalid start pulse', BADBLOCK : 'Error: bad block', BADREP : 'Error: repeat', OVERRUN : 'Error: overrun', @@ -33,6 +34,7 @@ def cb(data, addr): def test(): print('Test for IR receiver. Assumes NEC protocol.') + print('ctrl-c to stop.') if platform == 'pyboard': p = Pin('X3', Pin.IN) elif platform == 'esp8266': @@ -42,6 +44,11 @@ def test(): 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() + try: + loop.run_forever() + except KeyboardInterrupt: + print('Interrupted') + finally: + asyncio.new_event_loop() # Still need ctrl-d because of interrupt vector test() diff --git a/v3/as_drivers/nec_ir/art1.py b/v3/as_drivers/nec_ir/art1.py index ae1978d..ba13061 100644 --- a/v3/as_drivers/nec_ir/art1.py +++ b/v3/as_drivers/nec_ir/art1.py @@ -55,6 +55,11 @@ def test(): led(1) ir = NEC_IR(p, cb, True, led) # Assume extended address mode r/c loop = asyncio.get_event_loop() - loop.run_forever() + try: + loop.run_forever() + except KeyboardInterrupt: + print('Interrupted') + finally: + asyncio.new_event_loop() # Still need ctrl-d because of interrupt vector test() diff --git a/v3/docs/NEC_IR.md b/v3/docs/NEC_IR.md index 33be026..fee0ecb 100644 --- a/v3/docs/NEC_IR.md +++ b/v3/docs/NEC_IR.md @@ -8,17 +8,32 @@ microcontroller. The driver and test programs run on the Pyboard and ESP8266. -# Files +## An alternative solution - 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. +This solution provides an example of an asynchronous device driver. A more +complete IR solution may be found +[here](https://github.com/peterhinch/micropython_ir). This supports other +protocols and IR "blasting". It does not use `uasyncio` but is nonblocking and +is compatible with `uasyncio` applications. + +# Demo scripts + +The following prints data and address values received from a remote. These +values enable you to respond to individual butons. +```python +import as_drivers.nec_ir.art +``` + +Control an onboard LED using a remote. The data and addresss values must be +changed to match your characterised remote. +```python +import as_drivers.nec_ir.art1 +``` # Dependencies -The driver requires the `uasyncio` library and the file `asyn.py` from this -repository. +The driver requires the `uasyncio` library and the `primitives` package from +this repository. # Usage @@ -131,3 +146,9 @@ owing to high interrupt latency. `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. + +# Files + + 1. `aremote.py` The device driver. + 2. `art.py` A test program to characterise a remote. + 3. `art1.py` diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 0d7f1f8..12dda93 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -26,7 +26,6 @@ now complete scripts which can be cut and pasted at the REPL. 3. [Synchronisation](./TUTORIAL.md#3-synchronisation) 3.1 [Lock](./TUTORIAL.md#31-lock) 3.2 [Event](./TUTORIAL.md#32-event) - 3.2.1 [The event's value](./TUTORIAL.md#321-the-events-value) 3.3 [gather](./TUTORIAL.md#33-gather) 3.4 [Semaphore](./TUTORIAL.md#34-semaphore) 3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore) @@ -125,7 +124,7 @@ pitfalls associated with truly asynchronous threads of execution. ## 1.1 Modules -**Primitives** +### Primitives The directory `primitives` contains a collection of synchronisation primitives and classes for debouncing switches and pushbuttons, along with a software @@ -136,7 +135,7 @@ events. These are implemented as a Python package: copy the `primitives` directory tree to your hardware. -**Demo Programs** +### 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. @@ -145,41 +144,39 @@ 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. -#### aledflash.py + 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 + 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. + 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. + Requires a link between X1 and X2. + 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 + stream I/O mechanism. Requires a Pyboard. -Flashes three Pyboard LEDs asynchronously for 10s. The simplest uasyncio demo. +Demos are run using this pattern: ```python import as_demos.aledflash ``` -#### 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. -```python -import as_demos.apoll -``` - -#### roundrobin.py +### Device drivers -Demo of round-robin scheduling. Also a benchmark of scheduling performance. -Runs for 5s. -```python -import as_demos.roundrobin -``` +These are installed by copying the `as_drivers` directory and contents to the +target. They have their own documentation as follows: - 1. [aledflash.py](./as_demos/aledflash.py) - 2. [apoll.py](./as_demos/apoll.py) - 3. [roundrobin.py](./as_demos/roundrobin.py) - 4. [auart.py](./as_demos/auart.py) Demo of streaming I/O via a Pyboard UART. - 5. [auart_hd.py](./as_demos/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. - 6. [iorw.py](./as_demos/iorw.py) Demo of a read/write device driver using the - stream I/O mechanism. Requires a Pyboard. - 7. [A driver for GPS modules](./GPS.md) Runs a background task to + 1. [A driver for GPS modules](./GPS.md) Runs a background task to read and decode NMEA sentences, providing constantly updated position, course, 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 + whenever a valid signal is received. ###### [Contents](./TUTORIAL.md#contents) @@ -1838,12 +1835,10 @@ The demo program [iorw.py](./as_demos/iorw.py) illustrates a complete example. ## 6.5 A complete example: aremote.py -**TODO** Not yet ported to V3. - -See [aremote.py](./nec_ir/aremote.py) documented [here](./NEC_IR.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. +See [aremote.py](../as_drivers/nec_ir/aremote.py) documented +[here](./NEC_IR.md). This is a complete device driver: 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 task waits on the @@ -1871,10 +1866,10 @@ run while acquisition is in progress. import as_drivers.htu21d.htu_test ``` -# 7 Hints and tips - ###### [Contents](./TUTORIAL.md#contents) +# 7 Hints and tips + ## 7.1 Program hangs Hanging usually occurs because a task has blocked without yielding: this will @@ -1882,13 +1877,26 @@ 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. -###### [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. +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 + +async def main(): + await asyncio.sleep(5) # Dummy test script + +def test(): + try: + asyncio.run(main()) + except KeyboardInterrupt: # Trapping this is optional + print('Interrupted') # or pass + finally: + asyncio.new_event_loop() # Clear retained state +``` ###### [Contents](./TUTORIAL.md#contents) @@ -2046,7 +2054,7 @@ asyncio.run(my_task()) with: ```python loop = asyncio.get_event_loop() -loop.run_forever(my_task()) +loop.run_until_complete(my_task()) ``` The `create_task` method is a member of the `event_loop` instance. Replace ```python @@ -2069,6 +2077,10 @@ behavior. [reference](https://docs.python.org/3/library/asyncio-eventloop.html#a 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. + ## 7.8 Race conditions These occur when coroutines compete for access to a resource, each using the diff --git a/v3/primitives/tests/asyntest.py b/v3/primitives/tests/asyntest.py index e6fcb17..7bc7ab5 100644 --- a/v3/primitives/tests/asyntest.py +++ b/v3/primitives/tests/asyntest.py @@ -23,7 +23,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -# CPython 3.5 compatibility +# CPython 3.8 compatibility # (ignore RuntimeWarning: coroutine '_g' was never awaited) # To run: # from primitives.tests.asyntest import test @@ -48,8 +48,6 @@ def print_tests(): test(5) Test BoundedSemaphore. test(6) Test the Condition class. test(7) Test the Queue class. - -Recommended to issue ctrl-D after running each test. ''' print('\x1b[32m') print(st) @@ -386,19 +384,24 @@ def queue_test(): asyncio.run(queue_go(3)) def test(n): - if n == 0: - print_tests() # Print this list. - elif n == 1: - ack_test() # Test message acknowledge. - elif n == 2: - msg_test() # Test Messge and Lock objects. - elif n == 3: - barrier_test() # Test the Barrier class. - elif n == 4: - semaphore_test(False) # Test Semaphore - elif n == 5: - semaphore_test(True) # Test BoundedSemaphore. - elif n == 6: - condition_test() # Test the Condition class. - elif n == 7: - queue_test() # Test the Queue class. + try: + if n == 1: + ack_test() # Test message acknowledge. + elif n == 2: + msg_test() # Test Messge and Lock objects. + elif n == 3: + barrier_test() # Test the Barrier class. + elif n == 4: + semaphore_test(False) # Test Semaphore + elif n == 5: + semaphore_test(True) # Test BoundedSemaphore. + elif n == 6: + condition_test() # Test the Condition class. + elif n == 7: + queue_test() # Test the Queue class. + except KeyboardInterrupt: + print('Interrupted') + print('Interrupted') + finally: + asyncio.new_event_loop() + print_tests() diff --git a/v3/primitives/tests/switches.py b/v3/primitives/tests/switches.py index b90f92a..6a2007b 100644 --- a/v3/primitives/tests/switches.py +++ b/v3/primitives/tests/switches.py @@ -17,7 +17,6 @@ helptext = ''' Test using switch or pushbutton between X1 and gnd. Ground pin X2 to terminate test. -Soft reset (ctrl-D) after each test. ''' tests = ''' @@ -45,6 +44,16 @@ async def killer(): while pin.value(): await asyncio.sleep_ms(50) +def run(): + try: + asyncio.run(killer()) + except KeyboardInterrupt: + print('Interrupted') + finally: + asyncio.new_event_loop() + print(tests) + + # Test for the Switch class passing coros def test_sw(): s = ''' @@ -61,8 +70,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)) - loop = asyncio.get_event_loop() - loop.run_until_complete(killer()) + run() # Test for the switch class with a callback def test_swcb(): @@ -80,8 +88,7 @@ def test_swcb(): # 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()) + run() # Test for the Pushbutton class (coroutines) # Pass True to test suppress @@ -109,8 +116,7 @@ def test_btn(suppress=False, lf=True, df=True): if lf: print('Long press enabled') pb.long_func(pulse, (blue, 1000)) - loop = asyncio.get_event_loop() - loop.run_until_complete(killer()) + run() # Test for the Pushbutton class (callbacks) def test_btncb(): @@ -133,5 +139,4 @@ def test_btncb(): 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()) + run() From 0ae62dd32e593ee7f15fb13a5303002788f08b1c Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 21 Apr 2020 17:02:20 +0100 Subject: [PATCH 159/472] Prior to testing machine.schedule --- i2c/README.md | 6 +- v3/README.md | 24 +- v3/as_drivers/i2c/__init__.py | 0 v3/as_drivers/i2c/asi2c.py | 203 +++++++++++++++ v3/as_drivers/i2c/asi2c_i.py | 142 +++++++++++ v3/as_drivers/i2c/i2c_esp.py | 66 +++++ v3/as_drivers/i2c/i2c_init.py | 80 ++++++ v3/as_drivers/i2c/i2c_resp.py | 67 +++++ v3/docs/GPS.md | 9 +- v3/docs/HTU21D.md | 52 +++- v3/docs/I2C.md | 457 ++++++++++++++++++++++++++++++++++ v3/docs/NEC_IR.md | 11 +- v3/docs/TUTORIAL.md | 9 +- 13 files changed, 1098 insertions(+), 28 deletions(-) create mode 100644 v3/as_drivers/i2c/__init__.py create mode 100644 v3/as_drivers/i2c/asi2c.py create mode 100644 v3/as_drivers/i2c/asi2c_i.py create mode 100644 v3/as_drivers/i2c/i2c_esp.py create mode 100644 v3/as_drivers/i2c/i2c_init.py create mode 100644 v3/as_drivers/i2c/i2c_resp.py create mode 100644 v3/docs/I2C.md diff --git a/i2c/README.md b/i2c/README.md index ad98235..27fa8cb 100644 --- a/i2c/README.md +++ b/i2c/README.md @@ -27,10 +27,10 @@ application and is covered in detail ## Changes -V0.17 Dec 2018 Initiator: add optional "go" and "fail" user coroutines. +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. +failures where `Responder` was a Pyboard. +V0.15 RAM allocation reduced. Flow control implemented. V0.1 Initial release. ###### [Main README](../README.md) diff --git a/v3/README.md b/v3/README.md index 954033b..bfefa48 100644 --- a/v3/README.md +++ b/v3/README.md @@ -22,11 +22,15 @@ Documented in the tutorial. #### Asynchronous device drivers -The device drivers are in the process of being ported. These currently -comprise: +These device drivers are intended as examples of asynchronous code which are +useful in their own right: * [GPS driver](./docs/GPS.md) Includes various GPS utilities. * [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. Typical use: communication with ESP8266. + * [NEC IR](./docs/NEC_IR.md) A receiver for signals from IR remote controls + using the popular NEC protocol. # 2 V3 Overview @@ -65,10 +69,12 @@ The `Future` class is not supported, nor are the `event_loop` methods # 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` syntax and secondly -related to modules in this repository. +unchanged. However there are changes, firstly to `uasyncio` itself and secondly +to modules in this repository. -## 3.1 Syntax changes +## 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 @@ -84,10 +90,16 @@ 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.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. +[the tutorial](./docs/TUTORIAL.md#3-synchronisation) for V3 replacements which +are more RAM-efficient. ### 3.2.1 Synchronisation primitives diff --git a/v3/as_drivers/i2c/__init__.py b/v3/as_drivers/i2c/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v3/as_drivers/i2c/asi2c.py b/v3/as_drivers/i2c/asi2c.py new file mode 100644 index 0000000..0e704b7 --- /dev/null +++ b/v3/as_drivers/i2c/asi2c.py @@ -0,0 +1,203 @@ +# asi2c.py A communications link using I2C slave mode on Pyboard. +# Channel and Responder classes. Adapted for uasyncio V3, WBUS DIP28. + +# The MIT License (MIT) +# +# Copyright (c) 2018-2020 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. + # uasyncio V3: Stream.drain() calls write with buf being a memoryview + # and no off or sz args. + def write(self, buf): + if self.synchronised: + if self.txbyt: # Initial call from awrite + return 0 # Waiting for existing data to go out + l = len(buf) + self.txbyt = buf + 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/v3/as_drivers/i2c/asi2c_i.py b/v3/as_drivers/i2c/asi2c_i.py new file mode 100644 index 0000000..b64e805 --- /dev/null +++ b/v3/as_drivers/i2c/asi2c_i.py @@ -0,0 +1,142 @@ +# asi2c_i.py A communications link using I2C slave mode on Pyboard. +# Initiator class. Adapted for uasyncio V3, WBUS DIP28. + +# The MIT License (MIT) +# +# Copyright (c) 2018-2020 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 + asyncio.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 as e: +# print('OSError:', e) # TEST + 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 = self.rx_mv[0: n] # mv is a memoryview instance + self.i2c.recv(mv) + self.waitfor(0) + self.own(0) + self._handle_rxd(mv) diff --git a/v3/as_drivers/i2c/i2c_esp.py b/v3/as_drivers/i2c/i2c_esp.py new file mode 100644 index 0000000..4bd0437 --- /dev/null +++ b/v3/as_drivers/i2c/i2c_esp.py @@ -0,0 +1,66 @@ +# i2c_esp.py Test program for asi2c.py. Adapted for uasyncio V3, WBUS DIP28. +# Tests Responder on ESP8266. + +# The MIT License (MIT) +# +# Copyright (c) 2018-2020 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 +from machine import Pin, I2C +from .asi2c import Responder +import ujson +import gc +gc.collect() + +i2c = I2C(scl=Pin(0),sda=Pin(2)) # software I2C +syn = Pin(5) +ack = Pin(4) +chan = 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) + gc.collect() + +asyncio.create_task(receiver()) +try: + asyncio.run(sender()) +except KeyboardInterrupt: + print('Interrupted') +finally: + asyncio.new_event_loop() + chan.close() # for subsequent runs diff --git a/v3/as_drivers/i2c/i2c_init.py b/v3/as_drivers/i2c/i2c_init.py new file mode 100644 index 0000000..e9575a8 --- /dev/null +++ b/v3/as_drivers/i2c/i2c_init.py @@ -0,0 +1,80 @@ +# i2c_init.py Test program for asi2c.py. Adapted for uasyncio V3, WBUS DIP28. +# Tests Initiator on a Pyboard + +# The MIT License (MIT) +# +# Copyright (c) 2018-2020 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 +from pyb import I2C # Only pyb supports slave mode +from machine import Pin +from .asi2c_i import Initiator +import ujson +import os + +i2c = I2C(2, mode=I2C.SLAVE) +syn = Pin('Y11') +ack = Pin('X6') +# Reset on Pyboard and ESP8266 is active low. Use 200ms pulse. +rst = (Pin('Y12'), 0, 200) +chan = Initiator(i2c, syn, ack, rst) +if os.uname().machine.split(' ')[0][:4] == 'PYBD': + Pin.board.EN_3V3.value(1) + +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(): + asyncio.create_task(receiver()) + asyncio.create_task(sender()) + while True: + await chan.ready() + await asyncio.sleep(10) + if chan.block_cnt: + print('Blocking time {:d}μs max. {:d}μs mean.'.format( + chan.block_max, int(chan.block_sum/chan.block_cnt))) + print('Reboots: ', chan.nboots) + +try: + asyncio.run(test()) +except KeyboardInterrupt: + print('Interrupted') +finally: + asyncio.new_event_loop() + chan.close() # for subsequent runs diff --git a/v3/as_drivers/i2c/i2c_resp.py b/v3/as_drivers/i2c/i2c_resp.py new file mode 100644 index 0000000..c23a2b7 --- /dev/null +++ b/v3/as_drivers/i2c/i2c_resp.py @@ -0,0 +1,67 @@ +# i2c_resp.py Test program for asi2c.py. Adapted for uasyncio V3, WBUS DIP28. +# Tests Responder on a Pyboard. + +# The MIT License (MIT) +# +# Copyright (c) 2018-2020 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 +from machine import Pin, I2C +from .asi2c import Responder +import ujson +import os + +i2c = I2C(2) +#i2c = I2C(scl=Pin('Y9'),sda=Pin('Y10')) # software I2C +syn = Pin('Y11') +ack = Pin('X6') +chan = Responder(i2c, syn, ack) +if os.uname().machine.split(' ')[0][:4] == 'PYBD': + Pin.board.EN_3V3.value(1) + +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) + +asyncio.create_task(receiver()) +try: + asyncio.run(sender()) +except KeyboardInterrupt: + print('Interrupted') +finally: + asyncio.new_event_loop() + chan.close() # for subsequent runs diff --git a/v3/docs/GPS.md b/v3/docs/GPS.md index dfd3172..c01d7ba 100644 --- a/v3/docs/GPS.md +++ b/v3/docs/GPS.md @@ -50,7 +50,7 @@ changes. * Hooks are provided for user-designed subclassing, for example to parse additional message types. -###### [Main README](../README.md) +###### [Main V3 README](../README.md) ## 1.3 Overview @@ -92,10 +92,9 @@ time.sleep(1) ## 2.2 Library installation -The library is implemented as a Python package and is in `as_drivers/as_GPS`. -To install copy the following directories and their contents to the target -hardware: - 1. `as_drivers` +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` On platforms with an underlying OS such as the Raspberry Pi ensure that the diff --git a/v3/docs/HTU21D.md b/v3/docs/HTU21D.md index 947a679..f3ab1f1 100644 --- a/v3/docs/HTU21D.md +++ b/v3/docs/HTU21D.md @@ -1,7 +1,12 @@ # The HTU21D temperature/humidity sensor. -A breakout board is available from -[Sparkfun](https://www.sparkfun.com/products/12064). +Breakout boards are available from +[Adafruit](https://www.adafruit.com/product/1899). + +This [Sparkfun board](https://www.sparkfun.com/products/13763) has an Si7021 +chip which, from a look at the datasheet, appears to be a clone of the HTU21D. +The Sparkfun prduct ID is the same as boards which I own: mine have HTU21D +chips. This driver was derived from the synchronous Pyboard-specific driver [here](https://github.com/manitou48/pyboard/blob/master/htu21d.py). It is @@ -10,13 +15,38 @@ 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) +###### [Main V3 README](../README.md) + +# Installation -# Files +Copy the `as_drivers/htu21d` directory and contents to the target hardware. +Copy `primitives` and contents to the target. +Files: 1. `htu21d_mc.py` The asynchronous driver. 2. `htu_test.py` Test/demo program. +# The test script + +This runs on any Pyboard or ESP32. for other platforms pin numbers will need to +be changed. + +| Pin | Pyboard | ESP32 | +|:----:|:-------:|:-----:| +| gnd | gnd | gnd | +| Vin | 3V3 | 3V3 | +| scl | X9 | 22 | +| sda | X10 | 23 | + +On the Pyboard D the 3.3V supply must be enabled with +```python +machine.Pin.board.EN_3V3.value(1) +``` +This also enables the I2C pullups on the X side. To run the demo issue: +```python +import as_drivers.htu21d.htu_test +``` + # The driver This provides a single class `HTU21D`. @@ -35,14 +65,20 @@ 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 +import uasyncio as asyncio +from machine import Pin, I2C +from .htu21d_mc import HTU21D + +htu = HTU21D(I2C(1)) # Pyboard scl=X9 sda=X10 + +async def main(): + await htu # Wait for device to be ready while True: fstr = 'Temp {:5.1f} Humidity {:5.1f}' print(fstr.format(htu.temperature, htu.humidity)) await asyncio.sleep(5) + +asyncio.run(main()) ``` Thermal inertia of the chip packaging means that there is a lag between the diff --git a/v3/docs/I2C.md b/v3/docs/I2C.md new file mode 100644 index 0000000..beab044 --- /dev/null +++ b/v3/docs/I2C.md @@ -0,0 +1,457 @@ +# 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. + +This version is for `uasyncio` V3 which requires firmware V1.13 or later - +until the release of V1.13 a daily build is required. + +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.18 Apr 2020 Ported to `uasyncio` V3. Convert to Python package. Test script +pin numbers changed to be WBUS_DIP28 fiendly. +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 V3 library. + +#### Installation +Copy the `as_drivers/i2c` directory and contents to the target hardware. + +###### [Main V3 README](../README.md) + +# 2. Wiring + +Pin numbers are for the test programs: these may be changed. I2C pin numbers +may be changed by using soft I2C. In each case except `rs_out`, the two targets +are connected by linking identically named pins. + +ESP pins are labelled reference board pin no./WeMOS D1 Mini pin no. + +| Pyboard | Target | PB | ESP | Comment | +|:-------:|:------:|:---:|:----:|:-------:| +| gnd | gnd | | | | +| sda | sda | Y10 | 2/D4 | I2C | +| scl | scl | Y9 | 0/D3 | I2C | +| syn | syn | Y11 | 5/D1 | Any pin may be used. | +| ack | ack | X6 | 4/D2 | Any pin. | +| rs_out | rst | Y12 | | Optional reset link. | + +The `syn` 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Ω. + +On the Pyboard D the 3.3V supply must be enabled with +```python +machine.Pin.board.EN_3V3.value(1) +``` +This also enables the I2C pullups on the X side. + +###### [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 + +Demos and the scripts below assume a Pyboard linked to an ESP8266 as follows: + +| Pyboard | ESP8266 | Notes | +|:-------:|:-------:|:--------:| +| gnd | gnd | | +| Y9 | 0/D3 | I2C scl | +| Y10 | 2/D4 | I2C sda | +| Y11 | 5/D1 | syn | +| Y12 | rst | Optional | +| X6 | 4/D2 | ack | + +#### Running the demos + +On the ESP8266 issue: +```python +import as_drivers.i2c.i2c_esp +``` +and on the Pyboard: +```python +import as_drivers.i2c.i2c_init +``` + +The following scripts demonstrate basic usage. They may be copied and pasted at +the REPL. +On Pyboard: + +```python +import uasyncio as asyncio +from pyb import I2C # Only pyb supports slave mode +from machine import Pin +from as_drivers.i2c.asi2c_i import Initiator + +i2c = I2C(2, mode=I2C.SLAVE) +syn = Pin('Y11') +ack = Pin('X6') +rst = (Pin('Y12'), 0, 200) +chan = 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) + +asyncio.create_task(receiver()) +try: + asyncio.run(sender()) +except KeyboardInterrupt: + print('Interrupted') +finally: + asyncio.new_event_loop() # Still need ctrl-d because of interrupt vector + chan.close() # for subsequent runs +``` + +On ESP8266: + +```python +import uasyncio as asyncio +from machine import Pin, I2C +from as_drivers.i2c.asi2c import Responder + +i2c = I2C(scl=Pin(0),sda=Pin(2)) # software I2C +syn = Pin(5) +ack = Pin(4) +chan = 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) + +asyncio.create_task(receiver()) +try: + asyncio.run(sender()) +except KeyboardInterrupt: + print('Interrupted') +finally: + asyncio.new_event_loop() # Still need ctrl-d because of interrupt vector + 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 `syn` 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('Y12'), 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 +from as_drivers.i2c.asi2c_i import Initiator +chan = 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 `syn` 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. If the default address is to be changed, + it should be set before instantiating `Initiator` or `Responder`. `Initiator` + application code must then 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/v3/docs/NEC_IR.md b/v3/docs/NEC_IR.md index fee0ecb..a2fc938 100644 --- a/v3/docs/NEC_IR.md +++ b/v3/docs/NEC_IR.md @@ -30,10 +30,17 @@ changed to match your characterised remote. import as_drivers.nec_ir.art1 ``` +###### [Main V3 README](../README.md) + +# Installation + +Copy the `as_drivers/nec_ir` directory and contents to the target hardware. +Copy `primitives` and contents to the target. + # Dependencies -The driver requires the `uasyncio` library and the `primitives` package from -this repository. +The driver requires the `uasyncio` V3 library and the `primitives` package +from this repository. # Usage diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 12dda93..d84495f 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1696,14 +1696,15 @@ newline character. Required if you intend to use `StreamReader.readline()` 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. +`write` Arg `buf`: the buffer to write. This can be a `memoryview`. 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 +two additional mandatory args. Existing code will fail because `Stream.drain` +calls `write` with a single arg (which can be a `memoryview`). + 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: From 74ec36067af5bf6d5eaccce53836b512909c0128 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 23 Apr 2020 11:37:40 +0100 Subject: [PATCH 160/472] V3: add I2C. Add I/O note to porting guide. --- v3/as_drivers/i2c/asi2c.py | 2 +- v3/as_drivers/i2c/asi2c_i.py | 29 +++++++++++++---------------- v3/as_drivers/i2c/i2c_esp.py | 1 + v3/docs/I2C.md | 5 +++++ 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/v3/as_drivers/i2c/asi2c.py b/v3/as_drivers/i2c/asi2c.py index 0e704b7..7631cc2 100644 --- a/v3/as_drivers/i2c/asi2c.py +++ b/v3/as_drivers/i2c/asi2c.py @@ -160,7 +160,7 @@ async def _run(self): # 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) + self.rem.irq(handler=None) utime.sleep_us(_DELAY) # Ensure Initiator has set up to write. self.i2c.readfrom_into(addr, sn) self.own(1) diff --git a/v3/as_drivers/i2c/asi2c_i.py b/v3/as_drivers/i2c/asi2c_i.py index b64e805..cda241e 100644 --- a/v3/as_drivers/i2c/asi2c_i.py +++ b/v3/as_drivers/i2c/asi2c_i.py @@ -86,8 +86,7 @@ async def _run(self): tstart = utime.ticks_us() self._sendrx() t = utime.ticks_diff(utime.ticks_us(), tstart) - except OSError as e: -# print('OSError:', e) # TEST + except OSError: # Reboot remote. break await asyncio.sleep_ms(Initiator.t_poll) self.block_max = max(self.block_max, t) # self measurement @@ -99,27 +98,26 @@ async def _run(self): 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 + def _send(self, d): # 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.i2c.send(d) # Blocks until RX complete. self.waitfor(1) self.own(0) self.waitfor(0) + + # 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 + self._send(siz) if self.txbyt and self.cantx: - self.own(1) - self.i2c.send(self.txbyt) - self.waitfor(1) - self.own(0) - self.waitfor(0) + self._send(self.txbyt) self._txdone() # Invalidate source # Send complete self.waitfor(1) # Wait for responder to request send @@ -133,7 +131,6 @@ def _sendrx(self, sn=bytearray(2), txnull=bytearray(2)): 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 = self.rx_mv[0: n] # mv is a memoryview instance self.i2c.recv(mv) diff --git a/v3/as_drivers/i2c/i2c_esp.py b/v3/as_drivers/i2c/i2c_esp.py index 4bd0437..92088a4 100644 --- a/v3/as_drivers/i2c/i2c_esp.py +++ b/v3/as_drivers/i2c/i2c_esp.py @@ -51,6 +51,7 @@ async def sender(): swriter = asyncio.StreamWriter(chan, {}) txdata = [0, 0] while True: + txdata[0] = gc.mem_free() await swriter.awrite(''.join((ujson.dumps(txdata), '\n'))) txdata[1] += 1 await asyncio.sleep_ms(1500) diff --git a/v3/docs/I2C.md b/v3/docs/I2C.md index beab044..c63052a 100644 --- a/v3/docs/I2C.md +++ b/v3/docs/I2C.md @@ -376,6 +376,11 @@ chan = Initiator(i2c, syn, ack, rst, verbose, self._go, (), self._fail) # 5. Limitations +Currently, on the ESP8266, the code is affected by +[iss 5714](https://github.com/micropython/micropython/issues/5714). Unless the +board is repeatedly pinged, the ESP8266 fails periodically and is rebooted by +the Pyboard. + ## 5.1 Blocking Exchanges of data occur via `Initiator._sendrx()`, a synchronous method. This From e4b6560c962452272296b3a648e6a35f6635af18 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 23 Apr 2020 11:52:27 +0100 Subject: [PATCH 161/472] Fix broken links etc. --- v3/README.md | 13 +++++++------ v3/docs/I2C.md | 2 +- v3/docs/TUTORIAL.md | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/v3/README.md b/v3/README.md index bfefa48..458c62c 100644 --- a/v3/README.md +++ b/v3/README.md @@ -1,4 +1,4 @@ -# 1 Guide to uasyncio V3 +# 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 @@ -28,11 +28,12 @@ useful in their own right: * [GPS driver](./docs/GPS.md) Includes various GPS utilities. * [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. Typical use: communication with ESP8266. + asynchronous stream interface. Uses: communication with ESP8266, or (with + coding) 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. -# 2 V3 Overview +# 2. V3 Overview These notes are intended for users familiar with `asyncio` under CPython. @@ -66,7 +67,7 @@ supported. The `Future` class is not supported, nor are the `event_loop` methods `call_soon`, `call_later`, `call_at`. -# 3 Porting applications from V2 +# 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 @@ -93,7 +94,7 @@ MicroPython and CPython 3.8. This is discussed ### 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). +[tutorial](./docs/TUTORIAL.md#64-writing-streaming-device-drivers). ## 3.2 Modules from this repository @@ -143,7 +144,7 @@ New versions are provided in this repository. Classes: * `Switch` Debounced switch with close and open callbacks. * `Pushbutton` Pushbutton with double-click and long press callbacks. -# 4 Outstanding issues with V3 +# 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. diff --git a/v3/docs/I2C.md b/v3/docs/I2C.md index c63052a..35c47bd 100644 --- a/v3/docs/I2C.md +++ b/v3/docs/I2C.md @@ -31,7 +31,7 @@ application and is covered in detail ## Changes V0.18 Apr 2020 Ported to `uasyncio` V3. Convert to Python package. Test script -pin numbers changed to be WBUS_DIP28 fiendly. +pin numbers changed to be WBUS_DIP28 fiendly. 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. diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index d84495f..e9530ad 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -112,7 +112,7 @@ print(uasyncio.__version__) ``` Version 3 will print a version number. Older versions will throw an exception. -###### [Main README](./README.md) +###### [Main README](../README.md) # 1. Cooperative scheduling From 424520477cc614f44fc077006f4359845e2ec3eb Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 24 Apr 2020 08:06:17 +0100 Subject: [PATCH 162/472] IR rx remove ESP8266 hard IRQ: anticipate PR5962. --- nec_ir/aremote.py | 8 +++++--- v3/as_drivers/nec_ir/aremote.py | 4 +--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nec_ir/aremote.py b/nec_ir/aremote.py index 59c00c2..4ba91fc 100644 --- a/nec_ir/aremote.py +++ b/nec_ir/aremote.py @@ -50,10 +50,12 @@ def __init__(self, pin, callback, extended, *args): # Optional args for callbac 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) - elif ESP32: + else: # PR5962 ESP8266 hard IRQ's not supported 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) + #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() diff --git a/v3/as_drivers/nec_ir/aremote.py b/v3/as_drivers/nec_ir/aremote.py index 3448e6a..5742171 100644 --- a/v3/as_drivers/nec_ir/aremote.py +++ b/v3/as_drivers/nec_ir/aremote.py @@ -50,10 +50,8 @@ def __init__(self, pin, callback, extended, *args): # Optional args for callbac 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) - elif ESP32: + else: # PR5962 ESP8266 hard IRQ's not supported 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() asyncio.create_task(self._run()) From 213b898f2acaf893fc5f37b7553bb1dd03ba3980 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 24 Apr 2020 09:59:01 +0100 Subject: [PATCH 163/472] Add HD44780 driver. --- v3/README.md | 16 +++-- v3/as_drivers/hd44780/__init__.py | 1 + v3/as_drivers/hd44780/alcd.py | 106 ++++++++++++++++++++++++++++++ v3/as_drivers/hd44780/alcdtest.py | 19 ++++++ v3/docs/HTU21D.md | 2 +- v3/docs/hd44780.md | 103 +++++++++++++++++++++++++++++ 6 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 v3/as_drivers/hd44780/__init__.py create mode 100644 v3/as_drivers/hd44780/alcd.py create mode 100644 v3/as_drivers/hd44780/alcdtest.py create mode 100644 v3/docs/hd44780.md diff --git a/v3/README.md b/v3/README.md index 458c62c..78e6e26 100644 --- a/v3/README.md +++ b/v3/README.md @@ -11,16 +11,20 @@ These notes and the tutorial should be read in conjunction with This repo contains the following: -#### [V3 Tutorial](./docs/TUTORIAL.md) -#### Test/demo scripts +### [V3 Tutorial](./docs/TUTORIAL.md) +### Test/demo scripts Documented in the tutorial. -#### Synchronisation primitives +### Synchronisation primitives -Documented in the tutorial. +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. -#### Asynchronous device drivers +### Asynchronous device drivers These device drivers are intended as examples of asynchronous code which are useful in their own right: @@ -32,6 +36,8 @@ useful in their own right: coding) 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. # 2. V3 Overview diff --git a/v3/as_drivers/hd44780/__init__.py b/v3/as_drivers/hd44780/__init__.py new file mode 100644 index 0000000..3ffc82c --- /dev/null +++ b/v3/as_drivers/hd44780/__init__.py @@ -0,0 +1 @@ +from .alcd import * diff --git a/v3/as_drivers/hd44780/alcd.py b/v3/as_drivers/hd44780/alcd.py new file mode 100644 index 0000000..d271931 --- /dev/null +++ b/v3/as_drivers/hd44780/alcd.py @@ -0,0 +1,106 @@ +# LCD class for Micropython and uasyncio. +# Author: Peter Hinch +# Copyright Peter Hinch 2017 Released under the MIT license +# V1.1 24 Apr 2020 Updated for uasyncio V3 +# 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: # 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 + asyncio.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 + message = "{0:{1}.{1}}".format(message, self.cols) + 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/v3/as_drivers/hd44780/alcdtest.py b/v3/as_drivers/hd44780/alcdtest.py new file mode 100644 index 0000000..1261d34 --- /dev/null +++ b/v3/as_drivers/hd44780/alcdtest.py @@ -0,0 +1,19 @@ +# alcdtest.py Test program for LCD class +# Author: Peter Hinch +# Copyright Peter Hinch 2017-2020 Released under the MIT license +# Updated for uasyncio V3 +# 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) + + +asyncio.run(lcd_task()) diff --git a/v3/docs/HTU21D.md b/v3/docs/HTU21D.md index f3ab1f1..56f6af5 100644 --- a/v3/docs/HTU21D.md +++ b/v3/docs/HTU21D.md @@ -67,7 +67,7 @@ readings the class is awaitable and may be used as follows. ```python import uasyncio as asyncio from machine import Pin, I2C -from .htu21d_mc import HTU21D +from as_drivers.htu21d import HTU21D htu = HTU21D(I2C(1)) # Pyboard scl=X9 sda=X10 diff --git a/v3/docs/hd44780.md b/v3/docs/hd44780.md new file mode 100644 index 0000000..2c8069d --- /dev/null +++ b/v3/docs/hd44780.md @@ -0,0 +1,103 @@ +# 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. This version is +for `uasyncio` V3 which requires firmware V1.13 or above; at the time of +writing this has not been released and a daily build is required. + +###### [Main README](../README.md) + +# 2. Files + +The driver and test program are implemented as a Python package. To install +copy the directory `as_drivers/hd44780` and contents to the target's filesystem. + +Files: + * `alcd.py` Driver, includes connection details. + * `alcdtest.py` Test/demo script. + +To run the demo issue: +```python +import as_drivers.hd44780.alcdtest +``` + +# 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 as_drivers.hd44780 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) + +asyncio.run(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. From fe8c4e9c63bf730abfb6dc5ecad2325b27369c19 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 30 Apr 2020 22:36:51 +0200 Subject: [PATCH 164/472] TUTORIAL.md: await is optional in coroutine The await statement is not mandatory in a coroutine. --- TUTORIAL.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index e0ba9ec..06b5b7b 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -292,8 +292,10 @@ async def foo(delay_secs): ``` A coro can allow other coroutines to run by means of the `await coro` -statement. A coro must contain at least one `await` statement. This causes -`coro` to run to completion before execution passes to the next instruction. +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 From 26a8688dd34aff54e3e3c411009551bdde49ae01 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 9 May 2020 12:09:31 +0100 Subject: [PATCH 165/472] V3 tutorial, auart.py. Remove legacy awrite. --- v3/as_demos/auart.py | 5 +++-- v3/docs/TUTORIAL.md | 9 +++++++-- v3/docs/hd44780.md | 3 +++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/v3/as_demos/auart.py b/v3/as_demos/auart.py index 13fea5e..e3379d4 100644 --- a/v3/as_demos/auart.py +++ b/v3/as_demos/auart.py @@ -4,13 +4,14 @@ # Link X1 and X2 to test. import uasyncio as asyncio -from pyb import UART +from machine import UART uart = UART(4, 9600) async def sender(): swriter = asyncio.StreamWriter(uart, {}) while True: - await swriter.awrite('Hello uart\n') + swriter.write('Hello uart\n') + await swriter.drain() await asyncio.sleep(2) async def receiver(): diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index e9530ad..57b363e 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -1606,13 +1606,14 @@ demonstrates concurrent I/O on one UART. To run, link Pyboard pins X1 and X2 ```python import uasyncio as asyncio -from pyb import UART +from machine import UART uart = UART(4, 9600) async def sender(): swriter = asyncio.StreamWriter(uart, {}) while True: - await swriter.awrite('Hello uart\n') + swriter.write('Hello uart\n') + await swriter.drain() # Transmission starts now. await asyncio.sleep(2) async def receiver(): @@ -1633,6 +1634,10 @@ async def main(): 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`. The mechanism works because the device driver (written in C) implements the following methods: `ioctl`, `read`, `readline` and `write`. See diff --git a/v3/docs/hd44780.md b/v3/docs/hd44780.md index 2c8069d..dc6f260 100644 --- a/v3/docs/hd44780.md +++ b/v3/docs/hd44780.md @@ -45,6 +45,9 @@ This takes the following positional args: * `cols` The number of horizontal characters in the display (typically 16). * `rows` Default 2. Number of rows in the display. +The driver uses the `machine` library. For non-Pyboard targets with numeric pin +ID's `pinlist` should be a tuple of integers. + ## 4.2 Display updates The class has no public properties or methods. The display is represented as an From 3f13f7beb8f445ed173745c48a2477b2b7e8e870 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Fri, 15 May 2020 11:37:16 +0100 Subject: [PATCH 166/472] Add primitives/aadc.py. Add legal stuff to other primitives. --- v3/docs/DRIVERS.md | 272 ++++++++++++++++++++++++++++++++ v3/docs/TUTORIAL.md | 35 ++-- v3/primitives/aadc.py | 58 +++++++ v3/primitives/barrier.py | 4 + v3/primitives/condition.py | 5 + v3/primitives/delay_ms.py | 5 + v3/primitives/message.py | 5 + v3/primitives/pushbutton.py | 5 + v3/primitives/queue.py | 4 + v3/primitives/semaphore.py | 5 + v3/primitives/switch.py | 5 + v3/primitives/tests/adctest.py | 50 ++++++ v3/primitives/tests/asyntest.py | 24 +-- v3/primitives/tests/switches.py | 5 +- 14 files changed, 442 insertions(+), 40 deletions(-) create mode 100644 v3/docs/DRIVERS.md create mode 100644 v3/primitives/aadc.py create mode 100644 v3/primitives/tests/adctest.py diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md new file mode 100644 index 0000000..64fee29 --- /dev/null +++ b/v3/docs/DRIVERS.md @@ -0,0 +1,272 @@ +# 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. + +The asynchronous ADC supports pausing a task until the value read from an ADC +goes outside defined bounds. + +###### [Tutorial](./TUTORIAL.md#contents) + +# 2. Installation and usage + +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 +from primitives.switch import Switch +from primitives.pushbutton import Pushbutton +from primitives.aadc import 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 +between X1 and Gnd. It is run as follows: +```python +from primitives.tests.switches import * +test_sw() # For example +``` +The test for the `AADC` class requires a Pyboard with pins X1 and X5 linked. It +is run as follows: +```python +from primitives.tests.adctest import test +test() +``` + +# 3. primitives.switch + +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. + +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. + +### 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 `callable` may be specified +to run on contact closure or opening; where the `callable` is a coroutine it +will be converted to a `Task` and will run asynchronously. Debouncing is +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 + 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 + 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 primitives.switch 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 +asyncio.run(my_app()) # Run main application code +``` + +# 4. primitives.pushbutton + +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. + +## 4.1 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 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. + +`callable` instances may be specified to run on button press, release, double +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. + +Constructor arguments: + + 1. `pin` Mandatory. The initialised Pin instance. + 2. `suppress` Default `False`. See + [4.2.1](./DRIVERS.md#421-the-suppress-constructor-argument). + +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 `()`). + 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 primitives.pushbutton 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 +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). + +### 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. + +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. + +This `callable` 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. + + +# 5. primitives.aadc + +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: +```python +import uasyncio as asyncio +from machine import ADC +import pyb +from primitives.aadc import AADC + +aadc = AADC(ADC(pyb.Pin.board.X1)) +async def foo(): + while True: + value = await aadc(2000) # Trigger if value changes by 2000 + print(value) + +asyncio.run(foo()) +``` + +## 5.1 AADC class + +`AADC` instances are awaitable. This is the principal mode of use. + +Constructor argument: + * `adc` An instance of `machine.ADC`. + +Awaiting an instance: +Function call syntax is used with zero, one or two unsigned integer args. These +determine the bounds for the ADC value. + * No args: bounds are those set when the instance was last awaited. + * One integer arg: relative bounds are used. The current ADC value +- the arg. + * Two args `lower` and `upper`: absolute bounds. + +Synchronous methods: + * `read_u16` Get the current data from the ADC. Returns a 16-bit unsigned + value as per `machine.ADC.read_u16`. + * `sense(normal)` By default a task awaiting an `AADC` instance will pause + until the value returned by the ADC exceeds the specified bounds. Issuing + `sense(False)` inverts this logic: a task will pause until the ADC value is + within the specified bounds. Issuing `sense(True)` restores normal operation. + +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 +from machine import ADC +from primitives.aadc import AADC + +aadc = AADC(ADC('X1')) +async def foo(): + while True: + aadc.sense(normal=False) + value = await aadc(25_000, 39_000) # Wait until in range + print('In range:', value) + aadc.sense(normal=True) + value = await aadc() # Wait until out of range + print('Out of range:', value) + +asyncio.run(foo()) +``` +## 5.2 Design note + +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. diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 57b363e..2975f55 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -4,11 +4,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. -#### WARNING currently this is a work in progress. - -This is a work in progress, with some sections being so marked. There will be -typos and maybe errors - please report errors of fact! Most code samples are -now 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. # Contents @@ -126,14 +123,16 @@ pitfalls associated with truly asynchronous threads of execution. ### Primitives -The directory `primitives` contains a collection of synchronisation primitives -and classes for debouncing switches and pushbuttons, along with a software -retriggerable delay class. Pushbuttons are a generalisation of switches with -logical rather than physical status along with double-clicked and long pressed -events. +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. + * Primitives for interfacing hardware. These comprise classes for debouncing + switches and pushbuttonsand an asynchronous ADC class. These are documented + [here](./DRIVERS.md). -These are implemented as a Python package: copy the `primitives` directory tree -to your hardware. +To install this Python package copy the `primitives` directory tree and its +contents to your hardware's filesystem. ### Demo Programs @@ -480,12 +479,16 @@ the following classes: in a similar (but not identical) way to `gather`. * `Delay_ms` A useful software-retriggerable monostable, akin to a watchdog. Calls a user callback if not cancelled or regularly retriggered. + +The following hardware-related classes are documented [here](./DRIVERS.md): * `Switch` A debounced switch with open and close user callbacks. * `Pushbutton` Debounced pushbutton with callbacks for pressed, released, long press or double-press. + * `AADC` Asynchronous ADC. Supports pausing a task until the value read from + an ADC goes outside defined bounds. -To install these priitives, copy the `primitives` directory and contents to the -target. A primitive is loaded by issuing (for example): +To install these 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.pushbutton import Pushbutton @@ -1532,8 +1535,8 @@ 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. +Further examples may be found in the primitives directory, notably `switch.py` +and `pushbutton.py`: 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 diff --git a/v3/primitives/aadc.py b/v3/primitives/aadc.py new file mode 100644 index 0000000..5b292d1 --- /dev/null +++ b/v3/primitives/aadc.py @@ -0,0 +1,58 @@ +# aadc.py AADC (asynchronous ADC) class + +# 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 AADC(io.IOBase): + def __init__(self, adc): + self._adc = adc + self._lower = 0 + self._upper = 65535 + self._pol = True + self._sreader = asyncio.StreamReader(self) + + def __iter__(self): + b = await self._sreader.read(2) + return int.from_bytes(b, 'little') + + # If normal will pause until ADC value is in range + # Otherwise will pause until value is out of range + def sense(self, normal): + self._pol = normal + + def read_u16(self): + return self._adc.read_u16() + + # Call syntax: set limits for trigger + # lower is None: leave limits unchanged. + # upper is None: treat lower as relative to current value. + # both have values: treat as absolute limits. + def __call__(self, lower=None, upper=None): + if lower is not None: + if upper is None: # Relative limit + r = self._adc.read_u16() + self._lower = r - lower + self._upper = r + lower + else: # Absolute limits + self._lower = lower + self._upper = upper + return self + + def read(self, n): + return int.to_bytes(self._adc.read_u16(), 2, 'little') + + def ioctl(self, req, arg): + ret = MP_STREAM_ERROR + if req == MP_STREAM_POLL: + ret = 0 + if arg & MP_STREAM_POLL_RD: + if self._pol ^ (self._lower <= self._adc.read_u16() <= self._upper): + ret |= MP_STREAM_POLL_RD + return ret diff --git a/v3/primitives/barrier.py b/v3/primitives/barrier.py index 5aa73a9..6f126e8 100644 --- a/v3/primitives/barrier.py +++ b/v3/primitives/barrier.py @@ -1,3 +1,7 @@ +# barrier.py +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + try: import uasyncio as asyncio except ImportError: diff --git a/v3/primitives/condition.py b/v3/primitives/condition.py index 3172642..20df097 100644 --- a/v3/primitives/condition.py +++ b/v3/primitives/condition.py @@ -1,3 +1,8 @@ +# condition.py + +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + try: import uasyncio as asyncio except ImportError: diff --git a/v3/primitives/delay_ms.py b/v3/primitives/delay_ms.py index f7593b9..03df40a 100644 --- a/v3/primitives/delay_ms.py +++ b/v3/primitives/delay_ms.py @@ -1,3 +1,8 @@ +# delay_ms.py + +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + import uasyncio as asyncio import utime as time from . import launch diff --git a/v3/primitives/message.py b/v3/primitives/message.py index d685758..bf06558 100644 --- a/v3/primitives/message.py +++ b/v3/primitives/message.py @@ -1,3 +1,8 @@ +# message.py + +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + try: import uasyncio as asyncio except ImportError: diff --git a/v3/primitives/pushbutton.py b/v3/primitives/pushbutton.py index a22d231..2c6b551 100644 --- a/v3/primitives/pushbutton.py +++ b/v3/primitives/pushbutton.py @@ -1,3 +1,8 @@ +# pushbutton.py + +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + import uasyncio as asyncio import utime as time from . import launch diff --git a/v3/primitives/queue.py b/v3/primitives/queue.py index 1166bb4..a4e124b 100644 --- a/v3/primitives/queue.py +++ b/v3/primitives/queue.py @@ -1,4 +1,8 @@ # queue.py: adapted from uasyncio V2 + +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + # Code is based on Paul Sokolovsky's work. # This is a temporary solution until uasyncio V3 gets an efficient official version diff --git a/v3/primitives/semaphore.py b/v3/primitives/semaphore.py index 99f3a66..ccb1170 100644 --- a/v3/primitives/semaphore.py +++ b/v3/primitives/semaphore.py @@ -1,3 +1,8 @@ +# semaphore.py + +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + try: import uasyncio as asyncio except ImportError: diff --git a/v3/primitives/switch.py b/v3/primitives/switch.py index e25d65b..87ce8d5 100644 --- a/v3/primitives/switch.py +++ b/v3/primitives/switch.py @@ -1,3 +1,8 @@ +# switch.py + +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + import uasyncio as asyncio import utime as time from . import launch diff --git a/v3/primitives/tests/adctest.py b/v3/primitives/tests/adctest.py new file mode 100644 index 0000000..3ebc9cd --- /dev/null +++ b/v3/primitives/tests/adctest.py @@ -0,0 +1,50 @@ +# adctest.py + +# Copyright (c) 2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file + +import uasyncio as asyncio +from machine import ADC +import pyb +from primitives.aadc import AADC + +async def signal(): # Could use write_timed but this prints values + dac = pyb.DAC(1, bits=12, buffering=True) + v = 0 + while True: + if not v & 0xf: + print('write', v) + dac.write(v) + v += 1 + v %= 4096 + await asyncio.sleep_ms(50) + +async def adctest(): + asyncio.create_task(signal()) + adc = AADC(ADC(pyb.Pin.board.X1)) + await asyncio.sleep(0) + adc.sense(normal=False) # Wait until ADC gets to 5000 + value = await adc(5000, 10000) + print('Received', value >> 4, value) # Reduce to 12 bits + adc.sense(normal=True) # Now print all changes > 2000 + while True: + value = await adc(2000) # Trigger if value changes by 2000 + print('Received', value >> 4, value) + +st = '''This test requires a Pyboard with pins X1 and X5 linked. +A sawtooth waveform is applied to the ADC. Initially the test waits +until the ADC value reaches 5000. It then reports whenever the value +changes by 2000. +Issue test() to start. +''' +print(st) + +def test(): + try: + asyncio.run(adctest()) + except KeyboardInterrupt: + print('Interrupted') + finally: + asyncio.new_event_loop() + print() + print(st) diff --git a/v3/primitives/tests/asyntest.py b/v3/primitives/tests/asyntest.py index 7bc7ab5..a40d0ff 100644 --- a/v3/primitives/tests/asyntest.py +++ b/v3/primitives/tests/asyntest.py @@ -1,27 +1,8 @@ # 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. +# Copyright (c) 2017-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file # CPython 3.8 compatibility # (ignore RuntimeWarning: coroutine '_g' was never awaited) @@ -401,7 +382,6 @@ def test(n): queue_test() # Test the Queue class. except KeyboardInterrupt: print('Interrupted') - print('Interrupted') finally: asyncio.new_event_loop() print_tests() diff --git a/v3/primitives/tests/switches.py b/v3/primitives/tests/switches.py index 6a2007b..026ca30 100644 --- a/v3/primitives/tests/switches.py +++ b/v3/primitives/tests/switches.py @@ -1,8 +1,9 @@ # Test/demo programs for Switch and Pushbutton classes # Tested on Pyboard but should run on other microcontroller platforms # running MicroPython with uasyncio library. -# Author: Peter Hinch. -# Copyright Peter Hinch 2017-2020 Released under the MIT license. + +# Copyright (c) 2018-2020 Peter Hinch +# Released under the MIT License (MIT) - see LICENSE file # To run: # from primitives.tests.switches import * From 3bf9c96ed5813803d42494a473c3dcfd5145c6ac Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sat, 16 May 2020 18:39:49 +0100 Subject: [PATCH 167/472] Improvements to aadc.py and test script. --- v3/docs/DRIVERS.md | 5 +-- v3/docs/TUTORIAL.md | 56 ++++++++++++++++++++-------------- v3/primitives/aadc.py | 41 +++++++++++++++---------- v3/primitives/tests/adctest.py | 6 ++-- 4 files changed, 64 insertions(+), 44 deletions(-) diff --git a/v3/docs/DRIVERS.md b/v3/docs/DRIVERS.md index 64fee29..53eb724 100644 --- a/v3/docs/DRIVERS.md +++ b/v3/docs/DRIVERS.md @@ -237,8 +237,9 @@ determine the bounds for the ADC value. * Two args `lower` and `upper`: absolute bounds. Synchronous methods: - * `read_u16` Get the current data from the ADC. Returns a 16-bit unsigned - value as per `machine.ADC.read_u16`. + * `read_u16` arg `last=False` Get the current data from the ADC. If `last` is + `True` returns the last data read from the ADC. Returns a 16-bit unsigned int + as per `machine.ADC.read_u16`. * `sense(normal)` By default a task awaiting an `AADC` instance will pause until the value returned by the ADC exceeds the specified bounds. Issuing `sense(False)` inverts this logic: a task will pause until the ADC value is diff --git a/v3/docs/TUTORIAL.md b/v3/docs/TUTORIAL.md index 2975f55..f890460 100644 --- a/v3/docs/TUTORIAL.md +++ b/v3/docs/TUTORIAL.md @@ -29,6 +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) + 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) @@ -460,6 +462,14 @@ compete to access a single resource. These are discussed [in section 7.8](./TUTORIAL.md#78-race-conditions). Another hazard is the "deadly embrace" where two tasks each wait on the other's completion. +Another synchronisation issue arises with producer and consumer tasks. 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 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 +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: @@ -472,41 +482,29 @@ CPython provides the following classes: * `Queue`. In this repository. As the table above indicates, not all are yet officially supported. In the -interim, implementations may be found in the `primitives` directory, along with -the following classes: +interim, implementations may be found in the `primitives` directory. The +following classes which are non-standard, are also in that directory: * `Message` An ISR-friendly `Event` with an optional data payload. * `Barrier` Based on a Microsoft class, enables multiple coros to synchronise in a similar (but not identical) way to `gather`. * `Delay_ms` A useful software-retriggerable monostable, akin to a watchdog. Calls a user callback if not cancelled or regularly retriggered. -The following hardware-related classes are documented [here](./DRIVERS.md): - * `Switch` A debounced switch with open and close user callbacks. - * `Pushbutton` Debounced pushbutton with callbacks for pressed, released, long - press or double-press. - * `AADC` Asynchronous ADC. Supports pausing a task until the value read from - an ADC goes outside defined bounds. +A further set of primitives for synchronising hardware are detailed in +[section 3.8](./TUTORIAL.md#38-synchronising-to-hardware). -To install these primitives, copy the `primitives` directory and contents to -the target. A primitive is loaded by issuing (for example): +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.pushbutton import Pushbutton +from primitives.queue import Queue ``` When `uasyncio` acquires an official version (which will be more efficient) the -invocation line alone should be changed: +invocation lines alone should be changed: ```python from uasyncio import Semaphore, BoundedSemaphore +from uasyncio import Queue ``` - -Another synchronisation issue arises with producer and consumer tasks. 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 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 -until the consumer is ready to access the data. - The following provides a discussion of the primitives. ###### [Contents](./TUTORIAL.md#contents) @@ -898,8 +896,8 @@ 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. I doubt there is a role for it -in new applications but I will leave it in place to avoid breaking code. +`gather`. It is based on a Microsoft primitive. In most cases `gather` is to be +preferred as its implementation is more efficient. It two uses. Firstly it can cause a task to pause until one or more other tasks have terminated. For example an application might want to shut down various @@ -974,6 +972,18 @@ callback runs. On its completion the tasks resume. ###### [Contents](./TUTORIAL.md#contents) +## 3.8 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. + * `Pushbutton` Debounced pushbutton with callbacks for pressed, released, long + press or double-press. + * `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. + +###### [Contents](./TUTORIAL.md#contents) + # 4 Designing classes for asyncio In the context of device drivers the aim is to ensure nonblocking operation. diff --git a/v3/primitives/aadc.py b/v3/primitives/aadc.py index 5b292d1..5208520 100644 --- a/v3/primitives/aadc.py +++ b/v3/primitives/aadc.py @@ -16,19 +16,40 @@ def __init__(self, adc): self._lower = 0 self._upper = 65535 self._pol = True + self._last = None self._sreader = asyncio.StreamReader(self) def __iter__(self): - b = await self._sreader.read(2) + b = yield from self._sreader.read(2) return int.from_bytes(b, 'little') + def _adcread(self): + self._last = self._adc.read_u16() + return self._last + + def read(self, n): # For use by StreamReader only + return int.to_bytes(self._last, 2, 'little') + + def ioctl(self, req, arg): + ret = MP_STREAM_ERROR + if req == MP_STREAM_POLL: + ret = 0 + if arg & MP_STREAM_POLL_RD: + if self._pol ^ (self._lower <= self._adcread() <= self._upper): + ret |= MP_STREAM_POLL_RD + return ret + + # *** API *** + # If normal will pause until ADC value is in range # Otherwise will pause until value is out of range def sense(self, normal): self._pol = normal - def read_u16(self): - return self._adc.read_u16() + def read_u16(self, last=False): + if last: + return self._last + return self._adcread() # Call syntax: set limits for trigger # lower is None: leave limits unchanged. @@ -37,22 +58,10 @@ def read_u16(self): def __call__(self, lower=None, upper=None): if lower is not None: if upper is None: # Relative limit - r = self._adc.read_u16() + r = self._adcread() if self._last is None else self._last self._lower = r - lower self._upper = r + lower else: # Absolute limits self._lower = lower self._upper = upper return self - - def read(self, n): - return int.to_bytes(self._adc.read_u16(), 2, 'little') - - def ioctl(self, req, arg): - ret = MP_STREAM_ERROR - if req == MP_STREAM_POLL: - ret = 0 - if arg & MP_STREAM_POLL_RD: - if self._pol ^ (self._lower <= self._adc.read_u16() <= self._upper): - ret |= MP_STREAM_POLL_RD - return ret diff --git a/v3/primitives/tests/adctest.py b/v3/primitives/tests/adctest.py index 3ebc9cd..2d17e54 100644 --- a/v3/primitives/tests/adctest.py +++ b/v3/primitives/tests/adctest.py @@ -13,7 +13,7 @@ async def signal(): # Could use write_timed but this prints values v = 0 while True: if not v & 0xf: - print('write', v) + print('write', v << 4) # Make value u16 as per ADC read dac.write(v) v += 1 v %= 4096 @@ -25,11 +25,11 @@ async def adctest(): await asyncio.sleep(0) adc.sense(normal=False) # Wait until ADC gets to 5000 value = await adc(5000, 10000) - print('Received', value >> 4, value) # Reduce to 12 bits + print('Received', value, adc.read_u16(True)) # Reduce to 12 bits adc.sense(normal=True) # Now print all changes > 2000 while True: value = await adc(2000) # Trigger if value changes by 2000 - print('Received', value >> 4, value) + print('Received', value, adc.read_u16(True)) st = '''This test requires a Pyboard with pins X1 and X5 linked. A sawtooth waveform is applied to the ADC. Initially the test waits From f2116d9b2ea83b853a511377f14dc935c2e9b84e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Wed, 3 Jun 2020 07:24:53 +0100 Subject: [PATCH 168/472] 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 169/472] 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 170/472] 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 171/472] 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 172/472] 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 173/472] 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 174/472] 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 175/472] 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 176/472] 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 177/472] 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 178/472] 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 179/472] 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 180/472] 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 181/472] 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 182/472] 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 183/472] 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 184/472] 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 185/472] 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 186/472] 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 187/472] 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 188/472] 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 189/472] 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 190/472] 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 191/472] 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 192/472] 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 193/472] 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 194/472] 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 195/472] 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 196/472] 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 197/472] 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 198/472] 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 199/472] 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 200/472] 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 201/472] 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 202/472] 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 203/472] 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 204/472] 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 205/472] 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 206/472] 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 207/472] 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 208/472] 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 209/472] 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 210/472] 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 211/472] 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 212/472] 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 213/472] 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 214/472] 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 215/472] 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 216/472] 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 217/472] 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 218/472] 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 219/472] 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 220/472] 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 221/472] 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 222/472] 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 223/472] 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 224/472] 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 225/472] 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 226/472] 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 227/472] 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 228/472] 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 229/472] 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 230/472] 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 231/472] 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 232/472] 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 233/472] 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 234/472] 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 235/472] 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 236/472] 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 237/472] 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 238/472] 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 239/472] 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 240/472] 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 241/472] 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 242/472] 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 243/472] 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 244/472] 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 245/472] 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 246/472] 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 247/472] 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 248/472] 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 249/472] 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 250/472] 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 251/472] 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 252/472] 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 253/472] 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 254/472] 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 255/472] 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 256/472] 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 257/472] 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 258/472] 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 259/472] 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 260/472] 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 261/472] 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 262/472] 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 263/472] 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 zcmdSB1wfTS(=htbE!`~$0!oK;Nq32~v~+`%2Llk4E&=HdLFq=3Qo2Jzx-)ZY|9kI$_nfoO?Ck99%0up*ej{eZQ>B0L-dfB-&S!4Com!)TCUpFd#2A25s#4*rUd7YM`u zfXVQ|0+>)QUl)JM1?vxFdGQsH1W=KYQIL^PQBY9O&`{Aa39v9RFfd8+@UaOf$*8F) z$tWmj=vf(QuHT}gpt#A$bc>yXo12@Okze=@rw}U_Hz$k<92y!LCI%)678VI7Ed?#- z|2SQI0B}(OZ8%Wb>i|4190D%fMKeGR%83M85oX9A2@W0s5eXRu6%8E&Bq+y$SqcFj z5djGa5fP;J1?vGsTqHbN4sm3BHDi?PP6V7!BGORlB;J1{RPXyv&t>BL6b+q-n1qz< z1_R?wCT4CPUcNj00+M&7q-A8~IIL8h=7O!(+dvX1NiU*~*+N+1!D_WmOp9hdrdLKEjcbRv50nH$?M)h;#r?QNw)i_QiPJ!M%wxcHhjp9uxJBqC(=+EOGGX3n2G(J^~+>z=O8Dp1l3yI49z>??&A*BHeG}%C+@)llWlIsCEiC-yOuQl=8X+DTb5%+W#L+x^p=m}nw99gyl*nYA+coQxp>b=k*1PvyCgkQ}2zNr&6{=wgFWv(fHHh=4cGd!_Ui!p1s!hxtd@ zi4r9uZ!Uo6qMxehPMJGUa(7y&zXv{Yfj+q>dbqCLN%9itVJZuN@ z=u%h4!+SDEK2lL2peVj~a6yOoBYc38%?W5dX**|E`F;WL7sp?j!|!_gc~eTOpGZ6v z%J**w&>OLg6CYA$R6q-ui&2jeA%-sA_*@*HX=BmaK_6D+L`MO2V@>fHH%vp?FT?fh z<(XJC-`7H(xt`dndT{5VAg%sW~V;hO%zERu@N1Cd}*`2h~ojFtJQ zNJWLu8Bm;J%nP8GT;&32R4`({8VA-Za}Cg(*d9?gbvnB$ai=`G_v~93#yeG4gFF5t zKvNy&(n-Q`jF8{A@_d0q^=8YREEoGubX|H=N`$DMVs5y7sqDb4(GTx*b-#W>UqU1E zL-yL%+B{=QHEK7j9k`X_>=I9h)eZE_=ZnIv7_k<4+cT9v=LT>uT9TU+f=E&0U(N$poUDlW`8Q^pB2FlK-=2v z5Cr%V`4G-TQOCOaids^$MOV$;ncEhP&s0G?GU3x!B39N`M=xngeJc3Oi2FPld-FE6 z<+Ib5V)suJkMNr^A3RKWc!%!`p#AhtrAkk@mv&c*WyQ|u26+iPN2}oO zv9_u4i~>tNkwWVu+(8DMaFWe#-XalO(-g&iX9X9s{TV4H#o|Ianr()+K@_%w_U^_n zzQ*g_*~?!%_#S|KupD^$smCF4t%t|r0L}hP$|p6|`{mkG40Z$&Q~bTuuG%spaUOPvk=^f870uAp zfXHRG5UJbH8dOQm$HQDL(Jl(zk@Y)}Jy#Q7V&ojN_)^kSZcZ46d?-!*mm%c!3 zv?*OQB@!*A-AMNG*1Q0|hw6LQihQ*^l(al$CF7rEvt(_fjaZYH{k&LNRLEHVULi%K z?~viVajZA=n_FAGRJLAEo*Z+~?dv;VLS2va1WTdqpmFY1RU53E%zO1ktQqhf2wecX z`&Q6W%d=;+7XbRbz8w7vAWP@)0(fmEI-`(GHy_bCZzps}5KmR!J8@sZ7I`gEBlc@+ zBKDz9y>Px-Mz^@Ab?F805u}#kB&H9wq#KULjp%aCk$Hn7GGsLXeG+~F__G~w&p{0f zPv)TFup-4aHGQZl-SDe0-Id1oOFNU$ACYLz`}m7Ns3vZ5eF%fhTetT|k7uB5(0B`x0eo4}Ssm<-(7}o+%BrdWCZvsoAPQD+ zeCBAV*JR3G)W4~v2$gwjKennW?qE;#wex1do6u|QkUD|AlY6g3sH);EuOD=J>NPHs zVhidKhCytHJ$CiD3d+pjJ4nPHq4-l=D|gH*u91@0Hr(MJ(JwibY(LSBc_5J<35c>{ zPYJVo!5xNG5X~99PcpKP3~%r7e>mnR8R?9?PIYHVwEx|5!9gU~ff|o=o&^W=XeKj)qAI-g?P-#FGr|@_zTCGn9BGN_blQLqN#VksI)^ubV9xTE6qaX?%}`j zyF)#5-!jXNt}5`wpkJ0p%xB;f%02O>lDOpQvRoea(Hm>g{X;%zfOW;t9-;f@9V5M3 zyonbPMcGrQq!m6(xd?AgHKg7qa@h?TkK??X#7vFkXq#*k)##5L9hUEZnD=CvCAA+i z*9#dbyZ}yciXP`0O(~J?&_$ZHb&^^bf21gym`lFqo6js0f8TZC%cd8F=-Pvmlyl^~ zQMZ0*DTL0@^WbaG@mV#v26sWZF>13~SvQddOzzF?7uLtcS5_E=E;P=4gsg#+EB-Cq z1wd^Itqxl=v^l8F7+#$#f(%z0b;oVQdG>r|b-4fx63-qU7@BCsndS^BbKeA)#blHV zKteZX#sW)t3`LaN=v3JFWkK3217(f@yh`e~ccu)Yx+Xtf02ObcJU$H@FP8L21eGs< zgKd?lv@@H4Q>~t}vz|3*d1Gr>xdEhSsO|jRVLR`@+*{L4Am{D{P?U24NC_J%2QJ$L zhw5aPVmFE|3iEC(eWXz`WU2rI z!cD~S0^&-h!-p6tg#sr`mM_2BJYaphHaRI5Y{dgvE4TodI|88+8W+F__L@HJkb~^U z1POs}J!}^Mu_CmurrV>!%d$HT6u5AR;o)kw7h&Vs$Hujx71!yb-7?S`Acxf8-M-vO zGoOu}rzl%la~A+rsx_Y&K9_zgSi(md=S4lCJD~j6 zOIkBrxdM%o8yn(M&b*Qbh>dvf*+xXRF%DFV%p)oKFmEkf-L1jUH4sm zHf-mjLWk1A%hcNP&T{9bcTy3BSxorjjUNtTnmP&1BRokN#BMFQCSXx{y{0;}N$=t4 z+qyzBgMyIJWem4A9}O|qv0X0Kg3OJbmj`Pb}(jfV>%Drh58gVZgoKX*+#d( zBbh`E=rgyW1P!JTck0t^>I(p0=Jmwcy9;3Y#RYJb5F&pl)cQv&YSX$4Acdv)bZ2@- zd6)e37okuqbY1jtRusw*A~~^3ejwO$yz_&1cK0shIrJ2nB(tIGxXA&AOnH#+(cv=oTYkW3EuX1RZkh2?tv z+|IBFAIKk{z5_SQSTs9dvH^ipvQ;#aS26fx$S24HCFRXr7q0RDhK?8l{C`* z&~K*u5qqPKAy7Vlsx5>k01+6LBQ?3n@$CLmmUUQ-NOUjJzKLjMxzPy}JOxYhXtwXa z6*g3G+ZM?-E8m5;<}6ACgmSOC`7%!}o{* z+g+o5zpzWpE=j*+CV(SZnqy>lZrPVby~K@_e)p`HVy@DI?#_OjSN8RW1?cy) z<%z~A>LeK{+0T4zhk;AdFx;xZO?ah_^O@sb9p}f!$Ahj^*?#nwbmVcTpLTPXQ_(PT z)fKK~)bn+Povs}Z4wne28GObs5ol)r;waOd@s{!HR^!si8uS9-JL<26XX6#!DqfPm z5hm&j5s2D2Y}`EuT?~?VW+ipLDRbgBat?LaPT#8BKZL_hq&^(k&A8&*=ui*({95tp z+4P*uA1wQd9o9DCd1Hk)c8fKpGClFv9g{v$*}hpCW9sttu5TPDs!2Jvot{&%Ih}t0 z37Y7V!mF6VU)m?UVsHR$m@OMQSq*Nmg-jcDhh0jhe%^2hg@Chj7ZO7KN1rN&(<2jd zB(Xy0`)4acqu?>SF-eha3*3aLM+~sfDjc>mPkpz}K+l9sFJ1s-JR9e}AEXxBK=Rcu zIBK%V(Rr=_92FGx@1W5deXgJ`fY_buafcV?_zhMcWZdJ2dI6 zRlJFMLaLj$G#-32xJ>;z-ePdnD(OMKcE!Qc`E{!zQ&QZdaJpCDB0h&x#cxOvj8{vE zvVj+!l#ZS=L=~u@!omu8R6O>)0Q`b3fVPL}adcb9kZ^sKKZ#`Gz|E~^VF?P)cmYtR z2lG=yI>1x!-%YI)y&gm6c6KiSIQ$DBv`>m6tZ@MA?VR<)IR}-f4F5O*jKRZ=d=u*)P+kfnmosj63o-Q;Wd;N>d6(K0ZCkttL)G zSbC@;DLo8b`O#EYna(H`gxv_##E`%b>XkjE2zse6#x7>z-^ocz8mX$QNXscofpIu6 zhL&n;YUhH;4ghxcuFmQ*5;R~)hz4Z=KnLIfY%nxOYHaG_D6Xoi_%kr|&(DM5%dj0V za2v+!pX>i42FuLc#S{!^(STH9rjE{FAQ0{W2=jWlI>O-BAWUHT(AW%w3qkmnGbkVk zkHYXKSMWLvw!DPlK@cEt)>M}Sbt3>_8p}UmlRse7ht75&4L3+bZ)Rr?@`t~F1)ITO z9~f+B>juhpnP7oAOmllJ4e&`1ekcGLKn_p>Q~?^m1aJea09!DKc^iD%gBTY;9jur5 zH|@zT+pB`DOu$xFfGODGF5m#z0mhf@0oWLTG+_E|TNev1&Px;=rUU>WO;0Kn1#0Q_D6K-asn8(0Uc2Qo$gKohi;Viy3!CjbD0CCFR< z-{=hs2>-I%f28?gzstBS0z5qI4JJhJjf4t|fFhxzpdh1SqGMuWpkrWQUBkn{x`ulV z0|SQ$2N$1!kdP1)o0x=%fCP_#kl-?IivYGkL_$MCLLqZVRW62*xwP zkmb*~Ed}WK@URFc%oJA%4h+)5;?h={cYl~-_Q z<-}8?(2~!fRSj9`6J93p@T|vY^?`*reWdse^8lLWcYvk^Dxx1MedZVo=(aZ%#gY40 zGhY`EgG-a>fcF_bS84Jz+7ZMdQHR{(1pvs~xi3{N?m8cC!e7h$zEfO4oT}$8i=j+3F7PP0yA% z4_K5zrIAsjyIt!apFyLJx8|o>eYbE|js+T3PI96Kmqya~BR68kZa5FRD*L`pmLWDY z@B@JMW>|M?CT?VXH^yK7%2d(ei)%yMePg`Bk zSCOS@V4awmz;c>0*hX_%z;&^mw2hW(T@?0dXrO%Kb`!)u{RC#uAjQ-Q8^^A$I}3uA ze3D#hJl5Y5Mg(6^6~`@qq<@{5Q&pw_dQ-#`8n6T}3uXXG9HAfh zzV$Co6fb}96H%hbRdH&dDVD^g*KEmC)<_~pK zEJUtn4PBv>w-ci95(P$A=Dm;49k690w-yf#Ax1B@7WVW{U3hv_bT(Yp45AtJgoSb( zL{k^OMvR1o?;ShG$viq?dpX^h^uc+1U;S6 z*w1c-Q{C04EfKnV<^#hnU0?Ee_LJEZT}~XsO_I8)6_zQu=4-zniWoU~#5p9NoT(=` z?+Umq&v}ncBNS7+E$*M44$SRO_)?ixUh5G_NJ#27@aeI~^Qd<38|HBna;RLgHBJ4Y zs`%JM$p^s+Vg1bpL*?WO1N?_W0*iGnYi2tK1};N8-nmO}*B!Vpm`dM?DHfqi40B*x^py=PxmYzyU*>SDE ztZ(HIs{V>6w2eAzZ~DeIRk3KV&*L7Mw!U2zqo6`-h4z)6jF7$e13hjtTRu}gi6?c2 z9f3Jt4tEc$_xc96oYoe+zjyhfP}Pui@jmK-^o%dP`=Ru+(rULj))Fsj(SmL-pZJa^ z$n>P};>HF&+MhQwk}KcWb>7PEt`KAq4VR54 zqi`;0}nU$UTv-3?~Za2XRuVceAE+g@_qJUDR zWUlGmgKF4m8kT6*H|@vd*BSMCR(F%bSj9k8(Ne+up<0jL=#&MKhNT{*kR^og{qBj6 zM@H8k8((RYmr!z8oW1h#s*P~BZ(eooy6I!S#+1IDjkdK<9VbG^3TbtxV1%tUi;enz zxwXRZ#6&sIoSmD!)tyPtjNaWfT{|VsIE8x&g&Q5VH3{7Z9zDaibi0n-3M&ZjT5nOX zj`ojGWRd8*^ZT-8d$I-XdqSVh9eA;dge9DuDVeGp$|(C%IAnz;dlzGSFCO?z56vFv z3yWwsrquUjhvs;P8L>Jlo~JX2 zLh=wcVyp@Enrhu;FM-Y5Il<%lu#|zhdMbxB&-!HizWF_$T4#?Q4V_Jw4W%v_Q}XTl zGbpIPs3Db6-_jRuKH>k`j|-IUUHY-5x8Twm(QS*fnqfWkbRLgTVp}U4(2rRKDH`Gi zeBzqDz{r>Q_Ng#2wM>`qV}|#6Y;-vyp?v*qD#9ay^}F684y)VVtW+s1(>+s512P)* zZN)KLpLq5tyq0aa$L2s$L~W8kz;uNjLb?}X;g)@%#r(@V^B@9!)63Avp2DLxX;fQt zY-FjCba~gCS~RE4OK4hR*IS4>ee=Y4Q$S@fjwarhT5!cOTW+gL)Wl|T`wq1S1OT|+h2PomjV|ESumnyqbIacM#jm$SYQsi$lb{SPS@e-`#xw{$v#U~CG*{at;7_YAhLedzD~9^g z`o^1mm=2^2%m9E<+Mn31j#MvVIHjV(piKjAt*EyY2>^gd-}W7NIJ!*D8}?e}4BN|( ze0`}Ju3bAblIStI!DdpXI1r`-$jxtC#eVpQ1D9QV*RoBIYo8bZaB_ewt80a=>%MSQ zPcLjNxP4z&0y_Z2js3v>N4#K~fZYN#IBtix-eF6AT02(JUSex27I(hMtALdX#~9m^ z4~lYivbs8E!DHBVJIH^paRb}Iw(x&@)IrJ$h(NxJgLX@R9uK4y4`Q@&vFPk_>HU5J zb?Mu7P6Uy7o<6$Z2K@H{uA*~suNWf-?Dz6`1)ewDO4NwSU+?dn9JxCUrov7;m|)}k zUi2q~x-P-NouQw#m6EqDr%!{FA-La2cSSIj4vfQ{QzXGS0Kd?eLzc9QiTY6 zZY7Cbq5l2J%&7v_43@xc%G}TEnvrSu!BKqI2LO!3^ZYo@e9!SPSrPos|22NV(+7*e?P}& z+d8hWbOZ+h=}od=G5O$1IDkOk0=5C*)nQ%&a48bAzz$TLikbj$V{AjY8c>d-_FyZ1 z3A!%;M>_V@4}fpAyR;^VfGxwf5Qcm%fV72%z|P^CwZ+w<_ioWeY7Ne|WAxyOZP>D| z9YCOi7Xztj+MC0{$FD;(=p)Z-g{?;T5gEX^nK^vq^?Fp{G?5Bg@XAB=FIj@w)vz`uZS7F2Ea@bH7&M_J;d@{lh7JctL+=M> zSJ$B{)XPLhMUtB@lL`+P_qJ{4s>OaP@~#S`ge5o>OXm(?A0jwCFOt$-DgdJJJUgv( zL970C1B1==pEt07p?v^Yd5aZB0!vrMwd_7Tx&U%^3%mWmZj)VzlGoy>1}q1iP(>!Kki`VPe174c?K#IG7rk zRk-Ev0Io-;p1wjFsbCpFpb0dnVC*so!_w6ryU{?L(c{Z%sJG&LB9r2U8}W&6eP&n; z&(qL7>z6&QdO>ECOqx4!dEW^(Mz(nhZd2yqs)eY( z>~DhTsE*kI@Cn-%emWjpQ16l_)tmL5R1gJQ=Kt-TnV9?mNCHb&a#wkmg+Kd$_|XK51yc6K>|UM50)i#OcK{~6u%h#lVw$jiAkP*)P2cSlr-#`$SuGS(j z71lel}G{)VKKv^WD8 zsYC^-;gH~S&v@cw!N2DT*l*5O{&+9@juEyEn%o(GelGNl7Ro?y>WPykwbJUddHJdJ zus}D9o_Vs~wDpQ!+ld|FjJu!p&~%g77L?LEPF%Fw=He@e&?87Jx!ncwh#tC`t0 zT5~2n^nEcu!{lW-uD^!a!PBzh?0xaQ*<6ud?6G`=@=MlJ0{5+)AOAvP`iSJe%d++N zY(9(8!CH57in<+Ch;z(ZA|hWH7G)DK@>wfl$H2@dPKWx&u*_$-%fb^KYMNp3Gtt7E zR3Gm}7l+%>&6Bks!N#I9yYrMuI< zBR4JGh94?9XkyzU=^XsDs)Hsuhm--;%i4+&%0FuWB| z(O~?lfpzMMjV9`mvDW>f^#$>hPA@Mj?hf_av{o3hx)S|F;bs1rw`r}V@`y5ZBVf3Q zA2953RcVExep~iWvOg%mMt_%B$vg5@(zPlX^3)Rw+j8oHPc`EU*)@ZM>HG7xnh319 z2CM8p@MsZ>=N2(9?*tt#_rEb;6TUlRnCH+>J!zpZYBzOeXo_;8MCnZ6b&m4`!Yb7tIg=!b2bw`EY z_)la0CCC4vg&58!yz+~EntA2Aw~k(hiRsIN0H?D7sRMqre!mi?bfRPsgOr3nvjXGY z5K8&9!sva>-bwdY{p8MmU)Ax&Vq>(I*A%SAM%(37``8xI#YR>Tj54tvhx45Le-Da0 z$i#ll^rt+afX7e&kB|+tPsm+oliA)kvCr4bmCQn3=iQm8!F+LTfPGenz5riEX=em-k+ zVNCyQ`Uf+!n|#Xk_1pn(BC^ASgf=B6?LroHBKT<{=Za-X9%o&jby_mxK7MfY;Q>1d zCNctWAGb44#y4aqfzYbf?*|gj?*seJea)c0B)1iI{f95ENI~oa_G^rXw}>!HNX>4; zTEkG}YP{9^MD*_)G!#2uOu$I^f~3ZS-kA($*WU}5`|yqgiE%?Razd_AP|&+r+?^li zcQ3|=Vd{#mt#N25w2A_SFn*i_Eu38rvsjWAIi+a+N)cSL_&oIPf=SwN#y-CLv;|GKcUcJq;%)v0VO z8Rz6UDHdixfTSdCT6k~!Lo-fJ9a1c;$y7D+2#q|cyJT-OZ_yfKpx&2gBEq4^>7b2; z0q^{u|Hi)a_qW%w!#GLjes`qGA_G<9Z}}LU+R;D4qq;Sr8j_TFqn;`=Kqvi6 zIDYAndGW$Yts|Yf`5+zT^?Q=FB2`Na0;yg21FxF-0^vk_jOJQg5}R4kj~_EIO-kG; za=C+tU2)8cmXMd^h|w4?OeAKBG5nimJTcl0C3c8$s-DK)*U)D$ zuX!b3T#0W<(;06wyESb^lB@VID4jmt^{D82J)fh!n}h(#cs1@M-(o|vd}o52FdOzs3pqvpAjcIP*BuX6rZ z4|yz^*6;e_%A2b|ay}L06Y{>~PcX^kM{+G4|5U9$S4o?cFF$+|WD_QgDIUXLxr%Rb z*SES{);ZQKns?q}gKe-_CkHoH>?Pc`!$kYu_eHn5f7YY1~gP zm@c9upY*fGoVKePi2S*WUm7mSMD;XNAX2w6{lzvA4SLXkB@gHh0i~fvQ$g;g= zA^#!Mx+Wi!`k4$f;@@Uza{sCpJ?%61@-LY*a!be|;*s2>bpIhEl#I;!OM@RGQwlHB z8@)1akRQ{F@Q^`h=biPD_FYMojl4(gba?rON^XkG!QMA`b1XzYMHro%w=g)7H;#^1 z2A6z<1eaU@m+NTOzL1tz6>evc)KM$h!i}Lc{(KDGB;oo)culPPxaDjNWArmBwyg)R zC}N^5`O(xw9W~|Nmc{7H?*!OPr+2yAj&0^K-ypN@rBEBVHyi3kYVA4}KAXpvzrsnO zbo&A*_dO&?a7D^XuzR zLmlaW**Mi#r!S3j?v?l+qAm)R-HqPqQ`g9sSD%f|rcjS`D8@YG6Hb49=2aLJmhJ!g z?XXRUJ=vtLxvZO&2J6jL97EkvjgHTC?M%`^x9+?hO;S$^U5$OyV((Cu891p<{koJP zIzwZ=RP%L#`nd7J+mp<;al=z~hbZJ@c8alQ7K^fvD;_$a*<4SRmGqjICgoQdmtnzj z=InAx_HC{^?YJu^&pFOJp`HQ$yrt%2_-fv&lQ#Y)Azo;P?%K7t++WnYzI23DXWw4f zD%IC;%D$6n=caL1VI0*$9c)~r9Xjo?CvO`$>!jc3b0mA2ORZtvsQ z-r90vEUBEfnyl{O^)@*Pn>~sd__(Pi;Ewo2G(CQjc*P-}Jod3s-+1UQ%h$$%Tvyh8 zXJ12ZfCexuN4lGZMRDV~x=b1B?@#f^K|` zrM;Dd%Il@|&B@2pw z0F={uYllAXzn`!==d5wraVfCOQ4wIvIN|ik+~@LyjLK+WN0H|ixG1z3>alIYv5oKe ze!?zON#*;MBU-AER5;0;+y`H^s4iV{i)EBQdQeSkirn>wfQaL$H#6am2>6!)yr> zJmUBd^F)S|@77skMmgo8b|oZ7ZP{RT{$zBM8=7Rmf6PBrI*^k@+;>hYqV5$`*SJoX z7x+13P^&s?w)l43Au_x%3)qvI3C zqVn;p&k1?Mpx%fi;1mj&<;-bdv0czP44`&eS!SUIJBC&RzoRyjfc#y zq5})?sjb{}?CTQB$}$V0f`*qw26PKc$)3N;D3IkEGWw+DL#eF(lzD*6speH|K4*Q)WUX~i zy^ANvO2bV~iBU7kqyTS_P$#zBy8peBp{`O*VRpy87?aihScL)w$D#YNsao+t>Na@2 znOX}Kv8$1~nnCE{I(ekv3wc(Gc7a2vA~{`NVEt35x0*Vif=zy&+6LwV>Bth({5|su ze@(Mn{F>3TS~{<*neso2jpQux2_GaFn3#`8KMM_>XjO94QO?RA9NmnO7w5~DYZ>os zi(D|&D{({P9F@~G0+(OSn@j~-7Md&GyaNn#`CknWqD~PUUs&MOJt=&0D{P-^u2M7i zZD4*CY;Oz5K$>E|74_%)Nj2u$frupI?Z%>B~>SS33B*dZt;2m~Ts6 zz4)CcPorpJo(a4`iC zJfMJpA>L{-*=cw>T@Z7zPmO0aiK7zT4rit9UKT@!kvqbm1@yCPtjtFtj$A>~LGXts zMVh8U$9UwLRtARShh@i(zYfDwW~^BhoZpJ$W=)a?>4F<9;llJn@LHE8UCC1?QSG1e zNsy-@%FsV2OW?e9=Me&yb6uEMxKPifq%K;|o9@>cvJt;*nf)Gv;Xk|m=tl_z44`}< z`ZvbW&8QY7PX_bP%Z2F(>;(2`1N?nS17k|3&tR zHJNi|?muKilOhM9m@YC{LI@Z?E;!)vpQH246x8Z5ZHqlaAMFkQ40sZp53tzd6HHXy z+eG048d2HZX~M|v3CjceWH!%~9IcA5cs^!Vp(lL%8^Jw-{12$=fPX+#{Y~RPpx>zE zYSkUW-KInm~DPH`4?P>Rou`2xr65| z`j9sE5tg=DhpL2^v~ql+AGt}3rR`&wRub$~m}=~#KFj53-Mb-{g4JquO@!2gLj+&l zdr>V z^JV)eev{ktM~H>}9M7u(Hj~e0t2<|jkqpD|37}~W%;!GH=iv3DNIR^#$0?&Dkg?z! zW6#SApqu4QH?5=A@-~OcD*0^j$Ong^OP@YdJR}pUa=zjPUU0d<3$B-tqQ*(UQNjLq z^8Lpg0o8`0_`x{me|nrVZkb7Qt2MO#hIA*b0dWu`t#>fSe$9?Y)|I z7^8)prhKqzb3sQKFYV)af9SiHekmes+p{Fu;E&pl9sjE>=g&O}83CAiym%SN19vC{ ziXL#g1E=3o)mL#nKi(IuYM9Sor6Z0Juf`8wxvM0>)CBYRQ@T> zBlC|&Y|Sl#xiW-4-3U~TKRT!;{~i6nY2jupSi0kA~5G-ii`QGug!6+!$Lp2nf7r!JZ3&^x% zu~DkbG>q_)S|R@Za6)G8d(gpLXv_03<6iLp{wytj4tH8635?Y^VZ6`>!)7>_fg2}y zoxh4mxh0!03bGd(VdoN)R!Ro}DW38Mpo)*gl1w&M)2_H8gJTAT*Kkyq> zSzPcNQ~(_j4Gk3@9u5iig(?94(;i$rd~vlW5#KpDxzv$pC7hh!f9#(jpb?W)HI6Ll zL&mwz{=npE8n*^r6Cn}3-|Y6~H>}`qNJQam7>845zpE;{eEUl4p5pby@99fkGYm6o zpI|A*H^x+YoQ5pnH?r?#d^)3FRAg`LjU&CWn|598k>v|nY4~~IVK|e>F#$+BeLX@p zji&i#cm^@biS8L2>8<*;qj=>?is|hNKI>WU+#82sZ-51I>$ova zCZW$F%M+@t+3hRzMn`cgI3)(Njoa*Xxz3&oC)tk1rqA-Yz3U*n*U-WxY!HK4@Xb`4 z_@6_$n08Ioq-7XX7{A{U*gj_Oeabz`aVrXOimThUuQmNUHv)_DX~#$#xkKVn8sf<7I!k!m3x2^ zXq|AI#g^QQHl9JbnL!qi*TP9gd4eXj9k3m#9WCKNK%17-+^>1mlBGaemVriMAF0JH zoI}w5P&*zWlsN#eN+LG}+Vtlq%__CpLP`OxPU{xKw&uIU7WF0v!XHJoM-szNH6iQg zEU&m_SLV+}UN{n?QEQ)v<$r5HwM0&MXexkzLY-*Qn}#y^P%CoqDCMla3Yl%Co^ z^@u`V3T7O&9JErF-QcHV&oP6;^B{K#Hl&S477203Yty%)^Uuj}qu(afy>ei?Zojj` zumw?QPpm&@yG}mc&q0)|zJu%lVst)~ok=gShPtsv6J+Z}XDdXryzs!4YJI?5 z5EX{oWo^ji%=i}JsB}w7NL>DHSAN}TjKl&P8H%*6)^(R)#*mTgA)e*;xOB<5Hq{T@ z{Lz=Vc_r18Pc;w6a~cMXsaVudh$Wpyx1zIyET_lAC@||L^ZM0+1jG&0le*EQayTr~ z^W0>?2q6{a@rcfVK#TNhyL#^UEDz1>S20L!3?7=S8Px-KroQ$unBt1QaB*jR_`F)s zj*-vgarqxL(D$bW%DrB4HM4u%oO{e& zFNQjk(nc6<$Yb8DmUagYX`g8`EMOIdb}|9qy^;64Nr+zqdH(x-^kb z?8dyMRKUwzBqKU5&{IwE?QK`C73o_n(nGdu@6yWn5p?f4DVeZ)V>{;Z5=adn7cTHkZS2BVJ(=Mxw=_jQ%g{KIF2t$diE(ezl#BRYpk&hEhFZc?i@wSt1vah8(InX9 zsmQ*ZJ6Ne*c?nt6Cq>ezYt%Jm8z`BHMQoCiY!Zc|A^M-kDV^P`t97~ygqD4^AT7@J zs^iP+P5JX18VP;<_A=#IwxqKbbjn7{- zoXIE?f)`5my3}h6hA75UT`@Ovm1JC_scP9|)Ulic!MX85FRJ3mopD`74qIV{@A_(T=t4-cODeCAOe5Rk)HJXx6G{4b68FY%@FWG-2bD#FuUR7G2Q-j}t z;G~8n_G~ylS3n_p6O#XQ-yU`!?EUgoM{ZLB_oF&Li439aY#v3PKKKD&K`v!^ zn~~O8&g`MgF<-z}yTl!48D?!l`32bp`33oJS7b2mMVg`LF<-*Lf0J=POFd6b4PFo3 zi}hGO%P~e>KrTOi=S$wqZOx+DLuPFS<}61|fAu>ZTbf0&nzR&oDcPD-GI%&ekt>9> zk=8gxs7-fPBnUq|&BMlduTEf0VSbo_x?;&FaVH3eD()chEMV6?`cuosO`^cYZEx^@ z^W;b!DvO}|7`e`Uy8>PRC3r>90;By+fLg;=q<+SO$2sy*kGJCi)4fds&VmJosTG&by@!2P+rSuLk*25WxEI|CE4$E&+`h@km*-D42wDM`&0n(I z#AIFS$!RF)LFI)SPaQB(e2f;0SN>B=2odf3jx|CMt(e898@6wfCFwi8+BRE6y4-NX z<)ozBt3vBahokG#&i{R4aM*r+(&|OtV_I_EAJhKHFQWLfy;*og#cZ3Ce)2_*qT-ko zNnE6er}ZHkvtp)nv0{dEiHHmjhv1h>Q!Z|MrV>`5reP6*T4Gksa47(+LZb4DI&R3C ziAk8Ad;W8LF+3$*tGupNHrmHO7Rzuh7RzFpS!QR<*K=m#BR=Je4?(GT*DAQm=N?%P ztb(~9pk60^2QXLYWiOWq5+m1@XKnW5yval0nN+Vqh%{?ku&?z%;!3)RHA54Zmh#7y z$4{wO={*~qEFW=oSvVY|uK9C(d^H;Q3p9^`o)A4O>avJDh(Nwi$ApyK9cUATm>Lg% z$9gaBO-FzF9V;k+E@aV&YG(7+=UUm)g>0Ww@K7jW%@gM7HNI&lX#Lw+k0Y$67W!2D2RYg*9Bef7B4uiw5w{$tvxNo_O z@1YdKUV_=_ae(5+%Ckem?XTa_!bV}%Q{?c5y~pd}rb*xEFF#Q7sETUakn4<_PN$Lq zxao$fe>kNG`>0Y{Bc;DvU*T#sQ#MxMN?7l>MO+WyO^Bu1c!dDG2=0yv;YRBD5*drF z^uN4u^LSAC%~zZ{(H1cJYIi@y_ZUVG!vh4{wOY> zwlFvOwfZAawL#}XgDXXJH2UV*O&Vn{A`chE_2E!L2NN1Vg_>z*LQ5S$k?AEgJS~sZ z5km>8iCxtbi2V$4{_RuYy>6+HPE6XbGilxC$o%Hjz2C8kq0@%^y_!>Y@5AivEAiYs1nEY9N>k-wdMM!7r>My|A zPXPUs@@?|;Zy$ybP~U0zgYoUpP=5p9Dc(dDz=n_W2F=25@S-Png*X>Fqn(1OD$F0| zq+QAWWNipBEj>1-T1E)KK#}fLE2*rJU8qPZc951pm0VopB=x1w0TO#q!ZPJCvr>hR zUa?K>+f7accLm+7$X^%b>b<$P+T3O3oTXvI9MMYT+FYlWHqa2kCE@vT&{JYqipt&> z`G$`3B#4VF&`nQo)is|r08!Ev#6?SFxM)q2nl3;^4INpfEYZ}d2dyfa5^}vZX&jbr zV0T_OZgihnHhVhY(3BAQY>CytdI4ayd#7PHD|P)Y)J`N=gvK}_e>fE1nyrCpAn(AQ z>B-rU6K)Sw-o)M?>HWbdSUXugMU@AhuOcANbAs%+2GV-TyW_C1u?o2KSR zZ1sd$cF47=fOY44lU(BEVYN}&-DR6wfnRS?b~QZMUqN&Svfl1jtvI`x5MbEu{1iIv zGE3HHf!Z!|qpr#|y}QUMl$L2n&G?}H0qZ8ek1axYp&>#tbJlaJ<>qMqZi1&h*B9o8 zHjy27l8ap5>yPuh&}jG1FQ7E^COJBKBLwqokxT4Xp~BNmYV%W0m#NjB1dY^@RcPyS zzPcB?x4 z>-L%S9Tzg^#eB-0NAc{}XZEk$pZ2`c5L}bGC0()z+S#PXh=}3jeAUxuR)MkcWpiVc z#)Dle(v6dB_@1_Caho->3DLI~7RFZhjW`?@WoY4-Z--JJN;Ga!(~3pjceA>@X^d(D z6VPaQ@=r?C&v*4qa=os**IlMNJr ziDqAwBa^zAFU-1Sfud3n$DfyLaB(oN1MM zQA(fTVIOR2HtQ=#jC!hMm?>{2j+UVW3zm?S*faBrFuX-XeD4B6lcGFrx6+s>j zD3gUorPAF?HSw-lYn09lJmkzB-5;29HSDtW_rJzgUPH9c*0gVXkyRBG;=kTl8U$Qx z+B{S^H~-Lht_p24l3{$5Seo6V)fwm(@0#|rs?JZmjLvO%n*n;^GRv}j-*`@I{YW~x zQDG+c$$QU}6ijtB$nLH&ly7K;-ePldK?v)rrF^1e=?u+KhbOp!Uci>voHWb%G+7^A zmcUe2S&d9O`4$Voig?^J%C%meA@6Hba3oF5`J~h4>yU2ATr5E>IzpC0(GrrTu%*EN9!cq6d z%kNA7teeeJP3{pYtA<08Nb|bTSvpR{Eu5J~%bCiLnzeM! zSq)w(OSLIm5{FcUTTIUD{OS!sbB!L~PAPWxpD*2f*`bN!@eit`Nx1$z!QE@CQa!Ag zGJj3RFMLf#K5b08Y~}QdsVtjM_FbvzJ!T74%{|iHo+P8<_A&&mzIzg-dZv1!qkyx3 zGaa>o&md`eiv%F z*cAy@&xDW)IKzS=q>3BU++r>DE%mvR!JtHMibPCH3c9OIA$k`kx*!w~Su&sb|QdmuearDHvNz*5B#lz^AgRWV3W57LG7vBqp#K;?Ou3}(} zFHV^2swL-73Mi|=CyR^W2dO8^E%L@Md~R{Vee7#m>~CV+Lb5vmLXzT&Ju5lK2Xi&4 z`IwIGqF4Qxvr(-7mL7xnwYSD}eHnQ^%I|G98!K@rFS0jvBKB;VYtd-9OgwQ7$asWAIiffIx zVLRk<=wpF|qixt0#`o7A3&jiCKjZqR0{&X~KM3}Vaxr5i4=$-+ z+ZKu!NffBYr10;1psXSf{)*100_Ko~up8J-qi3%@gqT^r`!_uk=_i7Dp5aemsYX60 zJb&NA9q-gkJ>aW&=TXji*|(}LvPe=|?+35wVNuBC9FhYXEu4RIe*EVda6;TF>_P;a zRgtex;@_EZCzQ!0q+KrxaQu!{b`YXQ;>)XVpuTjUOruqmZ_XDKd0#CB=jY%~x8vp) zS)p6nk1#t}hN}dIgZvL0FAAI9zP8+$2Km%VeZb6Tg_*->i;K}8J*~{tSl!g6<4?QS zPaaWt`sdFrIGDWd?bSLydVl=vT=t9VxkU)G1$W0Egg_auxV4ObdX`Fj_B5_<&RHXl&Rg7 zF!$kjmG~+R?o)TDPhKn#Ku}5ce~0}~aahhs{=?`|n!|l)r!}wQOyqx6(LcrfGj%(p&i_gx|0-+&OC!|? zExfcU<2dBmIrA5SbEowwxTsh8jgm`&i{hm=#?VSrDlJC)@<^cR)r`59ZDk)PWK#RL z6ECJ(nAz>G|FnI0Vb%lt7xNsyqhFD~vOEF*&5YOH{xIWHXX+OJxGLpeCSe!lQuPt$ z-f@#tv5R6Ouzg;y_4ZJ@gKp-TqJTisj}rG&y9rDI{hJoYw+*ogn_G2%6jZPG*H5Yv zRWaq-ij_y1m>a4XMWd4+O2eJ>!_E6W3IlM^aQ_*_4JMs?-m=DN^QV|u^Q1+=Jn&fWVgir;(ZGaC8C-6=ctM9dp9c(qz+lLvBovy16V1W%d-xZ? z@mKmEh@2gayrv8dT)sXZeeVQd7Ilm;7faycTp}PNBEE!2^y60Gi;E>Nt2&d^03i`k zw5o}vtWXm4q2rI>F8RA)^{Ip z-1&Jd3L6Xm7LMXw&)YD|q}mMFn{Br3uq(a-jI-lUxVUJnl}G4R^Nl>+lcpPtiNK(0 zP%;c`M(h2_@t8tXGB0M#C8I6O)(3{|Sl_Yf6u822~6)GvGs$`EYiG*nhw8eJPDh_ok-8w z%I?3i|CG>$k;=1m5X&7T2>RqU#Ix-F}^c}4eFKc#UV<{5Rn|{T;0a9*^ zm%q2RW_eZ2Y68ei$Y|u^ULI2~DQlqer$i}*yyu(r;3+YCJ~zA0!{5s^3Ncuzw#jSR zt`!+yJTYdpckwL`7HhlgkB+HaRfcNt$b_wZ2{WM2s(fAG!Vxy24BEVrpYEb3Qr-U6 zi= zsAef0C0koWguMgyivxK=x%bmEraiv0jRH;wYj3Vy&no-XdnK!kWvI&%qa)WU#n-pS zGzeN^)E;uQ|0n{J&d4MU;gnsrTJf>&YaTMJ4i9MR7++m;BOTR8yrWorgAsKl>j5S* zkGOt>a#zmM+iVOTBjuVK`45(V#fyvT6-uT(6srK)9N;@fSgLq!I2pQf$TBUI0%Kno z1SA|qJaR9OzY&f}%Rlv|caX{+{@y&i7+n*(Xx7=&m2n3krHC?*U6S97cR@wGc5{*` z3c*aPsWngz3eM>P+#G<FJv*%wj zUe7$ZH?PkDEci(BGXjhe*?uJDX9O3O%f_0&Sz3#UM}*J+7ge#`#k zFwOlfjmu42cSS3M3{?f+GYdmg`&p|Ur%uw7POYv<(~LI0;vF4OfD^4@+{aeWh_pFs zQdeEjf|tzDlBju;+2&L4x)6z}81^yUv`|S0V4}LBzJC+U+FpxcCFrOz!VB_Ho zw#Q$r70z1a!@r4wsBMI*Ld$loa?{exjs;n@W;x zxRF^L4u~NFxDq)y9ZFMPy48BG+dD=j)2$CiqDJT=$`Gd-PA3Ft3iwD*dVrG~j|mTA zqBldlbsj8pl8#?lDN*Y}n&!Yh#wb?ksqgE`6Au=rUjj+B|y)9{r z^}|~K&QrcVOH#hk&H9XpNKUUP-4SC0iuoMpCOWuIa(Tq8SLsrD(2~1#NYW_C?tmq! z`A}yK>BuVNBJ;?*^hoR}(J(xC1Dau*@CMNFzlA!!_xw^sC({k$exDQxJ; z;2RE!=*nbw+%v0}652aU#QLz!PMQ=8hqvqz)0*WmgP!=A5k7nsJr!VdBbcWwA;a4A_hoXq<^}|7x2-n?lp3+1V?5RLz*F$XfR2V!V z)viGqRKWHXNUs-2Mq)C>txYKROvW86OsUe+Ou{#$gWgdP^1{2 z$`7fj+bck$Y&|roIJ6&tSr?Cqu2t2jM5;w`Ad#5)hMD1TOyj;Uj!jv@mM3p3$6BN# zg4Zm-W2emN9tY8k5e@_bQl^MIcoVQ2VZ-54F@e3UYxb4=HNdnRc`z| zqKZ5_jr$0-4p9-7MD_Ooh+;P6Dl};_efS-Z^R<`hN=YS5wl29*)}@^Kds#t?>bTB% zxNFl|>;pWN=jS7Nimb|_P!6!(7E}uJQWis>aLnCb94uOxCA0gES|E&?;C2u-$juMQjr4TPs63?-oKR{di9)&BvvmG679Zn?Y=y4 z+`?Z!yc1AaUE&7O5@soSXss)=oG@tYz)xALs?5P(_rY?(6Stn32dym9D>M{JGLcc1 zX~Whb^Q!baRyc=>ohU|~FyUYcQ(dPoT!PxJ5((Sm6NqFgnYkURgCZMgqjzRNGs!L_ zlx#x(4bVIij?*UHWNqtvlea0fe(UjS+@ycxx|)j$hEA|8K1XEg&Q@@GrVm%)>*-8!Ee{Cq1mg?5;4)8+0J_sB)u)O9HH_+Rcw5Jz84$JXHQ6!R z<_GhZYWGljNRKVj&NH24odV9m6T7t~0dpwnjeLC%5pY`jp#8+HlBR}}pvh^GO!$}y z=1R3MX(AmWi@5U%rR+{Jy)rrLg+8ZBsrEonk)|QiFj1t#{H@^Qd#ei){RyCakz~w6 zNkne$2YQ{s50DPD#D@FM%M?%Et1PIm+iASCUC)q1A~Y~f8tc^@hbY~-_EsK2d!PCY z)+`&){1i!99x^WZ{aH6@_h;Ub8%+Xo`uIpR&)|2g{Lj{-w1-?Vk#!Pd=5~5bPAtoX zi9lQaE>ZRS$;1(3_k}?2RE{<;zB1w(PNN6CA~>(6xmEGF>&E4QQf7P);|$9LbkN?S z$FNt@b{;bQ(#e^Vv=CyJV%F`F`m+9S_m&a9$5rXsAi@5RqaYgr7$2iXOrb7Mvdt0u zIaZcQB|r1+LP6>GM?vVfOwuWeloZv&-HomTqDq=bwb6_we9fy9NL4ZGi4jII50yUy zi5LoEgpu7tab7nvOc@V|n1*3q9t6;n%dCdoeiG1BCH>{s_k5ww2yH+~U*Ahk!7FvHL*vX##%(}h&#L4a4{ zlUSB!iWYMhfy7m-qkfMPQbseaSCzyy=2QY%8SfSMe~fcV_g6F|UdyqGxD>*X`>o;G zLe;pPF|KZi8^0Nm?SdnMW^#AxL#tP*2oFQB8%evSFx}KfI(s45fv~vhp)TbkV-%I4DFBG9SJ)09l2)pUSWS_eV& z)m?0DeclZPu_1t}QosU=Ep2hBbjv#%_)+%G%p zbBA!zXq#DzahD#FoV=_rp2+caz`V66tQJ_2h+a4Gq~@V2ndp#OPTDypSP~OeNSaE& zneA?m*5zLqQoSK!Z$f%m$*J8&-2wsv+3NNRB_WN{ceIDNOiED2IjbWMJosghj^bsBhT9yl#z_;$cm+F-WMSXuhf_Yby%W z++1+*AWdltwM0)Z!dAeuOS#hN-qCt33>N7n+FpCsYkZx((@r*<;kqqD-D>g^VxMY$ zTe&B+rk9p7st3mcW%nlmbfu+l1=G?)1aTZ;;*=wHyYs{*RU90`M7`8=M#=(!+(h<55Uj%o*AP9y32u%w zEuJ`Zm`C$hHRh33;W&fl#iv9L<1`604Xf6tw!Y|g3TQP)(Ti9QXK1o1KoLx6(Ozv9 z?gv*C%{3w^@pZ)ze6xn}E{^)&UI%fPP_cs#9Xfck+Bi=|Lzh@n-Ax1_4lMvNNkTO1 zI$4iY%vup^ant1uk5?0cA1$Nh7>y5#GI3__Zhg>yCi|+4r2I22G9pP&;yjo=&VGO` zgOsn!Oix*qa{=vJH5|&7n>5J-MKxOMKJ_~&OX$PYICi1rbjs30iHJhGeyY>1flzZU z%?^jTrcc@l&xO`;#!nHY8->;-G#l<8uLHtn-*b$RStzC~M(Q)~!QL`*p5}Y8dw?o6 z4=LKG?A`+qs$xufT~zr}j7*O7^Y=PJMASB7kWvLdmH7=2L#PEx(x|w%Yte6r)O{%w za7HcIO&LeX0drapD*dE|MD6SRq2CO^UHc<0)p!!w1}zAxm4 z%1wrxGVT(#p`@+A8v>w(q~g)dPzwa&To*r;)G5|yy7e#?7o2W(KgF+dW3KSnlq;zI zT?lKaxU)t1(7hoiSGhNF(nIe&S}GdB3q7T(-m}^DMX%JPa9@NZ+Mc#%7T9N~MGoRg ztz@$gH#YR?hxNZsbB%2<%Yu<4716*`Y&gu>^jai}042OOFP2L+*lgjI49Mf=O_x1B=*Fst>;lORDwX@ z29B)!@R-tqt*nn_xF0gBB;dn)n~-8ywsMn-$5V@3s1-%Jh=>SO0(sLjeP!68W0PF! zwYw*%?GugD@br3}T2k~ExIz}M7qNgDmD;^bw%KXx#8UWujdusvf_S{&9qAt_e9$|Y z3;hR$2dLLj!0|au{R2t-8sqgIky{bg0Tlh;0En+jY@fqaf1)G$zpALNUCRQ%FUUKkMdZS3SuZKjR0lAj#%faVc z!wZZ?vQn9Mc?pU{@7dfr-f)g7$FSD?zr+f=&{ecmVFmM6?Ek#d)j_S#mj(BS=l?RJ zstY6mB3j9teOmR`=NZ-hi&>d;VIY#d1jFxGJO7rYc$y1oR4Wdlq|N@3qGyU8guY_j zJ8@_IRbx||th7!0y2}h`6?ke?zRcXyX{C}uM%;<~j3{K;Q-1@~pxSlXRF?7}s;-Q4!ke5P`DEI8$E}k^ERG7VI0~1Z+yvP~9RgF2V^46KC&(mA9{ulkki$JNp zhXAQis(gn^hypLls&0>Rk#MHL8wI0SG;JL`ojtp=M$<*?2+xpa*Pms)w|brH8c?%v z-NV==WwTr>L0ODnY!GRiWwpd&?G@7)!wQCGrXl(6JzH`o29TtF(0hgSdGt_4UF!n3W5DOkiU4!SoZWG(0oYvqK1+h~iS4!> z-x}TN`x??VuLfu%&EqS~R&i=891J09eT@ZmYngJ?AG@mncvI82hpQS3&6sBA5a z-Ky^8?WUC!i2yZ7WG_`@KS)o112!xqc7lG#x+Cdy_DS?YH7JZ^64gIxW6aodLxoi* znMo(n8i|zYm8PZ?744uovgv5fX%e!zrLPWUbV2Z+!s%H&A#M=;-WiRtd$A%y*>3NZ zXP;JmeRU3SYy~1D<|E!UVwTZmp5Dsd^=(v}ui6d}xGx{d!9A_+wq6{6FDfg-QMja+ zdr{J1_yfwTkO5U7ttnBEKI^5jAJ(4VD7|Gd`|aKvJxyMiChkO`YLsn3R)`yiBdcP1 zKFCu`mvlhIVj*EEz#!7egAi4s;&2cSW&37A+>ypUJUl$|D(8K&Ujkjn?JMxAod8UJ zL8tTvDS6qR%+6ViGAn5}PYN>AaQBDsqP~P|r&v3oJfwShvIX%Zxjq)twcUsn;~sZl zFOKEnVQg)QXyH*-?gLA1vlR#-IT-1~&}}bH>{FVAz>L`^{YRQxaEcIdeu%hO1JKSG zQK&*y>WF?lVEXQ)Sm2xM`Oz$6#zUr0N+GJO>XGb2_A&EmAwX3&ACw_xL)A~n#ZeUX zr9|cgp^G4NC>}-R&dAnf0wC#=2TrJOGc-GO!N?d?axuD%t11?Tj49?1(uk*t8FVKQ zJPnI)^C%PYkJ(Uh1S9r*{8n%%5~YySrMe4=XqH51MiIx=Pu9KPu_EpzHE_oZ~Rwp)``!c*bXETiDg`)ENko79D1WHW%m*^j;J>Dj}0Mzg7mHE|U^8 zKO@kqe?;_n=8(Vc8i{*5$tZG^%`yqdTHd^#l#k9Tvya zn*qNtE%fL^4(d|OpWO4!uG(4DuN>RD&ZEicAd%ENY5~J&mxw>F=itmaKt+gom21@U zg!zO^pC%+{&0ar^eqt)KCv5HQtd3R#oCjwL+xslY197kx@OaL zJ4n@?;AqYqF;fQLPn_G%fiqNTtGPzWmjTBybh-_Wef`MqaI9bK8a(JOzis1NJ5dWjVxXARTh%;r$SP)~SD<77HzCA=SzId3-^^$;DB5 zIQGUmn$ZyIr$Ik*7~tDE@w`;7L@p#k5SFSCdwVrw7YTR>H^UWIjhC5m^-uYZC3+fv zkh8-Q@bVUm>18vUh&V=OZ%e%HqS||oE@xaPAO&lh}1euitv z98jYZzDhqALH5C`6-K$OvyVGwtgw4}{B_^=We+_Gria1&5M>WR`b=_Og7L&?#%xFS zE>l5g_}3s=>$noaPKi&px$z&)o^|M5T~j+(SnM5V`KgHhb7(K$e_ zT2(LThVGWXf-Bi^f&gbuK@ji49Q;;S0XmPM&-gu(@r~f48iG&0#>8s@dDx~*t=E|< zx|EI+p$*+t7C%&|y^h{O()xvnwcG{}n4u^%!^ zh0@T>n7LE2=Z=M`dW*>PDPDs(%Ao`Jp% zX6dnd!%x1Zg3QmZ9}`wef5)=QBkvNdqF`Rzhoji0^OlKnYJ(S!woaE%Y9a(MH&V`u zUzS;7z4G{e{XNm`n$ose-#&5GhvcWYz5~oKN`L3Q5ryk-P6B<4rFsx&!7b6b3J2k* zL2Ihhfa@Kqjy23(HD`(s@5Ghhx)x1-4tmYJ#bT-{tBPybRJj=ts;aI?9#i*J?efFv-oGZc^w;?HP%qg!hwKIjw`rlYS-N8nd7m zgB{0`Y6hqA=T^M4+uJy2(jJ7ZH;(0~J89H3vY~~sq!o^KAJTkpH@$6Z`jB?fMi|il zuhIUQ(JcR`jYe!Q#Vgjc#kR!Ntg-)Znd$oK|JJSlvAX4*a4~4_+9r;3hg=m!Xn&5w zR7>zNyMk{LX4JhxHg^wkc_)7pPGa7kJ?J@dd;TvsIE(2UY=Ghm#xdFMPT-n2i*bec6h_(OrX$gh{~JoAlvFMPUc&7>y^QRCC#CkS zW;ayFH7Ze#%(tsEcI<=*wZ2*Ny0@HnWKaJCc?%qPKSOVIams(vgFhF{MTpe?45q~y zbeTCQ=J8TQ6=}YaNwpqCYz6A*3sKO3jnM? z0r4l3^th=w9k=0|L}2L-u7objv3141SJMty=*s zp%l+I*-w#?eH9vxcAur3K%0-Z*Y+=p*hID%_S~3U1d>s5fHYA>Y~n61>EYLOLtJh+ zhyyn)3@UO_$SGy5%)ZPF8?`~zhL(DYx6>tgD*5^Y;#uz+KE`97+Q?@g*LBa;XZBO# z`AUx8!`e2pSQ>VkAyIb#RkBx(tA^;OsN zcZpT7CxdHw`rkEQg|_Otfng1yCXVyel9zeC22yTuJ>vkaIdG`t%OIVk&g3Qz!zNSJ zBI`4#%Yktnc`BRfrh8H%y-zofvk%@SX(zFAfa_EC3nnccC8@=WlOBOD%(lt$gd3T^ zDh+eK(F0ptoF^247ba(hmSZv`_Z5f3p##z%sqHbbDWIUZnIXv=r+K6O2~$_ZY_>c1 z^+Zj>^J|Gux5$~YdD;YxYWA2>d2pCQm$rx?rW(i$j`hUNf$4h|lXTz$@pT=-RpZLQfxqA}yOcds<} z=2)2MK;+&^<3t==AgwufEHytnnR^6{h>)?2X?F+_A5Qg!@9#K%7lhxOKk(76U$6S+X3R*?@xh3Z;QTL~KM)8! zoO^)!6)%h#UMQiHTKxejQoG>cK;q z{zdSsBnWe8gcxpsl+51}>^~)jm!(MTSsr?}Ma_rDuu~Uvk&LK5FyZ}3t3Y0Rw5b&8 zsjddvOH@o%D9CA^0&;Cp3Lyl|Zfp4Ico|P%?tK|P+Tqw0T1C^)gfFnpW_VUgLIM%# z1K@&I?`6>+>(3;{2ZN*J!5+olN+W?z!pEOWUtfDa6K3E~w0u@T6UIXVV?YV3StaxW zqLlstC>Wu2pR!UQr?%5)f&m32YYZ04o6N6Ioqc+%A5d~C##q9!C8eF){@Z?3)^+@M zItSr@+^C^m4zdZGJ}QDy=n54P;X9-HlS<5aX2yX(HZFZc>Gz598p@+jb%ib#6RS&x zX)7T_9BLzqYP=0=QjIEBszy%HMdjr`*Vg}j+V(+wN~4K{%D1@vOu7>^L>3{rS7RK7 zXTdExut+$fL&04CN~+(@X{m|3IoSgwSY4H47UoblZI z6v7h9646e~j&SI~gV|(`S~gL4_fD_Us;MI`A$FSf@&tI4pJlfsO*$~HI-;>I{IfDV zthv>9u&d?$=CdzCy-!HWUw@XA4iCBclCJK$c2p(y-iVQn-Lstg88t@b2E4RU-yHH( zg3C<2tv9H7`);l-`$T3RkzxvV@-I6o9nCg&1ka_WZvr-+HKd*S>${RaaKvnR;2vR? zmD^Q*L^deEp#g(Lj5ssqamjCHO?p(`RvOKR!xyf3(iU*OJSwhWlz5S?Y(LO=bDLL1 zv*sPtM8r8>I%*>Ep3k&FOy=Fu{CeyEX67%!|6M|>t9GIKn8#BztI_Dsp`}4c&^=0n zM8S7m*X~_5jxXvp#QJ1gE>ibeTXK>*v*%7G3RLV49kC5CzFlxX$@wt06WTN=ajLSy zz3}#m|8FRggvLa%5@pTx6rjfb@V*|aWo*Ca6#<@>rEy)vV1aNfIjYJ2uF@9&Jj3{z z`hzerIpk8_|NF=wRI!g+$}WlhLyk5jrt1)w@xHtNfuo!k)0edLFF3i&Ez!N_Z|TBB znRFlg$q8y0wl?FPAiT9&HYuEUjM2(gtnfc|F3kyBd_tKp3_EDf{|gR2`ifmcxexE= zEfkkz9OT8_g%oe2DM#v=W{t=0SQWh}XU0Bb!bw_P`pJj%DKx-$$PX(aU=m6vl z9gUk$0>SU^?nEbP7QeLfZ&9LxX=yAps<$Xs+_ysR$dS302bMoCQKH=42sfXib3+Q2fvm+|)2VQJyFA zl&4=_MV#O=**Z-mk@93D!Ua(#wVZq;N>UiD#gmjg(mOnzOqJqb=9xTW9xp%rGQ!B0 zl48tTH`?oSJTp;N=cw>WR(9OQb3PM!7u&C7Un<{ zrr|sCE_&AS;~W9?$Z^QV2Y`3&uPx+%RY+?HBPEj5>)RmLX~1*+A4fDcK4=H})fr$8 zF>qyW5sj<~X`%7Xv&HNMJb;j6;^-tj)BOCmwDjD?Ey~J+KmjP!J?b~4WL~yg_!R6F z6d8T{(Wx&s8jOtkfcPE5UxxaxAc;GgT7UM=e>iG%(O>5-?N$w6oK_EN#_VhR&;0)Q zdT!am-%cON|L6+T%(-(~pZiay^#Nk!$J|eYj#mU3U2>6VsMvsw$Q-LhT4am~82E5T z+FGAx)SLX`P)pu|F--9ZnDN>x7``3=u2LS8B@i})NDLjHuN84^y<(uEMX%)8KhU-5 zY|MDWjI@l6>vCR$we>1o;=j(SFdo`zvBdP(5dlEQ!!L?%NuEJ%eyb;nA3!Iw zmkHn16nD3N-{`2s6 z<41nAWAZDgOYjxuSEqQvx6VR51OGa9VGmC-oiK>LvTi;++55Rugn<%6m@tVILyvI8 zT?$N*z5vMuOGbK7kzkX_?eVoput=qC_S3iXS4eRK9%61%yFz+tEZ`Gjd z^5c(|7bDcJ9K80GCx7e-s4+j>t#%oSO8%KsO15LkQ?FyOJLsf zi5mG;ojboi0(}eM8$NfW4V#FBD9eSjeEo8AMYeqQNyWQJ}!S z@zld}SIh$(-xZ<<2h?)eMIFi9~NVYGV;{u&z9C0SAq-(^4C2 zb)Cvz*A}y%$gK~a)Szb*l`du<{B$N}KfW9t=^11y<2uIAm!0UYADq;5aPQFa8U!sC zQvR&t4gvbhr~PFXA+oE@LP6MFO6@7Gb?hHZZ`|N6#=(9pjuZI!4t7SEaFA?B1W>JJ z;<*mIMTY>N9@~r_kFkzmvILUMg)Y5Z=x->1UCmg0mx1l2cIE1ea57WT7dEkkiF$>c z;8D;UC=K}|t<~;uva2^-6HZM(WQb8|;j2aUBpVw0yG`Ui55WS_0+rSH4*D(&B_w=8 zV6hqzDtyYK*WssUkOp#3GtdfKUT&6l0|3d@Pz2 z`Oht6SqLa5^3`$Rl_F&!EEQ6hI5ilBtcI|phpCQ>yQTy8km@1;&|`et`V6A{44&V8 zM5qj++zSlG{pI_W`V-IdH~e3E9x0smtLR6WAEKYj$QX=Q>}109phy(G0=uNc@#U#j3kXsvXGa=$vtC9ulN3sSV#CG_L4 zSX7T1ls{jwr5-0Tcw1k7AGZ|Evb5{id5JiFwT5Ev{WFm04Dr=4279ClC7wPMuRNxy zTCZ1skw^oJlf*Mw$68(tSI-WgLG$rF8GPyIR@L};Is_DK47gZLabyix*gXlXEkgt@ zDGsWw31PuRi&k$o2a9b(gJJcy38v+VHc?AWXxyMnLZ8jvce7NKOgj7a@U-ti1-MSD%6JTcCsU%cplQ^G%pO_K;MRv0oK*PV;kUnieSnT4e% zi9bOVdV{R`h+Q{Inv(fBMVm)|EO!EDLJdpGBEuHWrILL8>or(HuAwx_G1jh8W(gYX zpOf+I_<%KPJ83$-DYSHI2AW9M+-j{auIzk|u~@Itu@KDP)!~d_QNV{rDPiYks|k|M zkU6jeR5*zgDrp!50>1DNQ)oWct7XLwg6A<{nFQiCtxVG}P3Z7t)Cds7yv9;s;R)2t zU~t4CPWW2e4W-9sp6wma~L%OIqne1t{F{HPcA$kdeb| ztpQU!S+{P^4(RhSXt7{K^TBl>vh;z3#YSvaKE!M7w@J*Os1XNYDEQS$use~k zK6dT38><^mbG(X76!h4?S5I0e9-G*{FQr#3WJ@b)OB0(U*vEUEDw}5_n{ncYJC(s| z|63w|CN&pu4TpsFl1`XO8aaV|kT}ufQ@m+Hwb$gh*f{+W^64t6IuGKC@dNQUA4kCj z5&~&)AfZ=QUNgFK=ST6V1uLon7=xOpl_CPw$Zft-5cK1GK7Kqh5!FA5dn;BCo0=Yn zsL(M=mT+WM=Q~!w2X8!{-et122Xc#6FSxNDhq9T@gZvNz z0Ma#=Ics#mogjFmx|epfwRiBP;*!ae<6efVF;`#X)br@m@ZC5stl_0&9jMMwp}wQ( zYV4^Ce77%wm)u7anRZ7d=9*$?kePz*kq`?&6GXvVL*ACa^xT++aJr2j^0iTZ4oQ*PkKfIUiRhD4lYJXp`hf-E{W`eHd6x ze9jf6`i*FnLdKENt%rrE1VVDhassE8#`aP$|Ga&hCcQ)cu9#V2bp6DG@I>V}YDEWy zlnj9>1;>?JtS;ZAvM-m#`@AabC#!dm^q|y~4DR__u#!(MU+a+28&} z6Y9Ep9B4Qe!|&Dly(lvEdJxtn9Zhb6px^1dm%zi$n~{3X3KzC!Q3=FlDkuJ?>LV`Z(51q$L_?%5#s2Czaa5aW1@a0CbGEU>qT-;B;SeRC++Qs_T-YFR%o>dW_z zGyU*uFs?);Kr&Sx&`;G1hF2N6<4#hm$=}1BdF(x6-_koO;O3E$2y@f~SBMt^NEKgv zB={&z@fcQ{4VN}Z?YfQ+f>Th9S_X;&v-KxZ0F;keDX^8Of|QQ&f*R=W^|075GTaov zC4OB@EGMl+!vYj#X{lOKTFm+;2aD-&D)nS2(nJZc2Tgm5N=2P;AeE1w}$pwyGn zvegI=G&@cB@h*A2d>113B@@=;E!=tg^~LRX*AXWpq8m?#NDC)2C+wue#&Xsf)-4sR z4(K;x&K+X$^%z8OhOey|!kK}f8u~jH{av_t9zpW*36DWRe0*3)SFC3(Z>ekvS!Jy6 zSaIA;eJsZNWR)k9n<))s5MGF##eGibS(J~P5LqoC^xoEYm) zoyhfj>xs{c!nMhAm~TpZi|Jui<4i8s7HC!xf;t0F2a6tqPuuDLKi1v?ppKnu8{N25 zTnj}uQrz9$-Dz=(yHmj(iaW*K-J!Tc(NY{*tQ07PB85WlZ0hGb@Auw&|9?+*CRusb zimha3GD${ny&1=VBEmO4>TdeK6#dC^fRO1GidFwSNH8IqlHfsdN5R8pkR}6(<%-ce zU{ynr$m+U;(vkiFNy)N2YLs!Bnj;KhyZ=IU>BfeQj%5U#t~jbX+G0kyTA*+KGm46H zCJ-!$D9IGDjckCdw#%QAI?7Zh=n)QxmJKGK%9#?>RwCmEjSTzPWF3*jmkb^hl!+SO z9tMlmfDOu4K_m?@)>e&=^oN#3!wh{6r}EgBMg=P-NQR2^WhxX3mxv=%!zi|KUc5`$ zi|cz2W`u0MRxjEScHo`FD|~q-8hB5q#|9H!**;#nNk%rnJ_ZGTg*x3n9_e5jhQR&( zJ*E{h@(}yj+JkA-;1%)_hiNPB_Z;b;qvGx3WnaqXE#=P~Ok173=eTlBd)lffv_eiw z(kd-7{pJ<@Wv2A0K-CBT4-abK2ZliwSYVl03JE{^s!fHr62M-a5MLNJPubilHV?#k zOA#aNTIv502ApDea@cPJ$l*Lm`lkRllmT%vJhjsw_^XTW+zJ~3b3PKPk~n`Eks|m? z*48e;?dV5!_xx*6FO(Wat*KIlQL%^%${%J!;hx-~C{ZhrOQeHp;vGq()A;VuC=FHJ zp#rEs{H_k|S=>K88YLgpbkD`* zCU^O1e>3nk?sL3%hm3>Ew`3=Kp^Xnw>+Ceuu%lg*F0lwXn=t(@pw|RC!Q$z(juvSs zB7Vq{RV<)~+F-Kh`7cZ6_=+_40wxee2>fVmLZ4dlP*XG+GjdFP@xkLTV0?KJIyJTL9gY#`ip+OtcDybq9@Zai zETmkPZnEmt8KRB}JfZYUEm`&AYWLLQ(0yrBjeGO_k{EZ9KCdnj@j$s~r5HbYvJHqWkDDmt!Q#vt8l7KD2$?A{T@)4hxG&l&yj7LhB-4 zx&HC)Y9I*FOeQCQ++U;&gAL*Cy5vR=Ox;mE3kGdiGvnkFXe{4PQI3zK;7N@^+N_^9 z^rxZ55Oax#kO(>W#p3Xb^8?6qpBlM^4N%-ak5Iuus$wJb^9`<}k(jM@;S}81lljoe zKiJDGQP!)6yi2z08BU8Q9o`O&2_0!odWILI`OueK){?=J&9CRa>@+cFu*$j!Pdd)2 z=0gQRGO|9LHk~fV(nY>7qWBD0VxbeK>+!*JXkwuwqk!FMt)=)!Ag0i3|g2Vs!~x6= zhpgH`BJkrG!7vyD>MVG+@FWyaTKN`rXH2M;C{)N-nO0zNc7nS!W4#`SzR)U}?jlBIO(Cs|G{VlOQ`YnJaztGXRl5iK#>Q&BVOQf>tTQm6@9Qgq z$AF6+|*`OFHEWA8wFBTePU$7>zdROug~}uY-jY{0-D#!B470foHdH396D^-1S@^ zMNmIw#0b{@5K|U+hnl|-9clM^Dck^XeI56ZIpuu70GRR=UtCZvq*4+xi4PD2l!Dhy1dK7 z8_c>-!Gfn44F|syc1L8juD5HXNuW7V+JZQPx!0}(BOB>S@|g!iyid7ZDs_L>Y}|Xb z*LO7xm_1%i7CDlv@vORh1HI7g&b*8s{Bq_PQ;(}l&Tw^toVRq$4(e^_OB-4yp`s~2jO-2vPwMD-KC%3(P#FM{^&dYL5Pp`nZu_n8U$#? z`kg^3#%Lim2_uH|d&p?7ut@znEJFMe?Y*4no=Z$pS9hp@h`RQ6V#MUW={nT>Xo^RGR*^Z0bc4b^2C4%LlQpF zofwP{4N7-^7Dzws-b$!I>Fj`bHMqymwI6+#w9()&dYOQFDttODl-)BGS05~uySXVtNGvp(q5IW}H#LoB z>92jnlnZ)&U#o&bBFo(`x`b$#VcjnV&a=K=PN1sDB03c=gH&SBH!*P8;0U;^#n&tL z74mR#oAX7L_LaegzYb;a_k+5(1=a;8*EcpPp8&N(`k^+iuts}M!o&L3hLF~aFi|!Q z7>Q3#I5T)$F9UJ(X-hDqF(fuCiaJf6jw@?b`?}+J=go59HjCbGZJY0`&X8*3nPhyu zm5lO9Wk!)jE2fnj&43&Pf=EiP zwB9z!Xt{pvTUj|rFr%B!atS;oYE)oocRrT5Zm+Uhy&_x;KbKhLo99i4v}&djhlY&i ziSW>+mubtj%Y0xw*(wg2dm)z~WK{q7ubGG}1}UNx5N5`4FLwI_t6%lRi79a24>ggNmh5V_?ha7Ok(iyb0i8Rff6KaB{rwgbzn-k z`#;*^&L!kDQrP==FXu&xdAt_M$=lBD(7!nD=n>bxLBmg2sLNz;#I;QJ2_`F%G`O6 z_?$so9I98N!FV+m%8m#xc>r_TKYEk72Ni5ewfsUHc9JbQw}Wb9PA|k3hffDhx!${! z9%RN7r-y8?E;26PHHp)JDx#uYQcf7sYEAmS_;HTdPR(<>ag8`Dq&Db@Xa=MOK2$oDI-W`rmsi}l9T z^ZXL3=T^!oaaj=FOdUKiUr-C65rINR#??j$?*aSzu7v6(`Nx^s2PQp|?Wk7Q-bGqc z`wZxkM4OoVVaH^`>&gp7H{|#S4%EgRWF1b3Oa+6;c)Oz~SJ~@`QJ{g=j)2a5csneFJf3 z5Vm|uG4krwAV>qr(dq1DqYVe>7F)`a_f_{t=0i=Y!3O(d<=BkNphcn(S_#Ess03w? z-Ip||cg1ncu=j4g{a}H z0C?0CN26}~bx=x|Pi9@f-B4YXa?!3H!Vv|}T8ZWONThBg3Y$F~1gS1-=+Ac}im9oM z)wl$!@uuJMxuC-9jqycl6y%$QiiajJ7c(d#&C<(3jnEPh4Z{!xV^?=e$agA=>)D1~ zLfa}}a2+$bxkL|LZl@kEGuJZc)R^=~;4$bBe0L$1w6aP@;K4bL;e-Ly?}3ysAK%`B zYK+2JOGAn?EpJ|deJs+1>&EXbY)9{(NS}G-T?tHGLjpMdUj|T}KP!|p`@8`r2t<}* z;gQRNt+))vs4oD4gdJH0+r5^f#ui(+};`ch2*aR^P|VQQk}pv2JcvMgNxfdQt70eNr{uk zpuCayA_--7M?tcb!*xTidWEj`91FM01ghtb|6(9hPpjVzPukp;R4gi=A3`v0Xt+Pu z*fJ#H)&DW!OM6tyiV_M_?1~bEVZ~{oy2~HXk^I772t~tjwlLN)8P?~Z5a|$`)6)+% z0>LFTqy%=yeJd$s3b#*{Sa6wDn>xQ9DkZ`@3{`(VF-n9Xc3%}Me~sAqIhJy={Gu}N z>zQ{7{2IG}N_E#AnlEAS*hLhvw}aCU=(>0Q9MhIJ&UE}eYE4Sb1DEsgLzC!55Thapf39EqJBD*DO;^VL<3!#$26N`sbn)AyMVw)U?a+kZ&d)7vA)DVS zr%wE_2QEgB2J({yjDXwMaKPI*D1lBn$M0|DP!VEJU*!XCxV%FOc&s~I&w)2}KVI0W ziY+*+-}0+xJy$1GCkF7+0HjOGDh?dxwTn8V< zzbzMbo{wb~W7!i?_YSB?!q$RKGS1}tU_jp7*NERk0kz2`mw$MrK&P0$tCS!7y|yv^ z1*K2%z%mkuw9StgzUu-oiVz94m-}6&(RtH7ATK?GjKZWimAQLCnCx(`g0oH5$*Axq zYDFu=@{##0^q_DbbmdWXPdW|tbgL0d4D%e3IfZw@kIj}OpnNG2lB9*D!|l%|kuMa% zsOJ?`X;8J!gQZ7=mqo;iQ6XYS1K? zR+_`#()4bnxqmSseXXQ8QR$9e^t~e>R?fW1lFaznE94U2It!?qx_rYZMmG6|gs}e$ z{aB}6uQz;IK<6ZQqhlxZ`0^>9OB?E!38fv?o5z9-R-PrSEk+T*z@m|Zd$iLj>AO9_ z!P&>r#pm}_4~6oUpGr4heka2;Uk2bXSg(^sWIo4yqZt?wn{KhWBuu^Vh3nOD<1@v& zW4o-_nFROzf6(G>zy1@bYH{zK><^ZI3AW1uh#|I@k)n1E7tuTSCWJG5cy64&zWXwG z^3$vG`EPpQ*Brh9zPwuml9&9$`+dD9|H^pd*vM`Aj}Xiu!!w`)3Z6rGiE&EDG@}X) zU*cducd-}|JstW8k5TOgqueZ*U6n1TZowIllT&!#MZA;1_aj2(_m1B#hHn2E88lUj zxeHH=^VVoG(}Nb1ZoQ^qlUsLp9n462qpv9^2CyBHeRX>L1O3NsVZ~4NxSBJj| zPm$*hk}&=fV zYK=*k==W=@q+)D1gHE+IYFfh2%WaJ@!5yC;7g#tqpD93D6nhqX=mINxwHvYf^SveB z{2J?Rv?HWB^YIzJ9q)Cx9q&DG1KQtL1}LOo7p-*LMKx#2tAH(fT;B+7FaQ&H~IcoVi+ z6q70Dg|tcY^Az|El#h)R*`o(Nx?GM5MbEDO4TSY;5c78@_4|?&hDqUq1>K!k?3?NB z^?s))^RJU!-JjkZ!xaV^b&ZpmSIUCeb?um{PW~;>|C1>*T*_!_X|kX@sCkUfY;3lKPeDrLVf?~ zKS*gRTk_I4d8uDp;AeJTR!L?YLi&$pNI}c{05f7xVW2?Jx7%G~^goX?;%1$c)vf@i z$>Rf?X4K0L$-?@AKp+`dUntlZ5D1N$@5R$>ELpGzAtN*r2oVTEf&z)a0_UGUGG8dN z7^t5<5QzBvyBii-luQl;h86|IA%Wv$e;G1gKnm0^?;j2jC`J?<14#P=9|MBDe^e2l5XrQGD#!dtT0y_e6BIW}YMpivw$AP*qkzC4T`G3^+khXKwFg z!LPGHP*9K{)P2B$hGdD%9<)u1z6&7=WkXXD4b$cVPAuie@4<2Uc4mMZC<@SR9-#pT z2tE!(P>z}iaUy|0s!?2vB3QEEnCd5$D<%5sMsc!aKawg7_J?Gg8cUYCQ=0h)5p|-V zY1$X+Y{*!vy$k+$M&bwlHeWyS;NN)B&jvd$)lImTM41ZH?D7}?8(y#QNMVHtNg&RC zzH!N}ap@=iwhI0sKL#K|>pvw*8eiFrLqUV!7mYw597xtsAr>aYg7yV`cdj5#G7v}v zA_!7RHFnex`63Yd&);qNK-41}D*Fcw4S?SoDA_q%U_09FbgRmEJFe9JjC4h=IQ;q< z53_TM{Arm`pE#2)k*iVOX0r~NpZStS7uNgzM{Z$o#%;)wan_;1x6~< z>Lcqqe7SHof5d=fw?<1gB4o0;v@e)iq`1%O^bO?vis*w4Hw>bYr7kT=nOLODzC2~& z&lnITKc~soXa2!(ev9XdM>O%oKox9*;8A|#p(L$=EU*3(Z_3^K?|4Mfzwni;PPg)Z z;z|A~A4%~RZ{W-}RFMd?wkc@Zsnt$2`k_8A)JP!)4HHwXytm)G?xWd{3?qI>1+qe+ zSDc2+>#T_|!s)e~`!$0ASaK!KrK*KI)y)1@5jO51i8cQR14w#vMlnBK2RfV~N!9Fp zg!$!~UFS1!925lwG>G@hX<;0Qx>^>L`V%j5eZE4&wFJcXTc>@Are(oNvfx-dx)Vsv z0)doCK(PRBeMUQFtpl!KI(MtnzPJDgXaj%fG%6Si=of%CBO==a(K(4f+rYoz;ii`} z9szjYE=33)BA*5D#>oSc!TyQ=L%t*|#;tsRnOpfwcuRJ<5I%E=hQB%ZpLmEIh(WTG zShA~6&4`o5fp{e$#9(nGaGVvOos-?D5YA~?@X{^#7B35Kl%)jv&TpUlcv`G8-2slXgZP)eQvq8cKTiHAc??3FmIcS(f^YGRD z=6M&=vfv>W%nu7L5*8ccWcj0OMY6kDwBlOw1v*7Y&~0j zE>LfntmLAqPBlju#=wMKme{FJA}SkXMvk|Wb^(|$h8tpuR-#2i8nVtFKmRyE0nqpv8EIy80?O;S%r(}`1R$*x-d+5=ogz~A9 z3-=hXZ7qE%bA;{S-HaS#$HXu?u!tOddU%k}F_rJGtDhC8z;BMk0?`SMXg{@rUfJ<3=3(`Z&W^+36;$QgZ`~ zYRWm6!SOXQ9C0ft9T(XnQVVHi4SiIPoViv!3+IqVCNbtV6^hK;vxFscYpkgk62MfU z&NCrQw)Hr>d-S4IAa)#8nV|_bR0+i4U~{Z>;6WqAWxJ$-ciL$tk3>D2a1WF zAuuK6zPL4bqeNYQzuGNsAprlh)%ZymZG0Kl=k;KVn-^5BGH*qn@tfc{hS;*hA0$=D zQ_oau67yh5e`JJ>>qfRo60*5l0%mT=T@@ zIM_K(a6e$-fvx=pa%g9(cVDj0@Jizx%Eq!N^)leHP02`QK^J2mvk#Hrzkz*Jub1hy|{dGG=$cs z@;VN#$s?97rSY3SKWBE>jWsyCiTLmhq}XuHHUD|ZaK_FDaNe7_iD66m20{m_c@FyO zj!N@17^Z%<|6&@@+Tri&3(mw+CcJe5TG$IoN^ryQ-Zv2Ib%u?miBhcg-F{_NUV{jy zJ+AwXj;HDgX*9ETWgU3UEqhdc3`j=uP#t!QYR|%BWioE`^_=x;FyFV6Gc(BSEDQ1| zm(#nD-t}tjZ4xr6q1(<&$iIqObPYEjvRC=U6l+SprDo?6E;C4d5VFwwCjIHP)2o!+ z`h%ddxY0@*wFBV{*PNK+oXW^S^kK8b@J$9-l4tBVmqYqnb#zFZpM7U8`CX;%h=woLB`Z^AD>Bc(&)^} zH;j1qGzppF@+MIgDT?yk~}_K zDb4bu?EyX&Ca0$rgAKi7_B-pz7TOP7wsoZKUMyuft$$3)N%LEKNWPFvzR`BznsP?X zuCd8LF9lnjS-!T=|hTC9Op=D1A%j=2J#XoYadpoFlP3qNk>ODVzf5+~%ckw|;2YtQ6}v%l$3*&WVy zUHzoZS^RJXN;Q30S}M;&!eDsUf+8a0^c?hz@g zYK>4-e123rY0G=3ebVlfV>S0goT`j}tN3uzo*myqrQ}udRMI%kS!#|ZBK9%r`+2Xq|=Wvj&!C4Hi!tNEXosOb+*Oc~6PhmdzLkD4V@AyDyj!4CbDQ ztt;(}25ZLJbbBn1Cy5s-`5$tauarh*ZtZ8v4L&}$-;F}gYdfNUKbezyU{LI8HYs=+ z-clj9XoO4Hp5SWD?JBvS8R9gSvrXb1?cC24d>X@V)I7T(R1~xOzIb+bWJLfyb?}Mm z?){JA#$$)=N*A1*b+ykBoF?_6nDW{!$15!yPsI=2h?UHZ4vFx;ftYLbYJ>hm&1C;u zX2?BAi>jZY_{}#MVl-p79B(9==^Z^hXSa)aAD`V8Md)FjXVLPQCaz}nYkDiCc=6#^ z_Y1mVvx67BcIb@hW>_$lSjE^ zlaJ4ulaH-Q8K%%_4aXb#@#Gs9{wnNFG)3F}EWc$}ygd2hk3{S?smp>YS1+VyQg!%) zgMgd9eXa7mnQXCX@wo_lK-13CMn$VPg-%3O-*-svh^qs7_HokoFFiwPJ{1h{1N5v| znky^;aoZ5~@JX||E?zsn+t`wsr7K=9yh>x~y20>frN>L^!_dh+{+H05FOUf{=k`LR z6k!U_e^igw|H#qm&m3`d;!iGjx7k(jMG-W%dXmI$^S_N2RrV%+ZsGRUqr0TyMf?%} zbN^W@H#~MCc2gizs>|PNC=I9~>Q%&T4tmT616JOgEpI!S$mhylIH4Yrvump1ZPn^e zrM}?xSUJYz@E>V=sQA6&ZLM<6aY)Who|W}aDy!l>H!38q??S;}L0jvGjytaD9z>3K{-oVF7FyvGAe$=Q`t2FiqoqWc|r{uBK%*&%@s z71vZLmA+Qi+uF6gLkS)f*xozbLt=Wy{TK^eS^Hs>b_-wKW7Zk_WgIcF=dt!v^ikf{ zwpM>}<%|_CI4XDj=Gy0~Zro3$K9FL%e>8QvO@g^odYNJ=65fUOt5F=8R>#hj4WPu# zB}X^(RT*E=wmr4xZEVUFJmoxWUDo9|8fVbX9}4~TkT*`*~(siUCy}; zziV-!!Aqz7pp8Y`o4>(Xe+>-AQm)-Ps;CICYtP-Yzl@Tw-<;k5VtGm{ zU4s!I*7ciimc!YD8`0E9_?yFRSd= zk@ww~e)r1d^FI!@AasVeujXFO1OK_vn!$jXl_t^B&arD$;7|bmcmF;r!>*kEA;Y-? zX?%CT>5j8E%JG#Bt&Jae#yByoep+D)FaGr!;>Nv$XNgA~iDJN`hTr$c4h+b%3z-Py z;p3J9#RA?E6cZ!B=#o=wmTrLlNe3P{AV_k*zbL-3&&0ohNFs@)hA`h(O?SeV0$-K? zaclhMj3sZ6`Ibh4U{%b-(TTZW7UZV$?tP(3voMZ ze@?jtX5K31SqU}#o^`9SfDC0`(~M=UwxG9#+a3Ayneg`EFt(guQIt)VbOLiFi0#+o z;;kcV+$CjEYiWf#BCfjNeN8EATG|Ht(hE4+w1u%nQpKFjKP?$|Ti$f>;d+7p&9bRJ z=L(6p^!i_{ION%-uy%p;-r@gb4NWPAuzPxOhs6JF#d5i0pWpi(65F{rzW-+B+2C)| zKR9l^v78F~F8@Oq5QINU6R#}AJgWQrbuF~q+-P!x=JO>jT0h}E;hAJHo%p^?@F$uu z0ok{TbHClnSf2m8r{3)f_sHCjDE^4;2k4)25Vi&A z7hf~o&rjx{RcfLq7jxcX!|}GxYIBcPX`bqP_R_Q^9dEF>&NIG~6grVR|hXrd*nF$^Ip5;g~{D z@=`-l8ApI>zY|^U%n)9>_JU0?xjS~h6E)iW$m}t50Oi6HLekOop7f z|H0#TMpd$0<6390@VAV|4^wHE%KwY_KjZ|&bR>7>=KhNL&!FGQg3-GBn0YKOc_@z( zPxbLQbK@`#Y1%cjXNUoYC@`W4iy{5J;|r+w4itr>glS^Myd{gmI^Fh%U-x*Qgh#7B zOKumhdfTQb>Qr#_eB}Ch^00LAOZipu`A@V!llyPPV*OoNhcf2ZhcO>65=aWOo2M_N z&7Tb<8Xtq_i=c6cEF6Z1&cE<7*8Ph?`#0kM84@cv{^hH;i{ix(PEo=~FiWJZmi(Ue zd-X_Ab>)0={51k-H?x;DWZ?ROKCCyWKlX(Pp4gkW@J*zjw)?O!N(O27y8{X zhi^?VQcg-O<}ea-)jw@4pHTP=7l%>dmh_d*-2*q}L=Ks_mgrcMYe=82jLKMJzoA$& ztiO?Bo+Scu&7^`^sM2y#M~)1N0FWF--RH4KC-rF*%tNntm9uI~k~! zJ`*W{<X^5zJztnKsDyx6h;mm)#ecswmj)XC^GZ`b9_>85f7Ht^1I;vX!d^^u^vnQZB`WnE^9NBwnV-^!vQX=7DmE4{@>>q(*77^wQ3921n2{S}#* z0sqdI2dM9OCN?YxS3z}=hp@nOmPdpGP8k-2Jp;J2c*63siR7#79ml;75ieP(z(GBj zh$V;-65;twzE;XocXNHr#Y31v8xq?Mm++>f*$pkt3{?`7 z-3&0Ddi~? znB53g2E2oYwso~9^Fx=i`;vFK{!Y1#GlM>QX6_(`vE6_l$4eS*0tinpzlq%r&ga^?{f?^J}c@YvSnL z$cuM9n%c?v`r(rHh*vVi zFj87=`uDGrrDnqdQRhgO{{mb?q!UJgCqQnZBi0ubh8Rrg2OWzlqZ3m@ZUP~oVBvuo zzI*vK$;18;!U_S&eEbKLIjVcYC1ZydOW?DB%B>tL3?Q1LKu)wmu|6v%JY*p5>ewEl z8*hB1^~|d3c@z{BR`R3Bq4v5U@q`snqUn-dMquL9d-IH1l1h)Fg=~YJN`VuskaFw~ zbW@fqrsT-?jq@m8Nh|Oo7N!kKsCo$^(+p+S9$*nwUZCMv$7WBr1mw@e)HWigac4Ep zJ(oRg=gVS*#-yKbjtiNYL6@M&Bf)5c>#0cz)s3zc=$8%OJ;TV>@WaY6<9{E6*a0e%(3hE*;MZA$HWXF_pntqrO7+)!hE~!$RQB*#k zlAtrhk$?}zq$doH*)YtBUQe<0grPNu9SztcADKg|KhS<4Yvp}^KZ3UiO%>bb<+7)G zi*DB_8exLm7zx$Fkj(2j3$gf@3#_{oN(9Qf^>isrTMos6B|uK6lL`5ZN69ITb(tj? zax#O^?!bJYTQw2KU4v4U#EO;z=eW5BWdtheA3UOSal1nG13sUTNNlAxp>EuDjpw%J1jof_~gKf%IZ;Hx_J63bV zmD0BLCI_yLij_rH^Owl@0Uc-elC_a(m$cMd-%0Lmpw!Me>Xk`7;9L#O8{T~^UA*Q@ zniQ}w_8N|)inJGWg(AAf?0nSHfYBz||E@mWUMjF-e0OxmD;5}zI^n=8E;;al|Hxe< z@4qm+w25!2I#>Mp`6uKm>l|$x|DyOi)u*O;n;p@fj2Umr<{YWylbefMN&Ask;NDC`LtaN5!N|(9`lQBsjJkwu}Qt$qK2=;?W_Gv zC>H7tF%NU3weqI<1L-JE*;mq61K$o92Z~wO^$wv>yuOD zFTHQ0Ei~*u<84dJmI{6YIa#iAUZ$nbrB$FWExTuKxYy?@ycmKWUr6>Mo(4NsrOxb|yOsrg@bA|61n1Fdp8a4Xx4gI6NX}U!`QWRmeN&$;Kz? zx>9a$6c@_;Inr;S=6Xk>YemJqajg)$Lw;q)Ha%7Ov~yOHnJHX1%g0K^E6)7pdH6PY z{&sHsN-rl>+HdnROaB0wd-b=P5k&xMX7CR+gYQi_BS3hghyLYp_$L?OvtlwPY;u?~ z2SD9~d{?x_pNi(?)Hc4aw%+K*-2%6W<7Tb7$sd%5YbfkWci^aT?2YioyJjZ$OmL&F z?9b{^zpb8@#prOs7phi;pBe2{UYcq=zIofg^#Hy}UERA*GiT}X^U3cm=Uq+B|Ijxo zv?_jdAXnjW$x$LBr!S3Xf4tX}SRbbFrk#9AzkL~IQ1?_Kq4IwCDmU*4bKzx@<59$} zo-MY1F*mPv#X$L*8mOXqm8t**pMPg73~vknV};b{bJphAy3U4X`_uFfodFVw558D} zlX2d8uI|uw21O`EXC8{1ChL=&69KY|!dJH+pMJr|x_&0*a{2}HQt~q}kizkC$8n8u zO>dc3ed1VHw=>s^)AgNaoa{rMRVn~Mtf4Q{xzT9IV9&XInRJq|O1FU%x;W~kGk&@< zOH5t4O_g80|odS1%d1( znwg0~)+3Y8SM2yj3n?8!WBu>X9=Tsp;ycu_dOuBl(b-qStX#|box(9)}q~=b>$zs#mR@41O{}c1eP3K9E7UwC?hs*0(`VEAO?^Pk+-mpn-X(GA^3M=7i4s@6eza1%O|$z;|8(N`bIrwf)BsabAi%&e zT!TXU#}bTOU}6Cvh4tlRjwcasx8n(ML|mK>Ntrp3BFB_9W-6*eWc9X;1?%{F*;nna z#cwu&b&@RT0VgCr-8r88zOy%~Uh`hB#tp>Q@8Q@JAc%f>{&Ib?ouv0mjDg##%ZYYO zGeSRcFx2ET)2a&&W(3C@GyF4QPA?SSC#uaxBnaEU_lg3+*#w##w0{eAzzD%n^ufck*Op`E+`gCs& z;(b3p>G{;V!KSi7lU`RV@p?{a&YIGzq3Q=xtHb@BTYHwlEnn%xrv^OEc~&L2Q&X3( zpPA9X3zSi*C8S)hgGh+tY)>AL$81)_h~u8zsFd331a83heNXVOIoe1yZF;Eker&;` z(C%2jGDg)+0YS+7TU^o_5UIhq3>e00UxXL-hiXKR8i?Tq2OI&N-UG-pB9UhM$jd%W zE(WpeXaDSqrYp8JZrd9$UhuO^;`z}4VKFF8`{)q1s>o5qMK5r;beQgasb@x`TQ!d` zkO6@bzjT2>uoO=wfeFY#G0S~ocm5GCY*}_(u7-1Ask&Nu!eG4|6;_x;t}1PU92HUt ze%DA4ZY-T4XL;dnb)pI1o%KHt)G8w%(0@8?i3tD1{^>@!gi<-6aMx3+rw`Ww&T0&NI{4@l1{sS}j$gOP)JBcUuU!~1jA5CM zVh9?m6zam%ZFk7V`Q8Zx?}FkgEnS(*+_dIY=C-mi6IC#iEu(bMckQ(G+MhqTcHI;PDX0!~oo7rhzj`w!Np z%By>%h#rdXp3MSpOr*Aqv7#ktk)z~t$g4Tsh*jv?4JDzPLv`{3L(rk)t{m0-Ca2vJ z)kM@~@W(<}l~|2TXx`^zrAP$&>I>kbho*`o3nCcuJu3Cm3csQrf)zw1*yyFW$0b>x zle4pzBSKLPZ3VNBkWxf|7mM4J1egAB`wBh^87XieKmtd}OoCMlZ?;Kue88|M%?_@( z6N8fU^X)cpxP96+<@~wN1Looxbvw6BLc~P4-RJs^dJe=`NF(q{yh3dQi1Kr2DT3W7 zKg+vWmqT%GuoTT2C847+nNzuEJe6;FXru5t=^o7K$=jAF#D%EA(`aO}NCxJ&%X4iG zf#p{_Qc-*ymc0RJVO|Kbi>77w>RQXJ%WlFfLH1Sfa$IR-*WMl8r{-5tI7GBbe#mMI zb?~1>b9S%=NvaK{NtFsM<3WZaZc!9TdNLh|?yjoxy5q(~O!tn80yD8ZWjAPko#;HR zq%Vv7A=LGv@JwXS``=GLG3(#MHfa1IVQ&H7?-L~KmH~32_34Q=%r5xA1e-g%9gUEx zyX4CCW>_*(RNIY!u!^&_mhm& z%gM-IZ=~(ijRslAYi1C}q>oEOV=&W|4iO7YT8X_gAY={(*DF8A$szYoLnfSRpmp8 z1Ue~*Bdmd!yldF3)b|SQG#j}mDOnL$y!)|6j-Q%TQeMjola(=XBZ^{)(wZh8xPsS~ zWo4MxJJ}j#U3%B}PE`?#p_*{4WJczMLL-w_oFeCLio?zIOnT7E67`CWRC^JIilXM( z?5J_;tf0^gf{2^_#nVUgWn3M)4SVBNT6W%bHgo9)<^Pw6hS$~it}LEDS}WuKaC_MN zI0HUlSLnw7%hCOxWPu-&G#iLb4PVk8+ltxVqqg6g4bn$i&G$OHimkw$kNdLakklwC{FHW{rBaXtrU6=>OUA1sH znD4_%IaL^rK$PfD5}Hqflj(qv8{Qc~ zW_%-n5-+IJq8i9V70U6`U-l5}lBnyg2N;x3tE+uoE^?_p-p8qNT=nzsRdfQ0_=n%S zLAqVH_hb3q-zU#cYe(?cFwVR4+rF@A%#=Elfl)o_}F0ynKvpT50Px z{=B7To{MjL@WC$Da*Umsi3o0mSqSEN*V}<3Zb1F!u#8)9oQlaIv z?hjrFd>twkgeile)0iXAD)oModho%oC|{8?C32aR(%rnRhr8}&*i(e0SRnAe}Psa^tS%XB>gqhjb?{2Hci{kq$PMu-jvSvaY3`f~)FVGE5qjubw*?%^vovokVr z`&0bygp;o3yNINZGxBvaoXQI?+!o544`DvFLM(T)feT-UQ7ehMX(?K7DS62p!p0oN z#vJU%9P`GUK>i7~Y3aX~mon>}2B8!Tu^`na)NL0&#kz)HzJW@B#e%?>>GIu6b2siz zwI`Tiy32l!Ler6$N`&&8R2`TnA7U{owCR6c=-Zmaq0J*& z+6AnW7%()*2uKg&1kMb-e+8$YvS zg=C4jK$yTAGPId|G=!ID@~eJSC={r$jl{m(bL#Y7S6wlE|qE4F=$CmGSfGnzm z!_!QE=dPx2dl5L_ej}uAQx%^yMI}HE4|(){YjES+0!6Pn!Mh}#^uV9z`7H1 zEjB{sW#=UJj)E$sW(%=7MTH*qHTsl9?8e}?j2EbP4e>B=?7_4*^+#6gGu=f2%J7+J&})^>#-n4cM#A-(XaYo@9Djt} zyRaOrDD58Mx+b&F+Vu0O_(!+^5%IO90dNyO_6O4+7=>iw^?;+#>Ca;BEz;b^h9_JB zS0txY=DJjFpjy z>0$ICN{gJxv^M3CTVAD}wbSQn{7^3sLOrC3ITyGcZC{rdF6c*oD+;cQXp%VWc-Fn< zUGEawTWpM5p}GTXY7j8k;H*_htWBR2ip&v&r@W@lVBv!GW8f`r#b|*?rE_E{SGOaR z&D zxmfu6&zI=TMF{!8qt;^3U3ty zj)Dqp2Y)=WE6EvJxMV^YRJfbmF{G3H?&4kqia<9^sOmvo8Q1B6OUJ zYhqU(bN3-9)1!?_*))ODscRPV-$v*jllLN8+^#-hgdg4D=AwVxmX|zqDCj5H@rvGO zzb#+%{L0!*b&-~DzNEsFQe~d?!ds_L3d60jezAj@`c`M^Q1IFbwy-u5MMFL4eTawH zgM>HorVN?){kYe{DQ%mAWUA@HkXHqJpOQ|yR%@!a@0B}irb6Whl5JmyUA_MyaQ(n@ zFLFBYBV#Mc2+vH;USg8=pfV_BB(gPUrQ}u#F)%#lnWked;Wy0%N;B>;PnP`~cTO^k z&Nap{MaUljt*v)^m;L2}Rw-_QBDbQ5?OcuaTJtCIlgWDH58O=HtyM_@D=*`m0+{2E z?kEUnXr%SuAe*naKlIW!HLq?<20LxD`)ig zny=DkKInTksM%5vFHadp69?zyhF*OE>s(6~&JWQrjf`EkmArI$3MtOYjkm zeHI#OVu~Z2<86s*Q`ysyMVt5Zfo;qf?avdd_lmmE;;`{$pBOWdx#qK1m{sKVInZ!+K7smNJn?+^&0+^UK_8u+WOazZ`XVQ6p-}C~fJ4%@F15oxG zGtb^iS?9>a$@#<^bt=XAdt)=Q*v~_~xXL@COS9L~GdNDQK7gZV%S2IeTRs%Idgee& z7iZ5|9Z?-2Gb3={pEYdnnsk=`g>{yb5CycVulZk=T7I6)@cGaE8@qGD; zd4h8#`;|jcffI|W_=(@mtf9QDY(XSWXnS6agVGM^VT`<)CeGwC;e}Gh~W>9tkuhH1~Y;~f=z%Hmbjw;G7^ZYG2(X0 z`nVY$WZcwvW30gn{F`c)8_nNKDq@H5@H0iv=zF4f@toDs)*h_}9ykPm zK>X4wQG-XxGfgx>{&lh@YY}rLj%KH~PJvX^x`0qb5%{3vs@9<)oaa+h<`%0kV8eC8 z^=Zga0s8s^ApsN^9l%G#pDXaDI}b)C;sl-U2(-adQ&|<=e~+VzU}T@s;#Ia`ixz8X z(7N6ika9iWNLOcw&wMEOt`^L>IB^rj(I|uCfIsX{aU-a?a%RK#3rw#z@y6I_W%R3* zE0e~!C1K1E@k$ZBI$4gYJ-3&dd3^LNOwPmvPzvH9e+Q?o-pUj{i7QK0pmpbU(c0I} z<63=!_LUO7ovRhkB$g}mOa)*kC-aTBCsQHzvdZZkW+jddX=OBWb^2mv+3XH(#redx zB5r)(nuNk4ABa7^=Bj)lYC;UNp#W?j|nwwTI;SSoJ^_%H8{cmMw=yD znsJMrF<+984wtV#mu}swEP+KfTzM;R>8HwC6GXxp&_U+e|)0i-~+kEKyS%U8f(GX$)rYIrm~NKisr$PsjqJb z{pahs&Sjt}Pgsalb(ZQB&{X;ZR(D|1Q#c~5I#hd4*v?PWqz;A5AF5TmdNXg%wV`iq zkY1T-W)AUudq*j()yx3$zOiH9dz7WcjOTnD^QD^R*_7|{=K0|sRd5O7dmD`(&)$Lb zQe`908Ft2mTJrOd$%EN;)@JovX$wgk(zMt&lzVO0$3_MqwnGxXkJs$PNgLWntRu!? zbI#_$(KB42{P*%_>et=D82cdG%y79-8Hrv>{RSCXpSP!Vv z#Ht$|t_PUY)i^V#j#Gl5Ht zA8no>d1PcurgRULOgp!d&6mDQ%{KegrcN^S*SkuAHZxwtl6$pR^OrdJt?%bDdA4hw7M;hML-{Mu z2WL;WSLjVUur}p)5BV^vv%5!9m}DU+sA9A6v8-#_Q_(Kyq_5EKHY&i~9M@l#f;#^* z)0Dq$+7;Gg2)}k|>p7Y2lgEDXi%8Xsr0{fhyJM1)6~dDRE=y3LuUURahnrIS@>jAo zPqI7m^DW7Rh=XeePRtVn-}xI{y(OhSope?fv|&H~li?mS$4XmS2U7lZl`V%tPTL(R zNNk19rgWIq=hAniMmorgm;8dA2)A@nFA^CXkkr z{9!&>{xZfU8|t)>0a6V)Oo@vhciWW|k+|8BMspdqA1jltc8zaLxu#!4oj}OLJWhft zC9b%};);MQi^}S@o2=$rVolNKE2aq|h!+f&)Dg(0=XfuF)<-!W{4()Z!JYoC(8$2D zY~Cj}`UJ})@`hq}!`UmC=hWLV<2H?T=uHPPA0PD>AI5Du>Snhc#7<|`+YiQ_+UmGo ze6;=Ai!P;HGNj8<;(~}hFn-|7i1pl6`&8c6F(?P+q9_WpenyFd!?#lB?{wJ1)*EZBRar?AWNT2dRH~-{wBg)k2j8eC;-z& zfi$v(+^&q))q4RUXDqQ3$&`n#@JPxS%1z7CK{+3APF?Z1q{_`k*v0v@+Jq=DCBSpL zc~8Xw9y7!j>I>*3la-4Ug+!3H5F~S6eMDsb>Jx%i6@8x%oOAZsIspwXY{pTv;=ev) zb(R{DgZ7ztCV;QAi=Td{Rzu+#N2{4aMMRx4Ij$-Gp6s=I@EAJyT=zmJe~Aa?4XRS7@So`?W;C68&~o4;9!B zjjq0z-B97+TCR9NA-Xg+86r1BY)%kGj)1&;&6q2yVW5KMte%MCP$B`1-j7TLQSFx^F~Xj5u>;>3Zyzs!MA_3}x?!n^P#{0Lnq~k+7g4qh?auxipZc zqt)CE+eA_aBam%xu+qdJP7(0+JjGMb)&R;_Fim!|w>C^o1z|KPqhoqjMXg8XpwGi< zpIf9OPK!tr0e#+q_LWiVh4LWs5Z{c-8)l>3rZ8`wx@e6iE&+d4J?rM zuNJ(dNj~m>)$4ke#iaH7xt+?f(FZTVeA;N-TyR|jz~wfNmTAqwno*YvYH}*E&-HT6 zC6j=A)Vy*ukcS+(QH#2(B07Q*{sK?~ac3ZVe{yliHEnrkAa1y})pNPgzRN&MqsllM zd^VaYr0ZAXTV}TiQ}G0%3D>p4c^$bUl5b2@SXjC133M~=kVmX%KIM$WN8}E0L=;1# zp~bMYN!*C29{0QFy~8g;AtA(2gDVOJ9GrL{9z-cKE-;PoXs@aQN;k@DOOzc&4Za^A2fso7><-{;eGua&*B);U_;{b^=X(rdIZpt5Bw=p*8De=?b* zg-j!$K~L@UlpYT_&NJSsC&f1+Jk8MkR8&RG2ZSh;#~PtfO7anREl}Vj^UhC8Ix&Uv z>g0|Epr9M5L=nE%Wab>#3>y{7r?3A9FW>Ca=V!!~U!JX$4^|j>M$8|NLIsxbl>z+~ z5f$pZ!Jn)OWx*Dj#SK^Nf%ka<#xMhl?D9!piuxUAIDyFfATHx3hE813;%sqFbu>AE z1(UEq;A#nzfpxYIBNWyUEMXV_!<&rlHZ5-7g#pctVk>jA>28h=!`RE^a<3fI_>1e6 zIWar}R@cJ0*6lM7G31vQZe@{V+Q4}V&%1JVq>BWo6f)@qw+6Zn%Ra`X*#f9(AiwJ`NPHvbs@ zc6<1UiLEJ}R8%+n>ae_fjJ6tO*n>Dj7-iFhPD(0$=yQDK_IEqQy_x`4)3iqq(1V?h zYVy&|YHY^z?-NR1F~3xJStV-@ESFldjLOcqXPV2itJg80ECdk!G)-yp9U7YKPEl`n zLX=y^9z6pnkwNYhEKsW^U9qf2Rv^c)D>({e69#lOhnbPbBS*nF9`mJZ^gBs9Ho?wh zvK~p_kMtLg_Py!;B!1@31y+yt&qu~TR&26Wt+A-=Au~Uw??j!xycTVn_Ut7~##Y2^ zJR!?bz=Vahw6r2~aNRvlb@|uIOEW+K-K9^YnrgW8v8cc|6cX|Z>U72=#m}_s#dSZO zfX-N$iyhrnaC>(6%03Juj;(Zcl?-M)*>HygVdzchZM z!%w2qnL#y3eyX5>PKh80ICtZopIwz}A{$O9|Zy&t0J=VBjN`C50M1>?lEX9#h>TktgN^T))mvjf{imvu?dMgVx1qZB?3 zR|Ft#FPtNAm?WLjnA5o^YNZQrW1OHf1VRM1QATD5Bo$0j=*h_GE@*v%LlxM$@%X8U zMpfm>_q?<2$uA8Uj&)01oXtOk-tC!)k@&i!S zSz6(efW2Vt;72!)4n>1o4Co;IcmoOe?}ec+iD);{kL6ZXgEbqiazm`}LszD{9=egI zOvr$iaRt$+57C?>20GxD4|~O5L^%+fZ(0TJp+{qR@IdR=&B!C}<9mRS4w^+QhAw96 z2#-9_O|my{LbLch=Wgny&iteu2awzE5Bes)6BeHwKWRzd!So;FADhlBp1FpH1<$Z& z0BLdU)pNBI0N*F1-L10-Eth)kByKl9t`O;PJN7t_8~<3cxmmVKuDOq`l|S+Ce&K%r zVn6@i0X(O93>QzxKkIsIWaIl~Z{w7$s6%^pR)2&!Zv$HaA7U$D)*kU*Tm26Jd4H*3 zK3;fq4$2~=ASYgDG;D!sih$+U+lAk#losr$#d73QHT6&M*Kf9)Ij)>Z*JEUT^=rc2 zqh33k=4O*t5SP<0Oz*9J8~8S`hm~)udq)@Y15l)xD5;#@Pw|8 z`B>545;SpWdYk2-X63(2_0DYOGWj3+zmc(`0hc#esgDDjJMten{s3rO&92+GJW)?} zDyiMv7rWbDIN10e?Rk;L#L4zueP)&ldL_E_rgtjdZA<-Oz1Dh#>IL63Z8HC9z~ z8HZUXh1$zd>#ZWOS5UMe<#d8cUaH)2Ua(Gwb@7AI)qRyZJ{$poD61>R5+ZWl-xR2Q zP6_`S*a%f(vSnS0l-1P`3O)<{yXl~_k$b*(^U_5-6D%+ZqbU{eBex)9q?3s-Z(vu$ z7a)|WV~=t_23)w+#Wq5COG3vfOQJg|znESUewi&EPWq8rV{JVk5+j8z;M=LT0lqk7 ztVH1bqYe+C{bfQJ7<`)~j+*kL{}zXU?S#ul)f2a-Pcp1mYh3H0{`j#>#j+lcKFH@Q z<428-gBX^-PJoWR;>bY%49mPFj=LNQPsf|at|ug_Ru8nNGRFqE@jr#J{*H7GWC$hr znXbkt!@3atx7Ke+Mh+wlHT)MC8x7oJUGRJMaM?#VHtUiCZh}rVr*Ce7R(rPIn|*@O zv1|SqR?V-!#}lyqC%vK^Puua&Q-|_*d>iXR@BH!Q+PksW_VNdScX$1J_4vZqj1=VF zL5vx{_DBmH`CmVEe*j>r7oEaqo^y16^becb`hm6oOyaOK^wzh2`d>Oi(p4BGU;PHZ z<(pRv*4~Jkq zw%nSxc`@#Y zFSvDn3Fs-!`b1#V2JQx1iN0m5W`y1AIXt$?m1F6R}9ujdfOu4dPB$q}}@g_%m89_6*!`exLbQ zoO}@moIv3q!hzBJ5A4sZa?XJ<@<1f?--&c78_d|z^eY|w)A!?W-kPNuLz4!rYSH!$ z#?T-afPZ8}gCv8IsKym96Uu4)Y?{(ZD{ba@C$VoAyBsESJpOz7qsD*jUQN(T 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 264/472] 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 265/472] 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 266/472] 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 267/472] 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 268/472] 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 269/472] 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 270/472] 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 271/472] 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 272/472] 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 273/472] 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 274/472] 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 zcmdSB1yml(vM4;b2X}WTf#3vpceez0f(H^L1b4Tf!94_r1Oma`J%R@dL4)(=1G43u zyU)A#t^Z$dy_xxDs;jH3t7^KdtGgMlXRf~i7;;jwQUDYb6d(hh!1W^D8_8$Z762eC z%LE_-kmDZs10g6_01A=>Z%}uz6_|vEf&pN_rz`k_0bvLYJmmKm821i_&_O}p^6>&; z=sTDk4@`gu;>~&eSGgedfh@1*0dW8k9v%T64iNzX0SO5a85IW&6$J&A5DOa}hm?q% zjFgCkgo1{Ro`Q;nnuLUbhmqw0J0~Y6IX#~sFUKRchnyS`CQwL7NT?{N1ZZdk9F!!K z9RK6v`Xhje2v|XZ%2EN)m{2g7P}dy*IVdL_Xhn!2?+z$v7+5%X1Vkic6p)}617ax{ zXjm9HI9OPa+8fLVU@_saDA`5fu~nZSP&wgnyo}93q!z1c!&MvoN%PRe*&hiR51)XL zh?b6?fsu)mi<^g+k6&CuQc7AzR!&_*Q%hS%SI^YU+``hz+Q!Az&E4afr`M~1z@Xre z(6G4pgv6xel+?60xq0~og+;|B@2YER>*^aCo7z8hbar+3^!AO7PfSit&&yI@8ednf6)sQ)C(FG76ujpq8Ajj2l#`*goUGIhsP3C zMR?+bO~vsN5l1XGr>YH!`k~rSTodO}WIP(qMcO@xYB!qw_Z0L0Kho?E#s1W54nT#0 z0$&~sCLjzkImu8Y?iinRrpyT^?cvY)=MWioBAY1%!S0HMO*)%?? zb@P%r(vf(pZcNtRt@!ilNRLT|7Rn0;gHrUyO{VaZ^-eU(C*`tHVn1qMQTTqvRoZpd z&~O)I>;G7&{B-K9v%SGj^r}Qig(Q0MxSa5U?@_N+Y4IarbH&({WrNt7doEc&j(;e6 zV(fK?{$ks)?jg7al64+NpD$&%?)?heSKUHO@7hvZrpI?{ z+^F-tms1^6qgVT5soCdv4B%UKU>|l!^n66LMZVVQJ?CGE-QVhAhPePvlqXxhUOB# zTN`Hh2YjSiSq{Vhc9mHrOE9r36HiD(Cw0%8c~}|ba7q!4CBNqDv8`%)FfSuSds5j? z`(Bp8r~jBCYVii| zdkfR?-Fx>t&1K^|;&Qg((xrt@c4&;TE_6ChTWDetimF4df$fBev>8SE#x1mOte>vL z)PjaQywgZOvq$U{T?31T1s}8KoMYgjxZh$&R}4%i(s#7t;eR@bl2qPB;f>yS7k#4Z z!+PL!4Iq)ce)y}U8_$?#j@Y* z573-&7es3zD&VN!59;O(!rZv(<2xubqwaHt8dQ1dL} zP@hVxT?6g!g0K9O5ymfx?(q%(Vjl2aG(V~AL%70{eKF)?TqPMjY|r#Ueor?dd5@;# z^cxC8h;Y=LiJ;j1pe?uwo(G%1R0q))%!9~%<{x;SY1sa3|E%tKoYA{Z8NqHTY*@SI zbRoZ|4}b7NmI-qlPnF~vSg!SuImxN$eza3!i;vw-EdcAj2Tjb2@MFx&#sL4}>W8S) zEBrPJ%3A$0vgRj&vKL}7s=n=oHP=9P`LC6%MWd%+bJQ{WbP^|$e$hTeeP*9~g=ZXg z@h!I_F6H3%&D8V=!oDCGe-(T+-qOQ&g?>es*^0<{)Pzqyr`;ToY4IRs$YsOG{Tjf! z(4v7B#**t@c+LCrqHgp)*eHuHr$cF-zH&REUncnp4xi&GR}(xsc)NaL3%$GeA%FF? zUT=}nDSb?~w4Y3u=xlSL-E>m#9&@LTF@49oX0n{JHwM1TwN;Kw=;UAYB?ViwJy!Y+fLa} z_6+*BgfU{}(Q#%;NK7$cb|-D=wz`dYYdmBhi6KeNxr?Hpe+(#AEOh0L*Rm{-{kbi3 z-dg)3;ivDUge-N7uI!tyi!-%R65(2na36^y*v$ah<<)NW1??o?_O89cON6Xz04nDi zSYi}jG<`OH^=kMU@Ef@X_HZ)6hRwZN7hDh>6kC#THPOJF@rio`JI>iDVJS{I2yYq| zCH%a=AU6Wh<<9;d)5)knp|(sRnAgXM|0a|I>3+EQiB6n=n?z64^&+m^D=*QasY| z1U`HfCKu>+jk_=PgBZ0qcCcX)U}HqeiG2|bH>-*D8_#{kt5o$vCJ96s@2l<7hbWO1HWx_cL`%Dy8Mse{H>too}UJ~o^&zTGE1A~S(U@9if zR#9PHNp>seeNraDK8)a})4a1T!4ENbBAzPKh$<{oIQyqGPW%Pe;(`9z|P zma+_`fzRs45A_`(|B3E51Ai$gKHPlf+?Q`LqwVC(o-x_nFP+3a zqZOSIJvzM9Fy$zwo^*NRlxbJ{tAzB8agruTs9wwYUXB62D_ZZa0k_seY9mcOm6NG0 znVO(Jq^y-5CMsB{31{WTGTXL3ZmAj1qJH)1qDsQ}C7zJU1tj~QNHFZaN1O8(k`qhz z(ad;&r5#F?l9SQRBcCBpUS>wN2MdOli8JOzlUyM zo+KZd+YSdvBlh};1KHQRioAUgbQTN+=&1=9blRs>qa*tRwHc_Gii30Uw!|>&i#@8j`ka4F2d$0)> z7+-n`RkA;lz>KZ%?RgHi$BLbc&DTf9zqC&^Kg-!lP5jVPcQ&&|GAU^ouIGY{eoW}u zd>%fkJ-%h=G)v+V+;XI!c*(G%cA7+f`0E;YQF$`Drek_#bPen#UIRpL_#Tbox_?Ev z238)wzXmEDFH^Kh*K~Zxzh3UQF6;3t)zXDIRB{%^T4Dw|3PQo+!BiC$rN0^D%xkpB zI2Qa8wtxRbr>X3jtOcD2lzj{vAK@~qs^T8LVP6eaXH~=H%A@t4T>aa0?)*Qct5l6d z@jDF7pJ+FxI0~ocHRiQg;%114_SnUWV&xvkc!L%R`=x1UKcQ2hB zSB)^ToO}{NkKgWAVI=-Y8(Vz!oxK3ndZwUl76A+)d4{9ZJ*0)zN>!yn6iLEgv{ura`JzO~W3#1%zLWJPWu(T{bOPn6uvkI5?#)}@c_D+eQu zCcZ7lNO_HgHz^cfC=8X^e)E}A;^kdyLMeS#J}aT^v|!1{z+O+jtn+J3VvNNCS{I4- z;?t1Re7&5VOfHjfDt*t_XUD$^4Sl=&FY&JdQC!McXCGbR-Hkj`Y<4;<<>HtLoE>`v z!(7}NN?CFXqgM+P&i<^~uoyl*t*Xig_<(&p@EDzQPezZs>-%ig79}UnO2!OR()c|n z;SJK@bI#aO*4iPB=*EfXX-{1>+q=G5@?yn`x>EQ@N#`Vt7FC42flUQqvsex<7>|3} zSCO<}(B?1W<;scD%M|YMha%=8LNU7-U0&xZ{G}>suuDUhr(I~R1tql zis&;dYduY5rCCty!lIhGH=D-$SdCzNo4|52{)K;v+~{S(Q@w_zbTD+F+Pkvq>vDl| zVL75v9;+$Dv#Ghwu%mvpe)Y_Ec`bTj`BM2x$R>nhNBtvG-FpD*MeWAb+30!J>GCxY zo#us$skmAyrOmw|^7oWOc@Hpd#`z59QqH#Vdo@>=> z-od8A)KOMFN2T^Y^g$Zj+>9wRK~W?Rgt|Eo8|4k@dIK}^Dkv$ zrY5y^7PPr+$)jls;vTQrsiZ3IY&G$z=`j+poHWCm+COy;luQzAlUm}%X`K5E7p6;A zlO3&BdkKaGDmy%2kOx}eh?2df%IX@h@MHc+`bMK2`)y{zDTJ7itR_P0)38kgmr>Rp z+qmyfaYYTtuH;JMyalPCWO~#z~SEZ zij$NY93!$!!7f^PGH#M7AV}An)HkM`nzt}6+3-_JID@NJ1WI_e$&vhU_ZpZk98}#- zK03f}l5uVRe&tb5sU7E0c`~7CVirVvek^bG40FdYo$cGhmvg0)VoGw`N}r*_7)miz zC2^p*3eP}yTseuNd<4!Aj7!AWY-z|bZDC#c1KY@d9hGEQ84bnSPM1xM_F(<^hU*iZ z?9i&bp`<7`A~!H6SuEAvAK%43a)tXqD@;sjbIrk)Sv4@uJmGQ{2`2i-vhYxoOVON5 zy=*;U&q{pP17WhG0i$ZdYm8j3yg%?$Y&Vb}xXrzCKI9calP!>WyO}iB*SpNq$dg$s(Og(7C@x-!jOv>0u zFUm^_*Om!;N+yvn_te!QJT-%6R3&l6kQv_i(cQVK9P6vrpQT4K64UR>&JtJl*3Ig% zWEx3RL&*Z;zX>DITOnp`Z3LGizy_(7P&d2)Ml~(T z7YuI-+WP4F9e)`3ewDw%v8%gU?Y=y0U6jNrBguI;Pwu>Yb!PlC*S!80oA53;`$T3a zeBvB*OK*FH1A9Y%d_ym!GHZX?9}V`M_SoDyb87Q$;hoA8RY?Jqgv_0C%lBz2@ryHA z?^_pDo93||RR7HOUOhZhm82y`D{YSaqiAH(HE<)_G$>ofs|0R+Ur>f~)XlCXv zrr@$J1xO`g`qUX*4u^US!rUINPa$v$2;-PqJuw5}G7x5Q1_cD+83^9w7T$)ymNzgo z2m(0H8fxO8Za5%JVfh!>}Nu0OTEjG~n^4ZCxxLa@?SxP{jZM?#uP{0WAO^!~?)(%=Pto&h_=> z8*nFK0RTQa{8`>H8vuBZKzz)fIGQW~Ko0DDgih_)Sf_4uJ1MME>JrooSJPb^199&#n zRCIg-JRAZn99*26-3u773@jWH92^o38VVZD|M<9W2X`-^a$&($eQ@Fa_wEG=*zlnt z>--Q?+#bJI`N2H^Vi13`3Gh1)Y__+18`pOW=@>8=Ansy^>k{SA7irIzexu1is72#R zLW+`^AdVo;hqk4s-CG2elDF% zY1eb};@6-L{SSc;I^X+N2vNHl8y@ovVHTJOiiUKUGTEgnz_*!3gsVH+2A3{2U9jaD zC%MWeaw&>Hm9ux$+KDze({V4E5N`O6lY9T%jXC>LICe4I`QFiXE%ScmudMr9*MOi` z7WtcuB~c*6B=y{K{Kb0aPrgT4!qcu6+{%y4!@UItPbzsfF0+CKU%qu3n(cWsdKl%+ z;YH@iownRxXaL-c^_vyM5;&Y+)6SeCmu~yI>JjgExSkxWJaNEB=2&>>UAkPINs9$= zE2@dYubX?gMk@a7HMF*Kk*u|w%iq&ymbB&$2yJ(Lf7M&2H!9W{0fy~rs9h#O4HOLz zl4x??sp6(|W5@KQ2NkQ>;RCo2DA1}E;_h`Q&-i4mxjRi8%bi5upIAG+JY$$wjt2Wa zzrb>JG&!Ci1uW^sMdrQCw{cLkL!n0kN8Var+)?WxZyOp3fW^9b?S7Cu4S?mW%=8>P zO0>icty*N_A;x0?dX1a>o0}GFER~4N4G2Y_CdfFe#MHgy70{bVL3ekvHjI_x3Y@!j^TxYGR$Mdtt7sgW;zo;Bcl5=W3xzgFG z3Ds76*s;uLy43I!M2T5X(}|@b$Sf4VPl{u zZfyE+z~QrOO@|(JIr|7l0t4u@BCA8?ZN)kgSWttNOjEH#UU`{5UFw(_a8R(3*otwb z<5uESjC_^DJ{bB@bUdZc>5YTkfCCXb{|c#WM0ZyD*~L|>SB8&m^*UYCX2-s3RIp&7 z&01MoO=PjP#5V@_wFiY+$gkr*OS92qaorCdITeZzY>oX?hmE(8G{;xP8%7!$PVy|3 zJK}(-iWk+=!j!!&^c7`9afu^Q!A9P_ml2>n3HpiEzJC7BC#0-wbfwfPxwY5b{W->8 z1i(a*ajDMYaKL8**zwpz;ZOr!5z_Wh6g$hXCxJ=%I8@2-7*VzF-FCg&fr}O_8ql30aAE${nN!|r{6E(!^1jP|(B|(K zxuHVA1-k9rOGksOLa{_(36UXX@R>BemMw;xFqj5eYs7Vlt%ib~2^QMp>!y+KYRlH} ze4wMiddvF!8N=~Kd9Ii~rU2roa`B^_QFdZ=k^}bTgBt%%5f+WVRquCGo0fHLmQmCK zU-8>9fg=}|n7@e-C?*jD1uy1@#{|m!N&mU9bZ^W16#JwpZ?EH_JkGfwVs%>AHuf-U zR|m_oJaAmY^XB|ytlPFnBZ)?&QEbQ3w(g7lyQIu2KpY>9-kxDs?!=-4DW8XZt(*=Y-sA zW7AQ+KNdG!90wcixUA=E`BrP47~4(b*ubG7_+EyQu{`c!jW&(?%-WuwlnB_AboNf? zK<+pCK+8btrvZKoRW@!b^nvu5i?*6;)TKc`%`H)<2>U!~$>9@~SQ`9 zAxpNEwt$%{U_e2^B<%}bZ#UQ5(0CdnunN(*`uU1pMCr<*7h16~V3l}}9NcfT0Sq9e z!P?>g@OfkButE2O?a}FAR#$q3LtN*w-`0mB*vXvqdN5@5mrC=MavlSqXQBXkLZhX+ z-8cM1Sn-@lIqba|h=C>2otFUZ6a&!s0l*CaV!=O=aM@nB?gDl!mcHMlfw@AZ@>4$m z)LRySmVra@yF~-s76Q|ergkHO@B*iUZ{I6`?gBdAm{oR7@;AjDA%eEh7p-&=v?eY# zXbUPDsE3$Xumch@Fzy-r+G}?-?v;v726r(0K)2dY7E^(fBhQv7E_;bdBJ+NFs1d2B zF2~)6Jj0jd9$5w7Kn2oYuTK@kK+)qXoa=4?(A{LGN5$D74?Nx`^O<24nmtnN(}T&= zfpO^K*;2Ud->rw}sD3k=_&dKV#MYo&=hGhA6HXCqg=Uovy}^J1SZY-M5+IQ3x;alS z;PpHull?1iw+|84QU=V>fJ=S%;qZXBvKnOY!E&n1GD!mvh)$1-^8h5?`r&Ft2$LFK zFUz?CKXjOJ$04KczFn>FY6CyJ!9D?YD7dhfY-ZhCttcB_HMcTA^?cwE-v<{bzE!4o z_(95pf0{DTEw}eJjhgT~`{u#xeIt4TCM23T)L`lttqK?26K>2;!6r)&*2|a;fI`@$ zi2(88FU~nfz_KXf!3EwWR=`g|?u*s!7ZsOmigXX@VxW6UL?8_qnvO4?yyAj9e{)I% zQWbzeb=ZsEe>Lsj;&MBX0s!$tQsQodQKcrqdxAzD4*DP!Ol*sBfsg4Xe_{ieijRD8 zjxz%AP7l{AQUJ1@E#7HXAQ~>`)40$iC-98ywU6_$khV`UT|8mD43wEB1*9QEVUzSm zPy3*c9obTV)DWg#X2Gl1fNh&G-(Sq1xa|7TL=gV}M%Cb(2P+oxaIl4lm)Ihl+hV(M zc~4)$4~)1;>^ZjA%Vdy(u@#zAw^S(tqv%qPT}COu=E(B&u)j957%A9qBWv(#xNmpr zd%AnX&pa7CNi`@uMG;zzSWx-OtzDFjrr9~r8-oW@FuB6M`2KF~H^~ilkO|~~s%9U5 z?}D~W`JH|HVBSfW0+a%`w0Q8nWl?HK=uHobn(WnwqQQ|IqcfqBNU`AP@S{@&yD_qY zS*Ras?D*>0`-nZ7F?~$lm|Oo*rGi;{U%!82F)TQ&VqBd7n=~3>Bss!yRD)v-si~oKU%Z~=GM489`4MH z2OW*X+=>d!rhNaxx3(8^NOIK@wLs=@ktJJ@sb{Dz@`-(z1 z83vzuE8ceEeu>;;Qe@3CM%RAT8k_ngN*~kC=B3G78HxK$etNhuM0=z*9#+Gk0fB?X_o9+bv>;v6=L)Zbsx3;Aw2-`z^K^ec#2ot2+wC!#aJAAt3PD8Ws~?-2qV z@_KzSF8tVQltIntH(*n^?xv30nz`PEyC zn|nO_F4JO8a}ROLUV?)q=n392#Yokxj~BWJr4w^*ezLod_lor0Y{iLYPx`}5uic=N zmGTX)&xP7V7th1L(BrJ9HE-(+K1-K+AVM)<_c|X`0V<@&Uct4Jm<}3UGPr#0?2)bH zN6-WOml{+nfyVxyY9Jcqyb$@e~8q5LN2Gh(}ic`ukovKBykvBOmv8(swfzGm~_5jinXkjr(1BHiP^L zI}@!moo{T@nr*(mW_6?1a@kO?*1H(TD;syt;OBQ%|k$&5TnuUoeF?2}V`&g*rdc#(cIlCBl8YD;k!_(2hk zXUPlw@M-b+%>Q#9r89?jC#uuEjk7V#x>~7>=M$}?Y{d?LL;dm`2R+WXNrrxP0vPz+ z9FUQI478q@-M$iK8{IGUd6D>D9!4lp;DJ8Ief-H*A-ONK zfmvc1DfS`sH2PW>zskD=)ZH_9EgvZ9)aC#DV3)*Lu*1);2uq)voYVQWdypt6Y$rqw z8syn76dLY0k2Ng-;c0o_3Z z#ZUwnB7>oe3rpO1Y_-wt8YXX1_lXM(=)gG@4WNmluji5yXXf`3uFvl^E+U4GKibOg zn!rIetf4QIOa^e7eOfgcKBl2BQ@9hOL=Vpr9e?gLRXC;D!2VnDrmb?L*23@~|s>5gIs*I0!)!*v7^Fk;( z?NXR*mn}qi~Q!Ytu>aY|K9NTO8vNfP}8y4V-OOfI=y1ZIUF4n`3lZGMC<02S=&PXKSKpAsNp_uo?V7JhTiDqVP3% zKKRYS(Whr|02Q_+LcOr=%1-X6E(J95=kCqn*7dFO`HU@!zgg!vlU|v*QW$gV$?k(z z&con%I#Y!h6fyw;8p#Ofxgocj$g=XGje;&UxB)HmvMp;;2iv_f>#7tKBCl@CcqL^m z?@UA9&l>qy*dSG#n zXdhuC(tFQ091SKkV3e(XI2%V!Dox|1NA=lR6u}pa&aJdE7c{>=o*QGmlNl1wgTCdt zF?#ODOLVp_t!$v`F_dZdx;0#c3o%ZHJ+&jVOA~W-!b?+<&hhd*x5mnzje;RHxJW_M z%6^|4Y57PwhtpruRa8xb>`OAD_CU^F~-U^)AWVg-*jL!)?s#|rP_Q;&qlgz%8 z_|8e#E*?HJ_I=F?37Bst%{_cU*08n@&tg3lbCU9JCT`?jB7%vw``v*DWs41zUo zM*caeDXsNGZA(VzEOwUxe9XqYWMD$R08q^h{g?eu=%W}#`^ zg-;U%QJUvyYR_7|`}2(KdGpZh(8_X+WoX{*st6tvG-qAZI@yKKdw!B%6H|@051{gE zpTDSIc2lqIETpe^%{M5*c^oy&+Gxo2dA)8b*6Pl-vH2to_-$BgX*qOh&|Gv_)I<96~vQ2CSk4a;e5B#HOQs`9~Fzx_H4hp;k z5h#;9=&=y65u)m(mk(%GjN%?(sdF|1VdylL$gcCTR z-i-G*%RHb5!4hG#*dV?zS4n{$pYeuHnK zq__$A{HW`n0|R+&U*>&}ep_&T=wXN2pMe3W+_at5e+om2mJi++Iu<|61{JPat*(i= zi&wya09@AuV*@0Ln{WzQuldwCzC~a6JNs-VRV;-rt_+B$@k7<>wrDmLrMih!z&8pS zfBv`a?wXM_dB&=Mw|pmvhKpv+$OA(f7L}NrnB$+Sn0qKz1*rmzp1|vw(X+)C^92LV}_U_*pM~O{?nqCRF7z=TDkpm z6en`zC9xRiZ1qI)MO%(Ms!0Aq6))6h*7M$3?s(M}6LRB;Zo)dV$~z46XU#Qz^NP&& zadlb#{fb<*N5}y)wZb9Rbfv|*=1wR*Mq*1Cn(0$`S_WcESlSoQvBGUNSKlA^sV|k- zSsgk%T@v4ux0iI+WL1>emWr9gBGHW_bVHGMo#^v?K*XI@i2cA8(FgmUNbb|lhgZ`A z0z9eW%5_Py_7CO^JyqxA*~I8Rd$Gl>jLQi~u(f$nn=H|g_=NrkafLLiq&4-p z-^U(PF}gcfq}T8W>jYJynGm(B=Q;IC+^2Lx7OO6m*41@_(M3ynbXcPRdpqhG_f?CzoYS&K9$J-a0GP1Os z)>){VPZQN<6m#fTw*_sUf1ly+x7`0xBxE>=(BG7O>Y+i-R`PNuqM|39{K-}<^s%&+ zfl~jtd$bv8(AD^Ol0K3y{pZ;a*=_|_2s}GU-Xh6S^b-h_4~)=)-MD~Vpw06VMujo6C=E> z%_qDNEG4{nUkb{gk`-L*8h9aGT67~*tqNGMD>~UtWU5^&Mbr+>z`uojGU>)dXK$6- zwc`4T(G9{Ze;$|Q+?o!`j`Ruj%hEqCN_`|9J)c`$Pdzm;xrr)~z%RYrL~l(XiEUzo z(>E2iZ@m#g)vZxpK6|P;Wxh?tgnVlEG5qIgQJfFtb(CbExjhGCySz8CRL(hsG`;{0!P)LqyO)3PC=(QjXW;(U-S z;SL+fX}0Fy<^KuM{yt+!!Bkk6e_PEPLTJ&H5uH&T&QxfYuCO5gwmJ}HFU5(+sf9)- zHqQPgC;P2^QIN=M)kwHV+;D}!0!|H1JhXg_nl|oSA`~KKyi^ng>!P5}5tAwXxyYUvA5idaw%2;t~k+G2Vh*ypU7cR2W7 zoCk^Y*hnpC)1ka`W`2h`Sn&1B$V9%wd6Y;Gacm-z@<(S2Np~@(G#hc}T<)`a?|uq1 zylqVp`fMgud>=8YIqSn#-208q=?X7noMuW3ZN&|irOXv8T&3{UFu=uAHM;5|i1 zzmj`@s9|0oTSyrb)-Z_uj=IG0r@N1+1W7EMovoDGlwM-n+-UhQ>V>lr4I@ZlTOQHjw{7!^?kUg|M8DE|Q;yI-HB{gU%2Yf6{E;NLi3L zWlWD52jP><$ITtaTxiX#Q^($XGH>|XxK}?dx$=&1r=sPu(x7p7rNM>ZQCMf|yflFe zA+jQp!ATQi=CT-m&s;&Ho0JNz#4Kw0xF0ppe?cqi9A3x#^W%D4>Xx=7LJKWuB_?U_ zR}^_6fftA}H7w0_?q!2y#d~;jLwG z;C(a0o3+v5XkQq8Tn|H0(09o}2uHW}Iw`5>2(8(CMXT=1qpr2-WerBo%Tg0kzR3E3 zxwLtU=luEgh;jPL+BVG(CLGEKB8T6akW5Hey^^c#lZPWiV+y2e=$m(cI2q*Oc@5wV zvlb`{nJBK8Ik*oDh0%;}s-${_sy!L{Shm@pGsQUixlm!;J(OQyfp_`d+%j8?H(!q! zYn!1HTYA`5sB>urZJ##?2T#Ln;P*(e;Yk}wX})2p0(19F&z5qShq{kejHAiNV~r?G zg>$ruth24TIn_90IOmD%!-B|-C4i>@|1M1$z9uz*Sd@`G5PN4`pu z&t7FvzZ>mRR|qGf(3nMLCKhY;>t(U!4RhH-ihkZ(*izM;X**_ZHKH`cBT*@kHkM5JA4PiqPa;uN zDwc~&q?Jo*KGamb7s~M~?1N8U$w04Lk<^Y{A(5ShEvrP?n?l04#2SSrsvQ`WRi$z+ zp>+Qr=WHjUby7$)IyW+8S-r~4g>&l^b!osqB<{0}Noq&jYXw(RMi+@(k4h-?nuR&MHKH@bI z?y*r|u9SdGw>7~IHEEtH1QoQpi`)>{!A4=3a#DSnLn<_ipqUUJVt`X|9@zdZ(ZhfZ zAH_fM>(d4UB(X*Cev6mGM*(y5Jr>yTp|#!G!3;*jUnnn&B!s`off##pqoVw3@6G9lwde7RP8rtJOWnr_ zuIIWdM60I>A!RPjr~_{ zVv5Le3%@h}UW6YuSLj9+F#9d7`HhCZHHZN9__QA+H3xnia{o0(1^5Tol2LiQv3p4C zt23A^N5))C>b5+n zT_roj9+!bkD9-I}jb$}!d!%1R>>LMI3a66$-pq)lyLCQfOu$^DE14>eb;#!-O&ZM` z8MCtH%}hYjswRa`M|8y6;b#fP zDGS8f@xoD%t~AMKg%;4Y`&Ew+`ov;4gl3WH-#Oc_A9b;-ym;qE~T`f9$wMr>-m}M7;J^ zaJEcPB)IT{c0AS){0f8Jqf_!&xZUgxAhtuo2{_NcUbY`+>`8_I*(pLuF0GG zzr#m<8O0?v75B^T-Ck@E0M6b}*Ye*A`b|wt>MG+;A^dr1{RRFL56%6qY>v0(?H?Du zVU~vfTqrfTFapOfCLWON7GjGrMWIah*@UQr3xw4>-62aR9b9s^7+$mN4}~QXY=4OL zcK~K?vyJ>8rO<-1*VM>hEh(k_TKwzAfiOxX6+2O>P-xml*`us*Pk$Nn?(#%iAlS%}17m#4=HAB%>XGFgZw_jIBVP48*ZgL$l8s*lzf^JZP; zyf*4x`Mfsc0mu3lx-@!dRMZL%XElq(N<;~ZrAk<7;?DMXuVjMy;crNTZb%Da7Q!W8 z%Hq^KY}gzR%6|m1DCd=RPFI5Pf~4|=S>>Kh6h=aBubi_oZ;@cm{v9$>?`d%kQi}Bt zYfSy{Qn0RzTE>s*g$P7+Uu*oww2LY8Bpuiwvh7S3A~F;zq`kC;qp9EwipOpEJbT4y z%q81EBE}(x1PiL4j1)i;uN7&8E6Dl16DD+iHqGG!5sjc+jfE-n3dKHKjN&Vq*!9OI z!f^@Va4*f6GT%1AU|uBT>9C+ssvVj8H$B`5m3ga!6U#IC*qi#?MvVmzjz-~#fH{xc zN)!d|r7RZq#S{vhU+m!w3myt|?2!)m5PA*g|M4BJu$DLecO`S`pLkP$x$D_ zIF>V39LML5aL*D)hsEO0z=1cyz+@Sp030CMox<c0 z&!w*#xoX*sqkc?@D$21pFKLew zH-DfAmksxm*C6vw)_&pi!pZfi8nJ@LQufwBL+Nrj zc!mt~%zp3mCO@bGhr;(dR}EzfPKh5}pKcZD(+I0%FAwmqjTVGfBd^P3s;G~^LDrGJW4zxgRyjfRSDB5) z0JsR4;GKPml{d0IyECYXRUw-wk%-qY$rrp>R~r9naN$gs3J*g~U>gqwin^)o@mn2H znccTKEU+&@kOf)V+rRBG4GJ81|0@kn)H$5r2vzb=S}I6Jmhu7Ub)cokZ_7+V9%pu- zHyUFSYQ3FI8A(2Sd+7@Xn7w?bP~ z)x6fn()BUeZ zz7?Ha_zp>)>|J`|E`7SQW9zon#&HPq(0@IMcrPZ?mjdf#Qro;8{>VDS)H zxRgERJ~||d6r#NbOr@3UCh#qJ$9_=!qgEFS@}F))xh4~P##0UfESYUt>MEM3j>KdW zS<+A%cbeFlII*I4k%o7$v`Jq-BtPzkl;Zoli1V8=ACPZaR405=*BG_H^WU?8+X(?; z(BWyZYMxCIaUW3$>|Xc;*MLpY?Md9nUXra$@P2<mC* zN?txPIUxEM%;PCf2_vW8Pn0D&CD{&yp(q?4n&OaCSNBi;!+%E+4Ro|}f35mo)V&2j z72CHzj!1WR_o1XgKtY;A$Du($q@+VYDUpzpJVc|-z1Ms7 ze&2oX_x?2u>+D%;?X^B@?>RGj)|uIJp6qfWG&`*i3|fWb=4TG%@^Y9(6_p}Y?CU(9 zY#oG~J$|5fE;rhhVD>5>Hk?~vz4pmSNe{Ie8bGC_N0`@{-nF08?Mvvbqz7zMDZ=Rw zp=DR0oNjQMkPL<}!5Aum&c!-*Ws})7gkTKWL2H=&3=})_SaIIq4SWT|>5beH9Oe>? z-h%v&GaJ5{$4b8umORU6HlYxxo71RIC>H_NSU9Df5N)LTZU->+^LlCgPfmvsfb#+> zfR<8nKCOtOr5$`%G^(xUQ5!U(94WmmZHZo<>C7Pii8U}cvnw#yAIJR z+)Szf@@GWGOSeoyeMx*CU2bh*Jj*#_d`Jl-J+K18Up1>Ad_x$ct*kX3#|pkBJ3Z&2 zyuY?Rd=>;~v%SbA-2?OxuK~$Hz|w6UAS2ha3gqN4*NwiwwqI-qi5a@0-!YYF1811a%usmRz4RKu zMH;n)kr9BVXXWtA!B^kJhOw%3;yI5At=Y>qd%jV6 zhs=454-TKU_kWMqv0xPd@|Rc*&bwzqHt}3XOw01A_d|wo^MSaOIR>w_yek*%1j4#( zeq-L7zl~7mcMK~)8WrO9?7Y#F3@@nr%Mc?1BNjLkv zzPW$K>soqmjG~J5k6zXEA-Eq&LtCqPsGCGira@yFht=cDCk9q98pq`eBqx#MVD(3GyS=rUvD&1Tg64};#uHhn7wV*?U|f0;wiLxGMWCR9EuOH#Bk?#Swl8S$6EBdbiQg0Le*)5V zP~fDfwGND?Z`O0Z^C0ABWS(gpj~RQ*>Tr4L8wDy!KUi0)!nQo^pZ+O=|B$5$0t*sPuS;3$1GNR@)l z8SrxBe`;Rpa0D3k|MvVI(5Q7*nx4pZW9j0|OS?xLdh@=-GQ+9o`OB~!v#-EK8gPOj zZ7L|3pnEQc?0%7L_&G03Je09%x8Hp0v+$3!NQvTJ#V?}_yjNdt0J%&OML+eRPehgd z@Wge1j{ao^J0L-S8lK8#jvl$lWZzQa{0uJ;e9v(IAy3e~NO#laC3pZ=$$1Id-w9h^&Fghm*>I|4AU6{I!s zw4(Pe%aU-N3)V;=cBj{x6a}D6rz#Eq0P5IEp?tddfN%4{w=d)lKR)n2$*@`c&O%71hq0a?EWFrHH;jL1-2KCD0sYjEvY7kdV1;-rwC{Nd#IJ2A z{PH6)UF~pj{-gi>KTB!Ft17^sBEkR|_zmbk|0$pZ@cv5({(e!UeW%1;8RvSJJ1TH@ zksLlSX#nC?bZk^4MBwuZaGNzSTPS7iQnWwX>+zv3U#kjYO>n>QJkkXg5!@9-PI&Sov@v+ zdMUZdY0Qg~sG}I%eN*Uom%~+oiBve)A%Xw+jq&xk)+BXW{Czo5?(4qyqyt&zXRz1l zTitBh`fBxohmBDLm4bJbjK;C4P9+lKML`c`!t7BjT~+<{B&pcmAkq^o>5-?@>mYMu zbQ6=^=*PJg@;JyeYnjg;3*Vqoz|KCXJ2(Ct ziDxLjge~UiG+GSKo}A=wM_KxWyFdsY%li<= zU|yZHx`}l=UGdcHtg^j1#h*OKi zuKfl91@FzW$2`6vFuI)%@=q%Vebs2+7YQ1B=jsz~)-Z3%CO{#8aL{Uc<-=FsDKaQc zBt|IC7vxHsz8z{jlov^oQ3T<5vH=#eMYKhAQgImj7NU}Kr-NLFr^Z+>+g@#F^y-x< z`EczxVh!4jVU0eFjuo(sXPc{s&LXWqc1nngk!+ij%bm(C3!RWyLxLox4AThIK+#mf z%$v+EhD%#_S&W$Oi4n31tVpTPBM790p9{rB)ZY^$CV2W)Ou?_(jm**W$y^IDcOb|i zcz#uzf5yRKsdchbj!0P5(p!Qny@U0*@BX&=GF9thicnIj6MdpGOwBJz;#?>Z62e+S z^bKLxCa8YfdMSuRz}l?6>O5ddl#9F?watR={Z%g2XU|v?*^x=YA2+k0F;x=++BfrF z446sg)|98v>-H9%^2sVK;M$E|z0WY9XI74zaX!lG&N#f{Y^s2O&_3n}wgfp+d11b` zN&beAlCSX*2U|x(0OyXC+95MIzoVeN>23UuljYp`kvc7A6b&*4^+EJ~DCrjgAaNUj zceNnIJli+x5!Dm7gpupVO*0b@*-uSF*{=h$%sMrI zsk1p`teD^MvUiroZz6ku7JM)Kdc3t9Ic%|B;IU#BnWH%8*3wZclrk)G^lNhPP4J>Z z4+Z5MRXnmb3|0hzXu_b{usjHR5d;kTWdrx1bSz-)#!WVl>*-Czw+dz>-2%IwCFcSo zXm^jK=fh6QqMQoL$26Z?cS6{Oi7AV}2x56dyJHdrgj=pNYAZzH(uoudnPaJn4&-)J zyFWkC@2~L-n?@DMQ3w;^AmVxE8t)X-@18{vt)i|9O!{?C;^6hhQ`gelpsN8^2Cv)6 zCf$oSl;jPX+NvQ3%-PQ?xQKm1tpS$oZF2+m6M{kA4+&v#A^GQPXYE>tH%V4ouXL!9 zMa5_cW4h5Np{U)$2>t3JE^UxtD||A397+*k9fGBpvw)-Rz%H!2W=nR=0cGR_n$ktC zFcsf21%JeI^J(*wcbZf76Ta&=f_h<;OOeb^W4*zj%Mjhv4-uvCv!1$>h^aFB66X`2 zRd%FNSI+7loX_XT-U|$HxOP{rN0eybiH5TTxl4|-M&oNO^|xI{MdWv+y%}SXtecl= z?moi2J@m%#-d8C`Y+STG zU-ihU%Xw`~&4=E!=RFEnx=NCO!o`VYaLMLFUKz`Hi}8m$a4zmN(Ebv;iBv|wPBX1` z(c!fCxzaDmRecANK-=EjsH8w&3C9ibbp#*DGovOrJXM!G5#B?7V5eY@tN=o$%Owhk z*2K64FQ!D!q;_AFatYt0xE&M!5b~2Wefd%TQAgeXRn0{ifA$es#Eh0k*iQfR3jX2^ z<+ZP;@b!dHw5ja}>)&mx$a*bH2bHFqP5y-HuSIgJJyH07RD#=X-(W|C^-5$yRQ=f|8=AYR#+p(X1F}2_>A6dBY zDGMujg?cjLZpO1@Dzjs+!efWw9$|S-`riU?vU)wyI{)#N$BPc&$$O2rT~g#+17Zzi zJrdUhL|S+EG$N?+kis!s>j#hrf{!O?pazkq2O)VnIeIs;R<(nLD06#_ zQ1y~A6be;B)SKv7G~4cGWPUex>E-zahD$GneOTVfxw&BhAMOV2u-x6<{VS5epOEm} zGd^*)*VL5NxO$B@b*EQc`iI$97oq)Z_Pf8o2rg~yUqtm!0=AnM!RTHD16lbgN|Q)& zc(5kF%55)NK{-0~#8tS3n@0+-H5<{_rEdUoDfnLTvXw3kYar_dWhp8WHKR%6K0!b3 z5x(`KK?QcPP8DfhwXt@e0+?08$FgN9qTgZ6tW*9r%~0HYcGY1mo&0( z-Fa=dLkptP3*~zzv&=MUGNiE0)*vmp)u>JNBw4(r0|qO+qupDP)r1$+l4&6`xI`id zfk;#2EkqAaojsYrq@pf669++j?j`yQ7Jp(!* z5Qri%K>ogH^THhT*6LdD+tS*E6?Y|7AP|jN4bNp8VWYII%^~k-LWnjLb((&adiA|g z6y6kTPr`ZrVel94?hvdio2l80*jF1@)m<2@_t5L)1eM{Zdo&|<6d4s7GojC-e}S1l z<&x|CI;jkb`NXdWc!po2GWgLXcY}t{#LY+ZXm>unTuVt1E!_@uW_|d=ErW^smFz-V z?v#tii#o+!Y2uc`n>)A)pH&+_c?PfEX#Ii0@|xWuDVsI*d+_5JLit3MMqZeu-oOl+ z)SNos1t_o4Yw&X}y>nOk)+S@0&^nYc^0d#b&`A2+#HJ1$883h{=iVK@HMoz9Cr!&t zHY&{NZa9ZFuX=cY^`>H-N%l_vkVAvm)W_Uo`moL4w#5GuhRFW`hI*Hok?HN}=Sj-# zo2fZ>v9C7hKw{W&E}DO(qbLa&3(zALGhFUW_S5c47ac^bPmr{=H6%Xl099C!;Aiiy zWR{+%-Q81Z)V8}LxrI?#5}KUzu-fNp8{XS4ugex8rH1bgaSpHKW+OJ4b+lyq{@Nzp zmC4LB^o?rIxXzR%Hh`U5%lV-dBLQKae!=Z=Rphwm8wH46?+>&dS*)LMYzS2cWBEu} zaL3#-1pfr%C@O^`1mpSQAV~|XJ z(etiB{fC^UZ-)!AFw0cK(NDQ;zI(YeVojp@x{^1MK7qIXt8b?eTf!2=NN%wPE&Bi+ie#W$}X zWBn?rG+)+MIH?T`IY;`zi?k8IVTTrf%I@{EB$f6ZEA#IoZYyS&s1EFl%C)z9dh_Rx zsw%!;I`GFf$S+)iHoUe7?5y7UAHH9=Bz+%5)2sZts3mOuYAMy0iEphS>*ai>?&HA{ z`@a&7=9Mv_;=d5$xhaa4mo$-gZ2v;cOmO8YYW_2Ejp+fs|~Rc3Er_rI=g^U9(1Ly}CtsX|}@W{+&ffnVwT&CUN{GZMN>{r+mW;Tzx?xxcL$ zEe<+drZKv2m;bh^y>$|)c}Z0ZO?X}^9qo$w7=LEZ=$zGeLu$55?9iMPMR6we7P0(% zj=OJunXd{d$0xBeKkAb$szBi-qnWni;%EN_*Q!n5)dK@X||Gu}~=>MrV>0WLudA~l_qqLI> z%*RZhr=A#*v-&z?#{V7T3+mQr;jOo>N4sNMY$rMAVE%kwbH*m4C*i#WqCCg;QxTr|>Hv9H#%w-czIS3ts;($36_i4?`_ z_BVLg`iDQ^(~_`XebU4)=9|y5@aeh6QFB%k{toSM)7n;Dw4RL1ctWIn!DE`6S4zd^ z^Q5{x)Q=K@q`;T&U)M9St+I75q-EN0VocDK^t?H6fjr{N1J5uBKf8PA9U}R1YI7F2^XR@0>b=v`5#G)_yZ{Q>rcS(2jh2Hji0g|>4sk)PEqF`BKhQID{n{McB7;j z7`b1h$%QR%8~;#RV*)k=FBU6Q3sS>M8Q?N`cu_n2Z?X$U9=tNF`tLH0O~3>cUilwn zKbgPOxG;2y;wRZ9;~xxN0{9c>cS9G9zoRXzy^&S8sct66z_E^lvh)q%zF+0NI};R5 zYd>jnMOoX5LG*@aokkBKHYCA0HIQ5bil?JGF#un?2+jb*ix2}@z}Th61>;iolk%m8 z=0$DxzbR{_&3B6_2ngwK;Smut0FTK^YHB%G z49;yM^QgPTHlvuCKdNkz3dmw~lD>Ro77?Dr)}~<5QJP0LD#~Zfc_GSlk4VT|K#Ym} zWAW!>uC3rl&7!aF%)hDyp+wxTh{u`4K9z*Y;XUs*|#LXt!wvxK_yv-BP936xjauNuqWw~IUJ9p^Y-vgM+aX7W|s?~O6< zf5)+_zCbEiuqTx;wN+U=v$&2-A~rGhDf`5Yt6Y!8!bdNXc{o9>^d_3-)pP@?yoL%o z5!^0Z%fUb{aecdGE(U`yhGGKag>3w@>8@0tTA0Y$#pXq2fqD2J+}wB=XPYmR*Azd8 zy>-vfJ)};4vA9|Cy85Hk&Ce#obHaDH=^wtixrC$OJ&zvvo==*&kJfQU=JotRpia#I z5;g7DRuuVLbAH_;s12K;306%WehuE9cK2_IKE&Sr66s$UpE!uAG}T-o8*#MN^Tg6; zWWd@c@dJ@3*L^+HG|Lms#4*d(ec$1Z5vp6==p#>}1{MTXo`$x3NKQ^p?;Saym%m4* zH=8v?-&#@ZAl+bJudlB!sxuklB2aHRP`l2pk|$*!Y3M@T0~dCBaJ|4;tTUU;(Im%D zq64L-rYSMuKJHUdv$TDPIx*t);q&o)+9m~IzgAY#0WNW4wOOXXtT==(zFQI5|D;3y zNe7v|C`P=n#qyQuhB5-aq4AHIE5pY+7Gyjubp0}Mx$hLuLK;HQ68i7Z&fG3FXo)yo z=m{wSZm`+s(**8Ui#>`6h>eZ?k*fo;Vo+mRF%bTrh+M}LM*_A##VhFxa~dK5QEWpt?j;%Ff1Ia zbvn+s;lS{ifyt*MtcqBunl*McI;+)!32dVW(;bj@}%5!NGgUn#)(QZXm(?=ab{Kq z{N^J|L`9P_4lnSnXcRP-1#D^f)JO?JWKrI@FJzGmU-GtRGwuO~^%r_mtOP)jz;iKy zLX%a>4St`h>>DgUx^xAz^5mgSR+8JrXMyjrke73Q#K}vs@1d{b44$l{bm{!HnC(B9 z$y!knoT61Yh?3jQSn3<7Fk9PplbdkUu~e>D+$+v*DTv=cT>66^gyacZW@o=mbS3JN zf1Hb!VJuS>x9Et77b>_j#iB|}C4>XanTW7?QM=f*REOjVqfN|FRrll0{Rb{g(Ep9T6H6OM(l zm;nl`0{uCmhKo(Ws={ArbpzA8uE)EslOix_xF}we_J6t2RIhMGNpPS z9>)oxVq{8n15fxI5V7A|Ml2;YxOuAWo?UE9X9e?7=)grHRQX>=)#)v7oOWfF?jnAz`F%6#`}sQw*zO}G%?iEz%Yq>@}3Vg__e zDlTk%xAv29xS^8S6==exjZ14k85d!dQ1U1kxG?#LjbDTKP+NDTo-xo8Pf=Bop*y(4T``(;~`#7M ze_I}@AJKky@V1^21&y4}Z+PuCH0&XT5c3Wme9u$Z2wI-$l?r)tWLkXW7b~%J0$lSE z0((pmb@a=V<0sz@?8=|cm`1e+_@Ot|FjkUg`0--UUY!egK6kDF{3yPGUpL<@5jbNI zGmiQ0bz%^v|HWDPuOzP@`F87R%r^w#4~&jKTe7vMk058F!VUQ7v?ZbRlH_BE^H<~9 z+DWmfq0h**S(CocJh|#{7WQ8>+t+*0=1pVTx7MF=ZyP#Lzg_hR5GXYL@J|}H7aG4c zJ9lBW<}YTY;bwLGz3PjP%(;C7m#=W`Y@v^xv^@Hd(4pF)qOWv^Scy@RO-mNlS_KJq>_Q{W(eyZQ7Ha0Azttv4y? zVFe}CJj3VsqM=F zal^>Xyn5u{(0%Sd_$XK+xUOabSF+ztVQPEwEDVu-C`GbpoE|If&-%jkhU*u~xbc-# za}sfKjTWH5FpyQ&f|nk8W>5o@VRO!@h%rEmEB%H5`Hk00G(Y*43GP%2>ok{o(4<}i z(`J#2#Ezll&7T=}aK|ljxbiEdwNn2hW?~Xbda{UMCe{NqgCd?J5;02rDgvSN&WEBQ zBXtOMztE7X6{~Lu?Z0h~kw2T`7uZNX8B%M8{)TDNf2SG0a4vM@$1mc%Wo^fq@B#C6 zjMT_iakBkqR(jNQT!dqbzc~0!|43O|@QsBcEPq2_2ymytdSJL$f)i(GonH67H{Sie zXD+4el+tD-wQr11!$=C_J$YAAMc=Gmu}-h77R)LwcXig?_M`Lbf0$e+B&JWdJ+-G? z9qnSI+?TBS!jngu+&eTc1I?=0q5 zC^bqN-?|}Tai8T8aBczWUn~bKmxrJlc3Onf{&QJOEia`ejp}LiQiBmyE!>KkQPE;;98qEGbNYK4 zUdq=FP5{(Ft)U;!q(};1=F^&;FjnGK(VUxeYTvwaoCb7p{_ZpHoL8NrBp-pFKo~{ZMyNPnR2t#x9%bmiz)e@Dgp!H0{6k zEv!c4sB=d6i|tspOQO%y^P*9MOy|KD@;jIYK(qMOjRP3DsZ?jy@M>g%hQuAGjt)vZ z+`~7|+Q#ku{7Jn=IR3>V^_E;=g@y?CamHyFdTJSK_w-=_#6B$ijRYK|)W~(E7pM;# z-V`56L`y8+=u@prMCup(hEUn1QQv9&dZYk?=3KTx>&~>LJp-NEP=$@kFEDz~jKUO8 z^J(wAh5Di!+&BpNs*qIeT6-14)SaWs8k6i)Jer&0tEm3uT$WVg%Az5tp; z4C``#5-qmVHn{?a7)zBm_4w7~Eaa36g_$DoK$hUTd_AjSFQPYz%yl`>m`R7+Q`C+rPjp5vFlDt^Fetg7TO8(S6Il<0 zvZ`i`c>f^^L5lsTwxsrer5HRhkgu91QFv5ZinI4cteuaLp1|<=9_wphP}L=Ils@9i zNhHl${-A>T;tB@6)|&@%AW!sA=RQ5*v(wGNiJFc6l5+|o6W;V1R=bN)0Q_RnC#EfZ zM6Ep_yn}x%ck^Nlpi%{#=_@_sCV&CJe~0}FNMroF<%;YA`MM^(q9TI-VAO-Z*SR8K z-LClSq680go_ZqcWn8{nCd14(1o4sVX`VgJaL?90FxLXEAX{C;|L<68_jXhG*T5Q% zWL`zGFU^+nNOnS!;|)7#tkM4Oh(-n99skn4?XNEXmsr5{lHxs?MCP4HVmEJ3zir^v z>TL*Gc$aq-W1^v51#T<`ZYu_+EJOhAD&{4mgU?nN6C00=$8bx-%=uB};1Cgypo^

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 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 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 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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 6be8902a80d9d1aaf8594cdf442bf2e9312de509 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Thu, 21 Oct 2021 18:07:01 +0100 Subject: [PATCH 275/472] 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 276/472] 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 277/472] 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 278/472] 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 279/472] 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 280/472] 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 281/472] 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 282/472] 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 283/472] 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 284/472] 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 285/472] 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 286/472] 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 287/472] 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 288/472] 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 289/472] 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 290/472] 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 291/472] 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 292/472] 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 293/472] 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 294/472] 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 295/472] 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 296/472] 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 297/472] 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 298/472] 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 299/472] 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 300/472] 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 301/472] 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 302/472] 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 303/472] 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 304/472] 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 305/472] 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 306/472] 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 307/472] 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 308/472] 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 309/472] 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 310/472] 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 311/472] 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 312/472] 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 313/472] 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 314/472] 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 315/472] 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 316/472] 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 317/472] 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 318/472] 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 319/472] 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 320/472] 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 321/472] 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 322/472] 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 323/472] 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 324/472] 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 325/472] 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 326/472] 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 327/472] 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 328/472] 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 329/472] 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 330/472] 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 331/472] 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 332/472] 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 333/472] 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 334/472] 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 335/472] 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 336/472] 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 337/472] 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 338/472] 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 339/472] 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 340/472] 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 341/472] 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 342/472] 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 343/472] 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 344/472] 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 345/472] 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 346/472] 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 347/472] 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 348/472] 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 349/472] 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 350/472] 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 351/472] 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 352/472] 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 353/472] 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 354/472] 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 355/472] 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 356/472] 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 357/472] 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 358/472] 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 359/472] 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 360/472] 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 361/472] 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 362/472] 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 363/472] 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 364/472] 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 365/472] 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 366/472] 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 367/472] 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 368/472] 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 369/472] 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 370/472] 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 371/472] 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 372/472] 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 373/472] 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 374/472] 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 375/472] 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 376/472] 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 377/472] 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 378/472] 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 379/472] 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 380/472] 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 381/472] 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 382/472] 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 383/472] 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 384/472] 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 385/472] 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 386/472] 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 387/472] 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 388/472] 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 389/472] 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 390/472] 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 391/472] 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 392/472] 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 393/472] 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 394/472] 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 395/472] 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 396/472] 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 397/472] 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 398/472] 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 399/472] 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 400/472] 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 401/472] 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 402/472] 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 403/472] 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 404/472] =?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 405/472] 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 406/472] 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 407/472] 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 408/472] 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 409/472] 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 410/472] 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 411/472] 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 412/472] 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 413/472] 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 414/472] 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 415/472] 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 416/472] 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 417/472] 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 418/472] 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 419/472] 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 420/472] 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 421/472] 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 422/472] 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 423/472] 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 424/472] 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 425/472] 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 426/472] 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 427/472] 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 428/472] 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 429/472] 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 430/472] 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 431/472] 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 432/472] 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 433/472] 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 434/472] 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 435/472] 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 436/472] 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 437/472] 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 438/472] 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 439/472] 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 440/472] 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 441/472] 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 442/472] 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 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 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 443/472] 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 444/472] 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 = """