|
| 1 | +# iorw_to.py Emulate a device which can read and write one character at a time |
| 2 | +# and test timeouts. |
| 3 | + |
| 4 | +# Copyright (c) Peter Hinch 2019 |
| 5 | +# Released under the MIT licence |
| 6 | + |
| 7 | +# This requires the modified version of uasyncio (fast_io directory). |
| 8 | +# Slow hardware is emulated using timers. |
| 9 | +# MyIO.write() ouputs a single character and sets the hardware not ready. |
| 10 | +# MyIO.readline() returns a single character and sets the hardware not ready. |
| 11 | +# Timers asynchronously set the hardware ready. |
| 12 | + |
| 13 | +import io, pyb |
| 14 | +import uasyncio as asyncio |
| 15 | +import micropython |
| 16 | +import sys |
| 17 | +try: |
| 18 | + print('Uasyncio version', asyncio.version) |
| 19 | + if not isinstance(asyncio.version, tuple): |
| 20 | + print('Please use fast_io version 0.24 or later.') |
| 21 | + sys.exit(0) |
| 22 | +except AttributeError: |
| 23 | + print('ERROR: This test requires the fast_io version. It will not run correctly') |
| 24 | + print('under official uasyncio V2.0 owing to a bug which prevents concurrent') |
| 25 | + print('input and output.') |
| 26 | + sys.exit(0) |
| 27 | + |
| 28 | +print('Issue iorw_to.test(True) to test ioq, iorw_to.test() to test runq.') |
| 29 | +print('Test runs until interrupted. Tasks time out after 15s.') |
| 30 | +print('Issue ctrl-d after each run.') |
| 31 | + |
| 32 | +micropython.alloc_emergency_exception_buf(100) |
| 33 | + |
| 34 | +MP_STREAM_POLL_RD = const(1) |
| 35 | +MP_STREAM_POLL_WR = const(4) |
| 36 | +MP_STREAM_POLL = const(3) |
| 37 | +MP_STREAM_ERROR = const(-1) |
| 38 | + |
| 39 | +def printbuf(this_io): |
| 40 | + print(bytes(this_io.wbuf[:this_io.wprint_len]).decode(), end='') |
| 41 | + |
| 42 | +class MyIO(io.IOBase): |
| 43 | + def __init__(self, read=False, write=False): |
| 44 | + self.ready_rd = False # Read and write not ready |
| 45 | + self.rbuf = b'ready\n' # Read buffer |
| 46 | + self.ridx = 0 |
| 47 | + pyb.Timer(4, freq = 5, callback = self.do_input) |
| 48 | + self.wch = b'' |
| 49 | + self.wbuf = bytearray(100) # Write buffer |
| 50 | + self.wprint_len = 0 |
| 51 | + self.widx = 0 |
| 52 | + pyb.Timer(5, freq = 10, callback = self.do_output) |
| 53 | + |
| 54 | + # Read callback: emulate asynchronous input from hardware. |
| 55 | + # Typically would put bytes into a ring buffer and set .ready_rd. |
| 56 | + def do_input(self, t): |
| 57 | + self.ready_rd = True # Data is ready to read |
| 58 | + |
| 59 | + # Write timer callback. Emulate hardware: if there's data in the buffer |
| 60 | + # write some or all of it |
| 61 | + def do_output(self, t): |
| 62 | + if self.wch: |
| 63 | + self.wbuf[self.widx] = self.wch |
| 64 | + self.widx += 1 |
| 65 | + if self.wch == ord('\n'): |
| 66 | + self.wprint_len = self.widx # Save for schedule |
| 67 | + micropython.schedule(printbuf, self) |
| 68 | + self.widx = 0 |
| 69 | + self.wch = b'' |
| 70 | + |
| 71 | + |
| 72 | + def ioctl(self, req, arg): # see ports/stm32/uart.c |
| 73 | + ret = MP_STREAM_ERROR |
| 74 | + if req == MP_STREAM_POLL: |
| 75 | + ret = 0 |
| 76 | + if arg & MP_STREAM_POLL_RD: |
| 77 | + if self.ready_rd: |
| 78 | + ret |= MP_STREAM_POLL_RD |
| 79 | + if arg & MP_STREAM_POLL_WR: |
| 80 | + if not self.wch: |
| 81 | + ret |= MP_STREAM_POLL_WR # Ready if no char pending |
| 82 | + return ret |
| 83 | + |
| 84 | + # Test of device that produces one character at a time |
| 85 | + def readline(self): |
| 86 | + self.ready_rd = False # Cleared by timer cb do_input |
| 87 | + ch = self.rbuf[self.ridx] |
| 88 | + if ch == ord('\n'): |
| 89 | + self.ridx = 0 |
| 90 | + else: |
| 91 | + self.ridx += 1 |
| 92 | + return chr(ch) |
| 93 | + |
| 94 | + # Emulate unbuffered hardware which writes one character: uasyncio waits |
| 95 | + # until hardware is ready for the next. Hardware ready is emulated by write |
| 96 | + # timer callback. |
| 97 | + def write(self, buf, off, sz): |
| 98 | + self.wch = buf[off] # Hardware starts to write a char |
| 99 | + return 1 # 1 byte written. uasyncio waits on ioctl write ready |
| 100 | + |
| 101 | +# Note that trapping the exception and returning is still mandatory. |
| 102 | +async def receiver(myior): |
| 103 | + sreader = asyncio.StreamReader(myior) |
| 104 | + try: |
| 105 | + while True: |
| 106 | + res = await sreader.readline() |
| 107 | + print('Received', res) |
| 108 | + except asyncio.TimeoutError: |
| 109 | + print('Receiver timeout') |
| 110 | + |
| 111 | +async def sender(myiow): |
| 112 | + swriter = asyncio.StreamWriter(myiow, {}) |
| 113 | + await asyncio.sleep(1) |
| 114 | + count = 0 |
| 115 | + try: # Trap in outermost scope to catch cancellation of .sleep |
| 116 | + while True: |
| 117 | + count += 1 |
| 118 | + tosend = 'Wrote Hello MyIO {}\n'.format(count) |
| 119 | + await swriter.awrite(tosend.encode('UTF8')) |
| 120 | + await asyncio.sleep(2) |
| 121 | + except asyncio.TimeoutError: |
| 122 | + print('Sender timeout') |
| 123 | + |
| 124 | +async def run(coro, t): |
| 125 | + await asyncio.wait_for_ms(coro, t) |
| 126 | + |
| 127 | +async def do_test(loop, t): |
| 128 | + myio = MyIO() |
| 129 | + while True: |
| 130 | + tr = t * 1000 + (pyb.rng() >> 20) # Add ~1s uncertainty |
| 131 | + tw = t * 1000 + (pyb.rng() >> 20) |
| 132 | + print('Timeouts: {:7.3f}s read {:7.3f}s write'.format(tr/1000, tw/1000)) |
| 133 | + loop.create_task(run(receiver(myio), tr)) |
| 134 | + await run(sender(myio), tw) |
| 135 | + await asyncio.sleep(2) # Wait out timing randomness |
| 136 | + |
| 137 | +def test(ioq=False): |
| 138 | + if ioq: |
| 139 | + loop = asyncio.get_event_loop(ioq_len=16) |
| 140 | + else: |
| 141 | + loop = asyncio.get_event_loop() |
| 142 | + loop.create_task(do_test(loop, 15)) |
| 143 | + loop.run_forever() |
0 commit comments