2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2017-01-02 14:45:56 +00:00
|
|
|
Implements an event loop with cooperative multitasking and async I/O. Tasks in
|
|
|
|
the form of python coroutines (either plain generators or `async` functions) are
|
|
|
|
stepped through until completion, and can get asynchronously blocked by
|
|
|
|
`yield`ing or `await`ing a syscall.
|
2016-04-28 05:43:37 +00:00
|
|
|
|
2018-04-13 12:57:04 +00:00
|
|
|
See `schedule`, `run`, and syscalls `sleep`, `wait`, `signal` and `spawn`.
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2016-04-29 19:48:59 +00:00
|
|
|
|
2017-01-02 14:45:56 +00:00
|
|
|
import utime
|
|
|
|
import utimeq
|
|
|
|
from micropython import const
|
2018-07-03 14:20:26 +00:00
|
|
|
|
|
|
|
from trezor import io, log
|
2016-05-12 14:18:40 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
if False:
|
|
|
|
from typing import (
|
|
|
|
Any,
|
|
|
|
Awaitable,
|
|
|
|
Callable,
|
|
|
|
Coroutine,
|
|
|
|
Dict,
|
|
|
|
Generator,
|
|
|
|
List,
|
|
|
|
Optional,
|
|
|
|
Set,
|
|
|
|
)
|
|
|
|
|
|
|
|
Task = Coroutine
|
|
|
|
Finalizer = Callable[[Task, Any], None]
|
|
|
|
|
|
|
|
# function to call after every task step
|
|
|
|
after_step_hook = None # type: Optional[Callable[[], None]]
|
|
|
|
|
|
|
|
# tasks scheduled for execution in the future
|
|
|
|
_queue = utimeq.utimeq(64)
|
|
|
|
|
|
|
|
# tasks paused on I/O
|
|
|
|
_paused = {} # type: Dict[int, Set[Task]]
|
|
|
|
|
|
|
|
# functions to execute after a task is finished
|
|
|
|
_finalizers = {} # type: Dict[int, Finalizer]
|
2017-01-02 14:45:56 +00:00
|
|
|
|
|
|
|
if __debug__:
|
|
|
|
# for performance stats
|
|
|
|
import array
|
2018-07-03 14:20:58 +00:00
|
|
|
|
2017-01-02 14:45:56 +00:00
|
|
|
log_delay_pos = 0
|
|
|
|
log_delay_rb_len = const(10)
|
2018-07-03 14:20:58 +00:00
|
|
|
log_delay_rb = array.array("i", [0] * log_delay_rb_len)
|
2017-01-02 14:45:56 +00:00
|
|
|
|
2016-08-05 10:32:37 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def schedule(
|
|
|
|
task: Task, value: Any = None, deadline: int = None, finalizer: Finalizer = None
|
|
|
|
) -> None:
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2017-01-02 14:45:56 +00:00
|
|
|
Schedule task to be executed with `value` on given `deadline` (in
|
2017-08-16 13:02:03 +00:00
|
|
|
microseconds). Does not start the event loop itself, see `run`.
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2016-11-12 11:22:05 +00:00
|
|
|
if deadline is None:
|
|
|
|
deadline = utime.ticks_us()
|
2019-05-13 13:06:34 +00:00
|
|
|
if finalizer is not None:
|
|
|
|
_finalizers[id(task)] = finalizer
|
2017-09-16 13:00:31 +00:00
|
|
|
_queue.push(deadline, task, value)
|
2016-08-05 10:32:37 +00:00
|
|
|
|
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def pause(task: Task, iface: int) -> None:
|
2017-10-02 14:18:27 +00:00
|
|
|
tasks = _paused.get(iface, None)
|
2017-01-02 14:45:56 +00:00
|
|
|
if tasks is None:
|
2018-01-22 12:07:12 +00:00
|
|
|
tasks = _paused[iface] = set()
|
|
|
|
tasks.add(task)
|
2016-05-16 15:10:12 +00:00
|
|
|
|
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def finalize(task: Task, value: Any) -> None:
|
2019-05-13 13:06:34 +00:00
|
|
|
fn = _finalizers.pop(id(task), None)
|
|
|
|
if fn is not None:
|
|
|
|
fn(task, value)
|
|
|
|
|
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def close(task: Task) -> None:
|
2018-01-22 12:07:12 +00:00
|
|
|
for iface in _paused:
|
|
|
|
_paused[iface].discard(task)
|
|
|
|
_queue.discard(task)
|
2017-10-10 13:32:43 +00:00
|
|
|
task.close()
|
2019-05-13 13:06:34 +00:00
|
|
|
finalize(task, GeneratorExit())
|
2017-10-10 13:32:43 +00:00
|
|
|
|
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def run() -> None:
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2017-01-02 14:45:56 +00:00
|
|
|
Loop forever, stepping through scheduled tasks and awaiting I/O events
|
2017-09-16 13:00:31 +00:00
|
|
|
inbetween. Use `schedule` first to add a coroutine to the task queue.
|
2017-01-02 14:45:56 +00:00
|
|
|
Tasks yield back to the scheduler on any I/O, usually by calling `await` on
|
|
|
|
a `Syscall`.
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2016-05-16 15:10:12 +00:00
|
|
|
|
2017-01-02 14:45:56 +00:00
|
|
|
if __debug__:
|
|
|
|
global log_delay_pos
|
2016-08-05 10:32:37 +00:00
|
|
|
|
2017-09-16 13:00:31 +00:00
|
|
|
max_delay = const(1000000) # usec delay if queue is empty
|
|
|
|
|
2017-01-02 14:45:56 +00:00
|
|
|
task_entry = [0, 0, 0] # deadline, task, value
|
2017-06-26 14:03:20 +00:00
|
|
|
msg_entry = [0, 0] # iface | flags, value
|
2017-10-24 11:59:09 +00:00
|
|
|
while _queue or _paused:
|
2017-01-02 14:45:56 +00:00
|
|
|
# compute the maximum amount of time we can wait for a message
|
2017-09-16 13:00:31 +00:00
|
|
|
if _queue:
|
|
|
|
delay = utime.ticks_diff(_queue.peektime(), utime.ticks_us())
|
2017-01-02 14:45:56 +00:00
|
|
|
else:
|
2017-09-16 13:00:31 +00:00
|
|
|
delay = max_delay
|
2017-01-02 14:45:56 +00:00
|
|
|
|
|
|
|
if __debug__:
|
|
|
|
# add current delay to ring buffer for performance stats
|
|
|
|
log_delay_rb[log_delay_pos] = delay
|
|
|
|
log_delay_pos = (log_delay_pos + 1) % log_delay_rb_len
|
|
|
|
|
2017-09-16 13:00:31 +00:00
|
|
|
if io.poll(_paused, msg_entry, delay):
|
2017-01-02 14:45:56 +00:00
|
|
|
# message received, run tasks paused on the interface
|
2017-10-02 14:18:27 +00:00
|
|
|
msg_tasks = _paused.pop(msg_entry[0], ())
|
|
|
|
for task in msg_tasks:
|
|
|
|
_step(task, msg_entry[1])
|
2017-01-02 14:45:56 +00:00
|
|
|
else:
|
|
|
|
# timeout occurred, run the first scheduled task
|
2017-09-16 13:00:31 +00:00
|
|
|
if _queue:
|
|
|
|
_queue.pop(task_entry)
|
2019-07-03 13:07:04 +00:00
|
|
|
_step(task_entry[1], task_entry[2]) # type: ignore
|
|
|
|
# error: Argument 1 to "_step" has incompatible type "int"; expected "Coroutine[Any, Any, Any]"
|
|
|
|
# rationale: We use untyped lists here, because that is what the C API supports.
|
2017-01-02 14:45:56 +00:00
|
|
|
|
|
|
|
|
2019-07-29 14:34:30 +00:00
|
|
|
def clear() -> None:
|
2019-07-11 14:52:25 +00:00
|
|
|
"""Clear all queue state. Any scheduled or paused tasks will be forgotten."""
|
|
|
|
_ = [0, 0, 0]
|
|
|
|
while _queue:
|
|
|
|
_queue.pop(_)
|
|
|
|
_paused.clear()
|
|
|
|
_finalizers.clear()
|
|
|
|
|
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def _step(task: Task, value: Any) -> None:
|
2016-08-05 10:32:37 +00:00
|
|
|
try:
|
2019-05-13 13:06:34 +00:00
|
|
|
if isinstance(value, BaseException):
|
2019-07-03 13:07:04 +00:00
|
|
|
result = task.throw(value) # type: ignore
|
|
|
|
# error: Argument 1 to "throw" of "Coroutine" has incompatible type "Exception"; expected "Type[BaseException]"
|
|
|
|
# rationale: In micropython, generator.throw() accepts the exception object directly.
|
2016-08-05 10:32:37 +00:00
|
|
|
else:
|
|
|
|
result = task.send(value)
|
2019-05-13 13:06:34 +00:00
|
|
|
except StopIteration as e: # as e:
|
2017-08-21 11:22:35 +00:00
|
|
|
if __debug__:
|
2018-07-03 14:20:58 +00:00
|
|
|
log.debug(__name__, "finish: %s", task)
|
2019-05-13 13:06:34 +00:00
|
|
|
finalize(task, e.value)
|
2016-08-05 10:32:37 +00:00
|
|
|
except Exception as e:
|
2017-08-21 11:22:35 +00:00
|
|
|
if __debug__:
|
|
|
|
log.exception(__name__, e)
|
2019-05-13 13:06:34 +00:00
|
|
|
finalize(task, e)
|
2016-06-17 18:52:36 +00:00
|
|
|
else:
|
2016-08-05 10:32:37 +00:00
|
|
|
if isinstance(result, Syscall):
|
|
|
|
result.handle(task)
|
|
|
|
elif result is None:
|
2017-09-16 13:00:31 +00:00
|
|
|
schedule(task)
|
2016-08-05 10:32:37 +00:00
|
|
|
else:
|
2017-08-21 11:22:35 +00:00
|
|
|
if __debug__:
|
2018-07-03 14:20:58 +00:00
|
|
|
log.error(__name__, "unknown syscall: %s", result)
|
2017-01-02 14:45:56 +00:00
|
|
|
if after_step_hook:
|
|
|
|
after_step_hook()
|
2016-05-12 14:18:40 +00:00
|
|
|
|
|
|
|
|
2017-01-02 14:45:56 +00:00
|
|
|
class Syscall:
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2017-01-02 14:45:56 +00:00
|
|
|
When tasks want to perform any I/O, or do any sort of communication with the
|
|
|
|
scheduler, they do so through instances of a class derived from `Syscall`.
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2016-08-05 10:32:37 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def __iter__(self) -> Task: # type: ignore
|
2017-01-02 14:45:56 +00:00
|
|
|
# support `yield from` or `await` on syscalls
|
2016-08-05 10:32:37 +00:00
|
|
|
return (yield self)
|
2016-05-30 14:15:34 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def __await__(self) -> Generator:
|
|
|
|
return self.__iter__() # type: ignore
|
|
|
|
|
|
|
|
def handle(self, task: Task) -> None:
|
|
|
|
pass
|
|
|
|
|
2016-05-30 14:15:34 +00:00
|
|
|
|
2017-09-16 13:00:31 +00:00
|
|
|
class sleep(Syscall):
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2017-01-02 14:45:56 +00:00
|
|
|
Pause current task and resume it after given delay. Although the delay is
|
|
|
|
given in microseconds, sub-millisecond precision is not guaranteed. Result
|
|
|
|
value is the calculated deadline.
|
|
|
|
|
|
|
|
Example:
|
2017-09-16 13:00:31 +00:00
|
|
|
|
|
|
|
>>> planned = await loop.sleep(1000 * 1000) # sleep for 1ms
|
|
|
|
>>> print('missed by %d us', utime.ticks_diff(utime.ticks_us(), planned))
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2016-05-12 14:18:40 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def __init__(self, delay_us: int) -> None:
|
2017-02-09 12:49:10 +00:00
|
|
|
self.delay_us = delay_us
|
2016-05-12 14:18:40 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def handle(self, task: Task) -> None:
|
2017-02-09 12:49:10 +00:00
|
|
|
deadline = utime.ticks_add(utime.ticks_us(), self.delay_us)
|
2017-09-16 13:00:31 +00:00
|
|
|
schedule(task, deadline, deadline)
|
2016-05-30 14:15:34 +00:00
|
|
|
|
2016-05-12 14:18:40 +00:00
|
|
|
|
2018-04-13 12:57:04 +00:00
|
|
|
class wait(Syscall):
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2017-01-02 14:45:56 +00:00
|
|
|
Pause current task, and resume only after a message on `msg_iface` is
|
|
|
|
received. Messages are received either from an USB interface, or the
|
|
|
|
touch display. Result value a tuple of message values.
|
2016-05-12 14:18:40 +00:00
|
|
|
|
2017-01-02 14:45:56 +00:00
|
|
|
Example:
|
2017-09-16 13:00:31 +00:00
|
|
|
|
2018-04-13 12:57:04 +00:00
|
|
|
>>> hid_report, = await loop.wait(0xABCD) # await USB HID report
|
|
|
|
>>> event, x, y = await loop.wait(io.TOUCH) # await touch event
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2017-01-02 14:45:56 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def __init__(self, msg_iface: int) -> None:
|
2017-01-02 14:45:56 +00:00
|
|
|
self.msg_iface = msg_iface
|
2016-04-29 23:20:57 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def handle(self, task: Task) -> None:
|
2017-09-16 13:00:31 +00:00
|
|
|
pause(task, self.msg_iface)
|
2016-08-05 10:32:37 +00:00
|
|
|
|
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
_NO_VALUE = object()
|
2016-08-05 10:32:37 +00:00
|
|
|
|
|
|
|
|
2017-09-16 13:00:31 +00:00
|
|
|
class signal(Syscall):
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2017-01-02 14:45:56 +00:00
|
|
|
Pause current task, and let other running task to resume it later with a
|
|
|
|
result value or an exception.
|
|
|
|
|
|
|
|
Example:
|
2017-09-16 13:00:31 +00:00
|
|
|
|
|
|
|
>>> # in task #1:
|
|
|
|
>>> signal = loop.signal()
|
|
|
|
>>> result = await signal
|
|
|
|
>>> print('awaited result:', result)
|
|
|
|
>>> # in task #2:
|
|
|
|
>>> signal.send('hello from task #2')
|
|
|
|
>>> # prints in the next iteration of the event loop
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2016-08-05 10:32:37 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def __init__(self) -> None:
|
2019-03-05 12:49:42 +00:00
|
|
|
self.reset()
|
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def reset(self) -> None:
|
2017-01-02 14:45:56 +00:00
|
|
|
self.value = _NO_VALUE
|
2019-07-03 13:07:04 +00:00
|
|
|
self.task = None # type: Optional[Task]
|
2016-08-05 10:32:37 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def handle(self, task: Task) -> None:
|
2016-08-05 10:32:37 +00:00
|
|
|
self.task = task
|
2016-11-15 12:47:36 +00:00
|
|
|
self._deliver()
|
2016-08-05 10:32:37 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def send(self, value: Any) -> None:
|
2016-11-15 12:47:36 +00:00
|
|
|
self.value = value
|
|
|
|
self._deliver()
|
2016-08-05 10:32:37 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def _deliver(self) -> None:
|
2017-01-02 14:45:56 +00:00
|
|
|
if self.task is not None and self.value is not _NO_VALUE:
|
2017-09-16 13:00:31 +00:00
|
|
|
schedule(self.task, self.value)
|
2016-11-15 12:47:36 +00:00
|
|
|
self.task = None
|
2017-01-02 14:45:56 +00:00
|
|
|
self.value = _NO_VALUE
|
2016-05-16 15:10:12 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def __iter__(self) -> Task: # type: ignore
|
2018-03-20 15:22:43 +00:00
|
|
|
try:
|
|
|
|
return (yield self)
|
2018-04-03 23:22:40 +00:00
|
|
|
except: # noqa: E722
|
2018-03-20 15:22:43 +00:00
|
|
|
self.task = None
|
|
|
|
raise
|
|
|
|
|
2016-05-02 14:22:22 +00:00
|
|
|
|
2019-05-13 13:06:34 +00:00
|
|
|
_type_gen = type((lambda: (yield))())
|
|
|
|
|
|
|
|
|
2018-04-13 12:57:04 +00:00
|
|
|
class spawn(Syscall):
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2018-06-14 13:03:09 +00:00
|
|
|
Execute one or more children tasks and wait until one of them exits.
|
2018-04-13 12:57:04 +00:00
|
|
|
Return value of `spawn` is the return value of task that triggered the
|
|
|
|
completion. By default, `spawn` returns after the first child completes, and
|
2017-01-02 14:45:56 +00:00
|
|
|
other running children are killed (by cancelling any pending schedules and
|
|
|
|
calling `close()`).
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
2017-09-16 13:00:31 +00:00
|
|
|
>>> # async def wait_for_touch(): ...
|
|
|
|
>>> # async def animate_logo(): ...
|
|
|
|
>>> touch_task = wait_for_touch()
|
|
|
|
>>> animation_task = animate_logo()
|
2018-04-13 12:57:04 +00:00
|
|
|
>>> waiter = loop.spawn(touch_task, animation_task)
|
2017-09-16 13:00:31 +00:00
|
|
|
>>> result = await waiter
|
|
|
|
>>> if animation_task in waiter.finished:
|
|
|
|
>>> print('animation task returned', result)
|
|
|
|
>>> else:
|
|
|
|
>>> print('touch task returned', result)
|
|
|
|
|
2018-04-13 12:57:04 +00:00
|
|
|
Note: You should not directly `yield` a `spawn` instance, see logic in
|
|
|
|
`spawn.__iter__` for explanation. Always use `await`.
|
2018-07-03 14:20:58 +00:00
|
|
|
"""
|
2016-05-11 12:39:57 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def __init__(self, *children: Awaitable, exit_others: bool = True) -> None:
|
2016-08-05 10:32:37 +00:00
|
|
|
self.children = children
|
2016-05-02 14:06:08 +00:00
|
|
|
self.exit_others = exit_others
|
2019-07-03 13:07:04 +00:00
|
|
|
self.finished = [] # type: List[Awaitable] # children that finished
|
|
|
|
self.scheduled = [] # type: List[Task] # scheduled wrapper tasks
|
2016-05-02 14:06:08 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def handle(self, task: Task) -> None:
|
2019-05-13 13:06:34 +00:00
|
|
|
finalizer = self._finish
|
|
|
|
scheduled = self.scheduled
|
|
|
|
finished = self.finished
|
2016-04-29 23:20:57 +00:00
|
|
|
|
2019-05-13 13:06:34 +00:00
|
|
|
self.callback = task
|
|
|
|
scheduled.clear()
|
|
|
|
finished.clear()
|
|
|
|
|
|
|
|
for child in self.children:
|
|
|
|
if isinstance(child, _type_gen):
|
|
|
|
child_task = child
|
|
|
|
else:
|
2019-07-03 13:07:04 +00:00
|
|
|
child_task = iter(child) # type: ignore
|
|
|
|
schedule(child_task, None, None, finalizer) # type: ignore
|
|
|
|
scheduled.append(child_task) # type: ignore
|
|
|
|
# TODO: document the types here
|
2019-05-13 13:06:34 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def exit(self, except_for: Task = None) -> None:
|
2019-05-13 13:06:34 +00:00
|
|
|
for task in self.scheduled:
|
|
|
|
if task != except_for:
|
|
|
|
close(task)
|
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def _finish(self, task: Task, result: Any) -> None:
|
2018-06-14 13:03:09 +00:00
|
|
|
if not self.finished:
|
2019-05-13 13:06:34 +00:00
|
|
|
for index, child_task in enumerate(self.scheduled):
|
|
|
|
if child_task is task:
|
|
|
|
child = self.children[index]
|
|
|
|
break
|
2018-06-14 13:03:09 +00:00
|
|
|
self.finished.append(child)
|
2016-05-02 14:06:08 +00:00
|
|
|
if self.exit_others:
|
2019-05-13 13:06:34 +00:00
|
|
|
self.exit(task)
|
2018-06-14 13:03:09 +00:00
|
|
|
schedule(self.callback, result)
|
2016-09-25 13:55:08 +00:00
|
|
|
|
2019-07-03 13:07:04 +00:00
|
|
|
def __iter__(self) -> Task: # type: ignore
|
2016-09-25 13:55:08 +00:00
|
|
|
try:
|
|
|
|
return (yield self)
|
2018-02-26 23:29:00 +00:00
|
|
|
except: # noqa: E722
|
2016-10-06 12:41:50 +00:00
|
|
|
# exception was raised on the waiting task externally with
|
2017-01-02 14:45:56 +00:00
|
|
|
# close() or throw(), kill the children tasks and re-raise
|
2016-09-25 13:55:08 +00:00
|
|
|
self.exit()
|
|
|
|
raise
|