.. include:: <mmlalias.txt> Screenplay: Pointless Blinking With Python, ``asyncio``, and ``libgpiod`` (and a Raspberry Pi of Course) ======================================================================================================== .. contents:: :local: Setup Before Presentation ------------------------- Starting Point .............. * Download :download:`code/thread-multi.py` as ``blink.py``, and ``chmod +x`` * Download :download:`code/gpio-multi.py` as ``gpio.py``, and ``chmod +x`` * Download :download:`code/off.py` as ``off.py``, and ``chmod +x`` * Download `snippets/*` into ``snippets`` Greeting ........ In a subdirectory: * ``stuff.py`` from :download:`snippets/stuff-complete` * Save away blink program with the squares etc Multiple Background Threads --------------------------- .. sidebar:: **Editor** * ``blink.py`` .. code-block:: console $ strace -f python3 code/thread-multi.py ... [pid 4677] write(1, "hello left\n", 11hello left ) = 11 [pid 4677] clock_gettime64(CLOCK_MONOTONIC, {tv_sec=164646, tv_nsec=833862215}) = 0 [pid 4677] _newselect(0, NULL, NULL, NULL, {tv_sec=0, tv_usec=500000} <unfinished ...> [pid 4679] <... _newselect resumed>) = 0 (Timeout) [pid 4679] write(1, " hello mi"..., 61 hello middle ) = 61 [pid 4679] clock_gettime64(CLOCK_MONOTONIC, {tv_sec=164646, tv_nsec=845864201}) = 0 [pid 4679] _newselect(0, NULL, NULL, NULL, {tv_sec=1, tv_usec=0} <unfinished ...> [pid 4678] <... _newselect resumed>) = 0 (Timeout) [pid 4678] write(1, " "..., 61 hello right ) = 61 * Three independent PIDs, using ``select()`` to implement ``time.sleep()`` (``NULL`` fds) * Fourth (main thread) also involved occasionally, likely due to Python's weird thread management (GIL) * Managed by OS scheduler * |longrightarrow| scheduling jitter, heavy (?) OS load **Blink** .. literalinclude:: code/thread-multi.py :caption: :download:`code/thread-multi.py` :language: python Enter ``asyncio`` ----------------- * Replace ``threading`` and ``time`` with ``asyncio`` * ``async`` functions, ``await asyncio.sleep(...)`` * ``async def main()`` * ``asyncio.run(main())`` .. code-block:: console $ strace -f code/async-multi.py ... epoll_wait(3, [], 1, 201) = 0 ... * *Single thread!* * Timeouts apparently multiplexed on the *event loop's* timeout parameter **Blink** .. literalinclude:: code/async-multi.py :caption: :download:`code/async-multi.py` :language: python Character Device Based GPIO --------------------------- .. sidebar:: **Editor** * ``gpio.py`` * The way to go for GPIO on Linux * Alternative: ``sysfs`` GPIO * Unmaintained * Not immune to hotplug GPIO (e.g. USB GPIO controller) |longrightarrow| fixed number range * Not reset when application crashes (a feature that is not wanted by most people) * No pullup/pulldown configuration * ``RPi.GPIO`` * Raspberry specific * Weird (a background thread fires event/interrupts) * Bound to go away (I hope) `Libgpiod V2: New Major Release with a Ton of New Features - Bartosz Golaszewski <https://youtu.be/6fxcDDLII6Y>`__ .. raw:: html <iframe width="560" height="315" src="https://www.youtube.com/embed/6fxcDDLII6Y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen> </iframe> GPIO Device ----------- * ``/dev/gpiochip0``: character device * Opened by processes * Communication via ``ioctl()`` * Selectable |longrightarrow| events/interrupts * User space library: ``libgpiod`` .. code-block:: console $ ls -l /dev/gpiochip0 ls -l crw-rw---- 1 root gpio 254, 0 Apr 9 13:30 /dev/gpiochip0 Most Basic Feature: Setting GPIO Values --------------------------------------- * Request GPIO 11, 10, 27 * ... configuring for output .. literalinclude:: code/gpio-multi.py :caption: :download:`code/gpio-multi.py` :language: python Entire Matrix On/Off -------------------- .. sidebar:: **Editor** * ``gpio.py`` **Snippets** * ``matrix`` **Steps** * Dictionary comprehension ``set_values()`` * Use ``ALL_IOS`` **GPIO** .. literalinclude:: code/gpio-matrix-on-off.py :caption: :download:`code/gpio-matrix-on-off.py` :language: python Bringing All Together --------------------- .. sidebar:: **Editor** * ``blink.py`` **Snippets** * ``set_values`` * ``blink-raw`` **Steps** * Transform ``print()`` to ``blink()`` according to table * No ``ntimes`` * List of tasks * Continue ``blink.py`` * Morph ``hello*()`` in ``blink(ios, interval, ntimes=None)`` (``None`` -> ``itertools.count()``, else ``range()``) * ``import gpiod``, next to ``asyncio`` * Pull in snippet ``set_values`` * Pull in snippet ``blink-raw`` .. list-table:: :align: left :widths: auto :header-rows: 1 * * GPIO * Interval * * 11 * 0.5 * * 10 * 0.4 * * 27 * 0.3 * * 4 * 0.2 * * 2 * 0.1 **Blink** .. literalinclude:: code/blink-multi-raw.py :caption: :download:`code/blink-multi-raw.py` :language: python Modularize ---------- .. sidebar:: **Editor** * ``blink.py`` * ``stuff.py`` **Steps** * Externalize stuff into ``stuff.py`` * Note this is not about clean coding |:pig:| * Cram stuff into ``stuff.py`` * All but ``main()`` goes there **Stuff** .. literalinclude:: code/stuff_raw.py :caption: :download:`code/stuff_raw.py` :language: python **Blink** .. literalinclude:: code/blink-multi-raw-mod.py :caption: :download:`code/blink-multi-raw-mod.py` :language: python Play A Bit: Blink Entire Rows ----------------------------- .. sidebar:: **Steps** * ``rows = MATRIX`` * Blink rows * Five tasks, one for each row * Make list of tasks **Blink** .. literalinclude:: code/blink-rows-raw.py :caption: :download:`code/blink-rows-raw.py` :language: python * Sideway, maybe: blink columns * |longrightarrow| ``numpy.transpose()`` * I like heavy dependencies |:pig:| **Blink** .. literalinclude:: code/blink-cols-raw.py :caption: :download:`code/blink-cols-raw.py` :language: python Coroutines? ----------- .. sidebar:: **Steps** * Omit ``create_task()`` |longrightarrow| coroutines * Show ``blink()`` in interactive interpreter (``await`` outside function? |longrightarrow| ``run()``) * Hm ... too much of ``asyncio.create_task()`` here * Instantiate coroutines first, not using ``create-task()`` immediately * Coroutines do nothing * ``await``, or ``create_task()`` * Pass to ``all()`` (pull in from snippet ``all-initial``) * ``await`` that instead **Blink** .. literalinclude:: code/blink-cols-coro.py :caption: :download:`code/blink-cols-coro.py` :language: python Not Enough: ``sequence()`` -------------------------- * Not using ``create_task()``: sequential execution * Pass a limit to ``blink()`` * |longrightarrow| another one: ``sequence()`` .. literalinclude:: code/blink-sequence.py :caption: :download:`code/blink-sequence.py` :language: python * Pull ``all()`` and ``sequence()`` out into ``stuff.py`` when done * :download:`code/blink-sequence-clean.py` Looping: ``forever()`` ---------------------- .. sidebar:: **Steps** * Cannot re-use coroutine * |longrightarrow| **re-instantiate!** .. literalinclude:: code/blink-forever.py :caption: :download:`code/blink-forever.py` :language: python .. code-block:: console $ code/blink-forever.py ... RuntimeError: cannot reuse already awaited coroutine ... * Need to *create* new coroutine at each execution * |longrightarrow| factory? A Stripped-Down Program (|longrightarrow| Factory) -------------------------------------------------- .. sidebar:: **Steps** * Strip down program to bare minimum .. literalinclude:: code/blink-factory-start.py :caption: :download:`code/blink-factory-start.py` :language: python Turn ``blink()`` Into A Factory ------------------------------- .. sidebar:: **Steps** * ``class blink`` * Method ``create_coro()`` * Call in ``forever()`` * ``class blink``, with a ``__init__`` just like original ``blink()`` function * Stores ctor parameters as members * One single method: ``create_coro(self)`` * Creates nested function ``_blink(ios, interval, ntimes)`` * Returns instantiated coroutine from it * ``forever()`` gets passed ``factory``, and calls ``factory.create_coro()`` in every iteration * |longrightarrow| done * |longrightarrow| except: clumsy .. literalinclude:: code/blink-factory.py :caption: :download:`code/blink-factory.py` :language: python Anti-Clumsy Decorator: ``blink()`` Wrapper ------------------------------------------ .. code-block:: python def create_factory_for_blink(blinkfunc): def factory(ios, interval, ntimes=None): def create_coro(): return blinkfunc(ios, interval, ntimes) return create_coro return factory .. sidebar:: **Steps** * ``create_factory_for_blink()`` * Replace: ``blink = create_factory_for_blink(blink)`` * Factory is much writing * Closures are objects * "Members" are *in the closure* * One *method*: the function * |longrightarrow| inverted view * Start with ``create_factory_for_blink(blink_func)`` * |longrightarrow| hardwired parameters: ``ios, interval, ntimes`` .. code-block:: python def create_factory_for_blink(blinkfunc): def factory(ios, interval, ntimes=None): def create_coro(): return blinkfunc(ios, interval, ntimes) return create_coro return factory * Wrap manually .. code-block:: python blink = create_factory_for_blink(blink) **Stuff** .. literalinclude:: code/stuff_decorator_stage1.py :caption: :download:`code/stuff_decorator_stage1.py` :language: python **Blink** .. literalinclude:: code/blink-decorator-stage1.py :caption: :download:`code/blink-decorator-stage1.py` :language: python ``@program``, Finally --------------------- .. sidebar:: **Steps** * Use starargs * Rename ``blinkfunc`` to ``func`` * Rename ``create_factory_for_blink`` to ``program`` * Decorate both ``blink()`` and ``forever()`` * Move both (back) into ``stuff.py`` * Decorate the others (and let them start programs by calling them) * ``main()`` becomes obsolete: just call ``prog()`` .. sidebar:: **See also** * :doc:`/trainings/material/soup/python/advanced/starargs/topic` * :doc:`/trainings/material/soup/python/advanced/closures/topic` * :doc:`/trainings/material/soup/python/advanced/decorators/topic` **Stuff** .. literalinclude:: code/stuff_decorator_stage1.py :caption: :download:`code/stuff_decorator_stage1.py` :language: python **Blink** .. literalinclude:: code/blink-decorator-stage1.py :caption: :download:`code/blink-decorator-stage1.py` :language: python Playground: ``cycle()`` ----------------------- .. sidebar:: **Snippets** * ``cycle`` into ``stuff.py`` * ``squares`` into ``stuff.py`` **Steps** * cycle one row * cycle all rows * cycle squares * ... .. literalinclude:: snippets/cycle :caption: :download:`snippets/cycle` :language: python * Cycle ``row[0]`` * |longrightarrow| **A-HA!!** Fast Forward: ``any()``, And Cancellation ----------------------------------------- .. sidebar:: **Snippets** * ``stuff-complete``, replacing ``stuff.py`` content * ``prog-any-demo`` * ``blink.py``: insert ``prog-all-demo`` |longrightarrow| explain * Show ``stuff.any()`` |longrightarrow| opposite of ``all()`` * ``blink.py``: insert ``prog-any-demo`` Playground: ``on()`` -------------------- .. sidebar:: **Snippets** * ``prog-any-on-demo`` * ``prog-my-blink`` * Show ``stuff.on()`` * Explain ``Future`` * ``blink.py``: insert ``prog-any-on-demo`` * ``blink.py``: insert ``prog-my-blink`` Goodbye ------- .. sidebar:: **Snippets** * ``prog-smiley`` * ``blink.py``: insert ``prog-smiley``