Lab #2: Architektúra chytrého zariadenia

princípy SOLID, stavový stroj a návrhový vzor State, periférie ako komponenty, mixiny, správca zariadení

Videá pre cvičenie

Motivácia

Na minulom cvičení sme si ukázali jednoduchú a veľmi naivnú prvoplánovú implementáciu chytrého zariadenia. Túto implementáciu sme napísali procedurálnym spôsobom. Programovací jazyk Python, ako aj jazyk MicroPython, sú však objektovo-orientované jazyky. Síce ani jeden objektovú paradigmu nevnucuje (ako je tomu napr. v jazyku Java alebo C#), my však náš projekt objektovo písať budeme.

Na tomto cvičení sa zoznámime s princípmi SOLID a správanie chytrého zariadenia začneme opisovať pomocou stavového stroja.

Ciele

  1. Porozumieť princípom SOLID a aplikovať ich pri návrhu chytrého zariadenia.

  2. Aplikovať návrhový vzor State v jazyku MicroPython.

  3. Rozumieť stavovým diagramom.

  4. Navrhnúť periférie zariadenia ako vymeniteľné komponenty pomocou mixinov a viacnásobného dedenia.

Postup

Princípy SOLID

Na písanie objektového kódu existuje niekoľko odporúčaní, pomocou ktorých je možné kód organizovať a písať. Jeden z najznámejších a najpoužívanejších princípov pre objektovo-orientovaný návrh (OOD) sa volá SOLID a ponúka päť princípov objektovo orientovaného návrhu.

SOLID Design Principles

Názov SOLID je akronymom z názvov týchto piatich princípov, ktoré sú:

  • S – Single Responsibility Principle (SRP)
  • O – Open/Closed Principle (OCP)
  • L – Liskov Substitution Principle (LSP)
  • I – Interface Segregation Principle (ISP)
  • D – Dependency Inversion Principle (DIP)

Ich význam a aplikáciu si postupne predstavíme počas návrhu a vývoja projektu.

Stavové stroje

Na mnohé chytré zariadenia sa dá pozerať ako na stavové stroje. To znamená, že celé správanie je možné opísať pomocou stavového diagramu, na základe ktorého môžeme povedať, že v ľubovoľnom momente sa bude zariadenie nachádzať vždy v jednom konkrétnom stave.

Ak by sme hľadali definíciu toho, čo je to stavový stroj, tak jedna z nich by mohla znieť takto:

Stavový stroj je výpočtový model, ktorý opisuje systém pomocou stavov a prechodov medzi nimi.

Stavový stroj sa môže vždy nachádzať len v jednom stave. Jeho správanie môžeme graficky reprezentovať pomocou stavového diagramu a v objektovom programovaní ho môžeme implementovať pomocou návrhového vzoru Stav.

Diagram tried: Návrhový vzor Stav

Stavový stroj je rovnako výborným príkladom aplikácie dvoch SOLID princípov:

  1. SRP – A class should have only one reason to change.
  2. OCP – Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

V tomto kroku prepíšeme pôvodnú naivnú implementáciu a urobíme z nej stavový stroj, ktorého stavový diagram sa nachádza na nasledujúcom obrázku:

---
# spolocna konfiguracia stavovych diagramov THSensor; vklada sa ako prvy
# riadok bloku ```mermaid, za nim nasleduje "flowchart LR" a samotny diagram
config:
  flowchart:
    curve: basis
    nodeSpacing: 40
    rankSpacing: 60
  themeVariables:
    fontSize: 20px
  themeCSS: ".edgeLabel, .edgeLabel p, .labelBkg { background-color: var(--bs-body-bg) !important; }"
---

flowchart LR
    start@{ shape: f-circ } --> init(Init)
    init --> measurement(Measurement)
    measurement --> sleep(Sleep)
    sleep --> sleepEnd@{ shape: fr-circ }

    %% spolocne styly stavovych diagramov THSensor (farby podla
    %% images/thsensor-state.diagram.complete.png); vklada sa na koniec diagramu
    %% a stavy sa k nim priradia cez "class <stavy> <styl>"
    classDef state fill:#cfe2ff,stroke:#9aa3ad,stroke-width:3px,color:#343a40,font-weight:bold
    classDef service fill:#cbc3a3,stroke:#9aa3ad,stroke-width:3px,color:#343a40,font-weight:bold
    classDef reset fill:#ffd6f5,stroke:#9aa3ad,stroke-width:3px,color:#343a40,font-weight:bold
    classDef config fill:#fddba7,stroke:#9aa3ad,stroke-width:3px,color:#343a40,font-weight:bold
    classDef error fill:#ffd7d9,stroke:#9aa3ad,stroke-width:3px,color:#343a40,font-weight:bold
    classDef ota fill:#c3f5c8,stroke:#9aa3ad,stroke-width:3px,color:#343a40,font-weight:bold
    classDef initial fill:#000,stroke:#fff,stroke-width:2px
    classDef final fill:#fff,stroke:#000,stroke-width:2px

    class init,measurement,sleep state
    class start initial
    class sleepEnd final

Obr. 1: Stavový diagram jednorazového merania

Naše zariadenie bude fungovať nasledovne:

  1. po zapnutí vojde do stavu Init, kde rozsvieti diagnostickú LED diódu nazeleno

  2. po prechode do stavu Measurement dôjde k odmeraniu teploty a vlhkosti

  3. pri prechode do stavu Sleep diagnostická LED dióda zhasne

Task

V priečinku src/ vytvorte modul context.py a v ňom triedu Context.

Trieda Context predstavuje samotné zariadenie. Robí dve veci:

  1. Uchováva zdieľané zdroje zariadenia. Pri vytvorení inicializuje hardvér (senzor DHT, tlačidlo a NeoPixel). Každý stav sa k nemu dostane cez svoju členskú premennú self.context, napr. self.context.sensor.measure().
  2. Riadi stavový stroj. Udržiava referenciu na aktuálny stav a deleguje naň správanie zariadenia. S každým stavom komunikuje výhradne cez metódy rozhrania BaseState: .enter(), .exec() a .exit(). Kontext teda nepozná konkrétne stavy a nové stavy môžeme pridávať bez toho, aby sme ho menili (princíp OCP).

Stavy si prechody riadia samy. Metóda exec() vráti objekt nasledujúceho stavu, prípadne None, ak má stavový stroj skončiť. Kontext v cykle pre aktuálny stav zavolá:

  1. .enter() – príprava stavu,
  2. .exec() – vykonanie činnosti stavu, ktorá vráti nasledujúci stav,
  3. .exit() – upratanie po stave.

Potom vrátený stav nastaví ako aktuálny a pokračuje ďalej.

Kostra triedy môže vyzerať takto:

from machine import Pin
from dht import DHT11
from neopixel import NeoPixel


class Context:
    def __init__(self, initial_state):
        # hardware
        self.sensor = DHT11(Pin(3))
        self.btn = Pin(20, Pin.IN, Pin.PULL_UP)
        self.diag_led = NeoPixel(Pin(18), 1)

        self.state = initial_state(self)

    def run(self):
        while self.state is not None:
            self.state.enter()
            next_state = self.state.exec()
            self.state.exit()
            self.state = next_state

Konštruktor kontextu dostane počiatočný stav ako triedu, nie ako objekt. Stav totiž potrebuje referenciu na kontext už pri svojom vytvorení, preto ho vytvorí až samotný kontext pomocou initial_state(self). Hardvér sa inicializuje ešte pred vytvorením stavu, aby ho stav mohol používať už vo svojom konštruktore. Pri všetkých ďalších stavoch už kontext existuje, preto ich metóda .exec() vracia rovno ako objekty, napr. return Sleep(self.context).

Stavový stroj sa potom spustí takto:

context = Context(Init)
context.run()

Task

V súbore states/init.py vytvorte implementáciu stavu Init.

V stave sa aktuálne len rozsvieti diagnostická LED dióda nazeleno. Po skončení stavový stroj prejde do stavu Measurement.

Task

V súbore states/measurement.py vytvorte implementáciu stavu Measurement.

V stave zo senzora odmerajte aktuálnu vlhkosť a teplotu a vypíšte ich pomocou funkcie print(). Po skončení stavový stroj prejde do stavu Sleep.

Task

V súbore states/sleep.py vytvorte implementáciu stavu Sleep.

V tomto stave zhasnite diagnostickú LED diódu a ako nasledujúci stav vráťte hodnotu None. Tým sa vykonávanie stavového stroja zastaví.

Task

Vytvorte súbor main.py, ktorý spustí stavový stroj.

Stavový stroj sa spustí takto:

if __name__ == '__main__':
    context = Context(Init)
    context.run()

Task

Overte svoju implementáciu.

Ak ste postupovali správne, zariadenie sa po spustení zapne, rozsvieti sa diagnostická LED dióda, po odmeraní sa v termináli zobrazia namerané hodnoty zo senzora. Nakoniec diagnostická LED dióda zhasne a vykonávanie stavového stroja sa ukončí.

Konfigurácia chytrého senzora

Niektoré vlastnosti chytrého zariadenia bude môcť meniť používateľ za behu, ako napr. údaje na pripojenie do WiFi siete alebo interval snímania. Niektoré naopak bude definovať výrobca a nebude ich možné nijako zmeniť, ako napr. pracovná frekvencia mikrokontroléra alebo čísla GPIO pinov, ku ktorým budú pripojené jednotlivé elektronické prvky. Nami vytváraný chytrý senzor pripravíme na obe možnosti konfigurácie.

Nemennú, resp. statickú konfiguráciu vyriešime v podobe samostatného modulu jazyka Python, v ktorom jednotlivé vlastnosti zadefinujeme v podobe premenných s príslušnými hodnotami. Tento modul sa bude volať constants.py.

Konfiguráciu, ktorú bude možné meniť, budeme volať používateľské nastavenia. To bude možné napr. pomocou webového rozhrania senzora, pomocou mobilnej aplikácie alebo vzdialene pomocou vhodného komunikačného protokolu. Samotné vlastnosti budeme ukladať na zariadení do súboru typu JSON, pretože manipulácia s týmto typom súborov je v jazyku Python veľmi jednoduchá. Súbor, v ktorom budú používateľské nastavenia uložené, sa bude volať settings.json.

V tomto kroku aplikujeme statickú konfiguráciu zariadenia.

Task

Aktualizujte kód inicializácie elektronických komponentov v triede Context na základe hodnôt premenných v súbore constants.py.

Task

Overte správnosť svojej implementácie.

Ak ste postupovali správne, program sa spustí rovnako.

Zariadenia ako komponenty

Ak by sme chceli vymeniť senzor teploty a vlhkosti za iný, prípadne za dva samostatné senzory, museli by sme zmeniť kontext aj všetky stavy, ktoré s ním pracujú. Rovnako by to bolo aj vtedy, ak by sme sa rozhodli vyrábať iný model toho istého zariadenia, len s inými senzormi. A to aj napriek tomu, že sa činnosť samotného zariadenia nezmení.

V tomto kroku sa zoznámime s prístupom, vďaka ktorému sa budeme na všetky periférie chytrého zariadenia (senzory, akčné členy a ovládacie prvky) pozerať ako na komponenty, ktoré budeme môcť jednoducho vymeniť za iné s rovnakým správaním. Aby to bolo možné, o každej periférii bude platiť nasledovné:

  • bude sa nachádzať v balíku hw,
  • bude potomkom triedy BaseDevice,
  • jej funkcionalitu budú opisovať tzv. mixiny.

V kóde budeme periférie označovať ako zariadenia (z angl. device).

Na implementácii periférií si môžeme ukázať využitie všetkých princípov SOLID:

  • SRP – každá trieda bude mať na starosti práve jednu perifériu,
  • OCP – novú perifériu pridáme vytvorením novej triedy bez zmeny existujúceho kódu,
  • LSP – periféria bude môcť nahradiť inú perifériu s rovnakými schopnosťami,
  • ISP – každú schopnosť periférie bude opisovať samostatný mixin, pričom jedna periféria môže mať viacero schopností,
  • DIP – zvyšok programu nebude závisieť od konkrétneho hardvéru, ale len od toho, čo periféria dokáže.

V tomto kroku vytvoríme triedy pre všetky periférie, ktoré zatiaľ používame: tlačidlo, senzor DHT a diagnostickú LED diódu.

Task

Vytvorte triedu Button, ktorá bude reprezentovať tlačidlo zariadenia.

Každé zariadenie bude potomkom triedy BaseDevice. To znamená, že o každom zariadení budeme potrebovať vedieť:

  • name – názov zariadenia, napr. označenie súčiastky,
  • description – krátky opis zariadenia,
  • pins – piny mikrokontroléra, ku ktorým je zariadenie pripojené.

Názov a opis sú pre všetky inštancie rovnaké, preto ich uvedieme ako triedne premenné. Piny sa naopak môžu líšiť pre každú inštanciu, napr. ak máme na zariadení dve tlačidlá. Preto ich nastavíme v konštruktore ako členskú premennú self.pins. Je to slovník, v ktorom kľúč určuje úlohu pinu a hodnota jeho číslo:

from .base import BaseDevice


class Button(BaseDevice):
    name = 'Button'
    description = 'Push button'

    def __init__(self, pin: int, alias=None):
        BaseDevice.__init__(self, alias)
        self.pins = {'data': pin}

Trieda Button zatiaľ o tlačidle vie, ako sa volá a ku ktorému pinu je pripojené. Nevie však nič o tom, čo tlačidlo dokáže. To jej dodáme pomocou mixinu.

Mixin je malá trieda, ktorá opisuje jednu konkrétnu schopnosť zariadenia. Predpisuje metódy, ktoré zariadenie s touto schopnosťou musí mať, a trieda zariadenia ich implementuje pre konkrétny hardvér. Mixin môže okrem toho poskytovať aj hotovú funkcionalitu postavenú na týchto metódach, ktorú potom zariadenie získa zadarmo. Trieda zariadenia zdedí všetky mixiny, ktorých schopnosti má.

Schopnosť tlačidla opisuje mixin ButtonMixin z modulu hw/mixins/inputs/button.py:

class ButtonMixin:
    def is_pressed(self) -> bool:
        """Return True if the button is currently pressed."""
        raise NotImplementedError

Ak má trieda Button túto schopnosť, zdedí aj mixin ButtonMixin. Mixiny sa pri dedení uvádzajú ako prvé, trieda BaseDevice ako posledná:

class Button(ButtonMixin, BaseDevice):
    ...

Zvyšok programu tak nemusí vedieť, že ide o tlačidlo pripojené na konkrétny pin s pull-up rezistorom. Stačí mu vedieť, že zariadenie má schopnosť ButtonMixin, a teda sa ho môže opýtať, či je stlačené.

O triede Button bude teda platiť nasledovné:

  • jej implementácia sa bude nachádzať v module button.py,
  • názov zariadenia bude "Button",
  • opis zariadenia bude "Push button",
  • bude implementovať mixin ButtonMixin a bude potomkom triedy BaseDevice,
  • metóda .is_pressed() vráti True, ak je tlačidlo stlačené, inak False.

Implementácia triedy Button môže vyzerať nasledovne:

from machine import Pin

from .base import BaseDevice
from .mixins.inputs.button import ButtonMixin


class Button(ButtonMixin, BaseDevice):
    name = 'Button'
    description = 'Push button'

    def __init__(self, pin: int, alias=None):
        BaseDevice.__init__(self, alias)
        self.pins = {'data': pin}
        self._pin = Pin(pin, Pin.IN, Pin.PULL_UP)

    def is_pressed(self) -> bool:
        return not self._pin.value()

Tlačidlo je pripojené s pull-up rezistorom, preto pin pri stlačení vráti hodnotu 0. Metóda .is_pressed() hodnotu pinu preto neguje.

Task

Overte funkčnosť vytvorenej triedy Button.

Triedu môžete vyskúšať priamo v REPL jazyka MicroPython. Vytvorte inštanciu tlačidla pripojeného na pin BTN_PIN a zistite jeho stav raz s uvoľneným a raz so stlačeným tlačidlom:

>>> from constants import BTN_PIN
>>> from hw.button import Button
>>> btn = Button(BTN_PIN)
>>> btn.is_pressed()
False
>>> btn.is_pressed()  # tlačidlo je stlačené
True

Ak ste postupovali správne, metóda .is_pressed() vráti hodnotu True len vtedy, keď je tlačidlo stlačené.

Task

Zobrazte schopnosti a ďalšie informácie o vytvorenom tlačidle.

Trieda BaseDevice poskytuje dve metódy, ktoré zariadenie dokážu opísať:

  • .capabilities() – vráti zoznam schopností zariadenia, teda názvy všetkých mixinov, ktoré trieda zariadenia zdedila,
  • .info() – vráti slovník so všetkými informáciami o zariadení: názov, aliasy, opis, piny a schopnosti.

Schopnosti pritom nikde neuvádzate ručne. Trieda BaseDevice ich zistí sama z rodičovských tried zariadenia – za schopnosť považuje každú triedu, ktorej názov končí na Mixin.

Ak ste postupovali správne, výpis bude podobný tomuto:

>>> btn.capabilities()
['ButtonMixin']
>>> btn.info()
{'name': 'Button', 'aliases': [], 'description': 'Push button', 'pins': {'data': 20}, 'capabilities': ['ButtonMixin']}

Task

Vytvorte triedu pre senzor teploty a vlhkosti.

Senzor DHT má dve schopnosti, pretože dokáže merať teplotu aj vlhkosť. Tie sú opísané pomocou dvoch mixinov:

  • TemperatureMixin – na meranie teploty, a
  • HumidityMixin – na meranie vlhkosti.

Trieda senzora preto zdedí oba mixiny naraz.

O triede senzora bude platiť nasledovné:

  • jej implementácia sa bude nachádzať v module dht11.py (resp. dht22.py),
  • názov triedy bude DHT11 (resp. DHT22),
  • názov zariadenia bude "DHT11" (resp. "DHT22"),
  • opis zariadenia bude "Temperature and humidity sensor",
  • bude implementovať mixiny TemperatureMixin a HumidityMixin a bude potomkom triedy BaseDevice,
  • metóda .temperature() vráti teplotu v jednotkách určených parametrom unit, ktorého hodnoty sa nachádzajú v triede TemperatureUnit (predvolene vráti teplotu v stupňoch Celzia – TemperatureUnit.METRIC),
  • metóda .humidity() vráti relatívnu vlhkosť v percentách.

Task

Overte funkčnosť vytvorenej triedy pre senzor DHT.

Vytvorené zariadenie môžete otestovať v REPL jazyka MicroPython. Stačí vytvoriť jeho inštanciu a následne zavolať metódy z implementovaných mixinov:

>>> from constants import DHT_PIN
>>> from hw.dht11 import DHT11
>>> sensor = DHT11(DHT_PIN)
>>> sensor.temperature()
23
>>> sensor.humidity()
45
>>> sensor.capabilities()
['TemperatureMixin', 'HumidityMixin']

Ak ste postupovali správne, senzor vráti aktuálnu teplotu aj vlhkosť a ani pri ich zavolaní hneď po sebe neskončí s výnimkou OSError. Medzi schopnosťami zariadenia budú oba mixiny.

Task

Vytvorte triedu pre diagnostickú LED diódu NeoPixel.

Ako diagnostickú LED diódu používame adresovateľnú RGB LED diódu označovanú ako NeoPixel, ktorá je riadená obvodom WS2812B. Na jej ovládanie nás budú zaujímať dve schopnosti, ktoré sú opísané týmito mixinmi:

  • OnOffMixin – na zapnutie, vypnutie a prepnutie diódy, a
  • ColorlightMixin – na nastavenie a zistenie farby svetla.

Na rozdiel od tlačidla a senzora teploty a vlhkosti si toto zariadenie musí pamätať svoj stav. Keď diódu vypnete a znova zapnete, mala by sa rozsvietiť rovnakou farbou ako predtým.

O triede diódy bude platiť nasledovné:

  • implementácia triedy sa bude nachádzať v module ws2812b.py,
  • názov triedy bude WS2812B,
  • názov zariadenia bude "WS2812B",
  • opis zariadenia bude "RGB LED controller",
  • bude implementovať mixiny OnOffMixin a ColorlightMixin a bude potomkom triedy BaseDevice,
  • konštruktor bude mať okrem pinu a aliasu aj parameter number, ktorý určí počet diód v reťazci,
  • metóda .on() rozsvieti diódu naposledy nastavenou farbou (predvolene bielou),
  • metóda .off() diódu zhasne,
  • metóda .toggle() diódu zapne, ak je vypnutá, a vypne, ak je zapnutá,
  • metóda .color() bez parametra vráti aktuálne nastavenú farbu, s parametrom v tvare n-tice (r, g, b) farbu nastaví a diódu ňou rozsvieti.

Task

Overte funkčnosť vytvorenej triedy pre diagnostickú LED diódu.

Vytvorené zariadenie môžete otestovať v REPL jazyka MicroPython. Stačí vytvoriť jeho inštanciu a následne zavolať metódy z implementovaných mixinov:

>>> from constants import DIAG_LED_PIN
>>> from hw.ws2812b import WS2812B
>>> led = WS2812B(DIAG_LED_PIN, 1)
>>> led.on()
>>> led.color((0, 255, 0))
>>> led.color()
(0, 255, 0)
>>> led.off()
>>> led.toggle()
>>> led.capabilities()
['OnOffMixin', 'ColorlightMixin']

Ak ste postupovali správne, dióda sa najprv rozsvieti nabielo, potom sa zmení na zelenú, zhasne a po prepnutí sa znova rozsvieti nazeleno.

Správca zariadení

Prístup k jednotlivým perifériám už máme zabalený do vlastných tried. Kontext ich však zatiaľ nepoužíva a pre každú perifériu má stále samostatnú členskú premennú. Kontext tak riadi stavový stroj a zároveň sa stará o hardvér, čím porušuje princíp SRP.

Správu periférií preto presunieme do samostatnej triedy – do správcu zariadení (DeviceManager). Ten bude implementovať návrhový vzor Register (z angl. Registry). Jeho podstata spočíva v tom, že všetky zariadenia sa pred použitím zaregistrujú u správcu a ktokoľvek, kto bude mať k správcovi prístup, si z neho potom vyžiada zariadenie, ktoré práve potrebuje. To bude možné dvoma spôsobmi:

  • podľa schopnosti (implementovaného mixinu), alebo
  • podľa aliasu (vlastného mena), pričom periféria môže mať viacero aliasov.

Task

Vytvorte triedu DeviceManager s metódou .register(), ktorá slúži na zaregistrovanie novej periférie.

O triede DeviceManager bude platiť nasledovné:

  • jej implementácia sa bude nachádzať v module manager.py,
  • zaregistrované zariadenia si bude uchovávať v zozname,
  • metóda .register(device) zaregistruje zariadenie; ak už je zaregistrované zariadenie s rovnakým aliasom, vyvolá výnimku ValueError,
  • správca zariadení bude implementovať návrhový vzor Iterátor (z angl. Iterator), takže ho bude možné prechádzať cyklom for.

Zariadenie, ktoré má byť zaregistrované, môže mať jeden alebo viac aliasov. Aliasy sú užitočné napr. vtedy, ak chytré zariadenie obsahuje viacero periférií rovnakého typu a je potrebné ich od seba odlíšiť. Aliasy, ktoré budeme používať, sú pripravené v module constants.py ako konštanty ALIAS_*.

Task

Overte funkčnosť vytvorenej triedy DeviceManager.

Vytvoreného správcu zariadení môžete otestovať v REPL jazyka MicroPython. Vytvorte jeho inštanciu, zaregistrujte do nej niekoľko zariadení a následne ich vypíšte:

>>> from constants import DHT_PIN, DIAG_LED_PIN, ALIAS_TEMP, ALIAS_HUMIDITY, ALIAS_DIAG_LED
>>> from hw.manager import DeviceManager
>>> from hw.dht11 import DHT11
>>> from hw.ws2812b import WS2812B
>>> devices = DeviceManager()
>>> devices.register(DHT11(DHT_PIN, alias=[ALIAS_TEMP, ALIAS_HUMIDITY]))
>>> devices.register(WS2812B(DIAG_LED_PIN, 1, alias=ALIAS_DIAG_LED))
>>> for device in devices:
...     print(device.name, device.aliases)
...
DHT11 ['temp', 'humidity']
WS2812B ['diag_led']
>>> devices.register(DHT11(DHT_PIN, alias=ALIAS_TEMP))
Traceback (most recent call last):
...
ValueError: Device with alias "temp" already registered.

Ak ste postupovali správne, cyklus for vypíše všetky zaregistrované zariadenia a pokus o registráciu zariadenia s už použitým aliasom skončí výnimkou ValueError.

Task

Rozšírte triedu DeviceManager o metódu .get(), ktorou bude možné nájsť zariadenie podľa aliasu alebo podľa schopnosti.

Metóda .get(alias_or_capability) vráti prvé zaregistrované zariadenie:

  • s daným aliasom, ak je parametrom reťazec,
  • s danou schopnosťou, ak je parametrom trieda (mixin alebo konkrétny typ zariadenia).

Ak hľadané zariadenie neexistuje, metóda vráti hodnotu None.

Task

Overte funkčnosť metódy .get().

Vytvoreného správcu zariadení môžete otestovať v REPL jazyka MicroPython. Vytvorte jeho inštanciu, zaregistrujte do nej niekoľko zariadení a následne si ich vyžiadajte podľa aliasu aj podľa schopnosti:

>>> from constants import DHT_PIN, DIAG_LED_PIN, ALIAS_TEMP, ALIAS_HUMIDITY, ALIAS_DIAG_LED
>>> from hw.manager import DeviceManager
>>> from hw.dht11 import DHT11
>>> from hw.ws2812b import WS2812B
>>> from hw.mixins.sensors.temperature import TemperatureMixin
>>> from hw.mixins.actuators.colorlight import ColorlightMixin
>>> devices = DeviceManager()
>>> devices.register(DHT11(DHT_PIN, alias=[ALIAS_TEMP, ALIAS_HUMIDITY]))
>>> devices.register(WS2812B(DIAG_LED_PIN, 1, alias=ALIAS_DIAG_LED))
>>> devices.get(ALIAS_DIAG_LED).name
'WS2812B'
>>> devices.get(ALIAS_TEMP) is devices.get(ALIAS_HUMIDITY)
True
>>> devices.get(TemperatureMixin).name
'DHT11'
>>> devices.get(ColorlightMixin).name
'WS2812B'
>>> devices.get('blabla') is None
True

Ak ste postupovali správne, správca vráti diagnostickú LED diódu podľa aliasu aj podľa schopnosti, pre oba aliasy senzora DHT ten istý senzor a pre neznámy alias hodnotu None.

Zostavenie chytrého zariadenia

Správca zariadení sa stane súčasťou kontextu, aby k nemu mal prístup každý stav. Kontext ho však nebude vytvárať ani v ňom registrovať zariadenia sám. Hotového správcu so zaregistrovanými zariadeniami dostane ako parameter konštruktora. Tento prístup sa nazýva vkladanie závislostí (z angl. dependency injection) a je praktickou podobou princípu DIP.

Task

Zaregistrujte periférie chytrého zariadenia.

V module main.py vytvorte samostatnú funkciu register_devices(). Táto funkcia vráti inštanciu triedy DeviceManager so zaregistrovanými perifériami.

Periférie zaregistrujte podľa nasledujúcej tabuľky:

periféria pin aliasy
senzor DHT (DHT11, resp. DHT22) DHT_PIN ALIAS_TEMP, ALIAS_HUMIDITY
diagnostická LED dióda NeoPixel (WS2812B) DIAG_LED_PIN ALIAS_DIAG_LED

Výhodou tohto prístupu je, že registrácia periférií je na jednom mieste. Ak sa zmení hardvér, zmení sa len táto funkcia.

Task

Upravte triedu Context tak, aby používala správcu zariadení.

Konštruktor triedy Context aktuálne obsahuje len jeden parameter, ktorým je počiatočný stav stavového stroja. Upravte ho nasledovne:

  • signatúra konštruktora bude:

    def __init__(self, devices: DeviceManager, initial_state):
        ...
  • správcu uložte do členskej premennej self.devices,

  • z kontextu odstráňte vytváranie periférií (členské premenné sensor, btn a diag_led) aj s príslušnými importmi.

Výhodou vkladania závislostí je napr. to, že pri testovaní môžeme kontextu odovzdať správcu so simulovanými zariadeniami a stavy otestovať aj bez skutočného hardvéru.

Task

Upravte implementáciu stavov tak, aby si periférie vyžiadali od správcu zariadení.

Stavy zatiaľ pristupujú k perifériám pomocou členských premenných kontextu, ktoré sa v ňom už nenachádzajú. Upravte implementáciu jednotlivých stavov tak, aby bol prístup k perifériám zabezpečený prostredníctvom správcu zariadení. Na získanie konkrétnej periférie použite aliasy, napr.:

led = self.context.devices.get(ALIAS_DIAG_LED)

Task

Overte správnosť svojej implementácie.

Ak ste postupovali správne, zariadenie sa bude správať rovnako ako na konci kroku o stavovom stroji: po spustení rozsvieti diagnostickú LED diódu, vypíše nameranú teplotu a vlhkosť, diódu zhasne a vykonávanie stavového stroja sa ukončí.

Pred odchodom z cvičenia…

Ďalšie úlohy

  1. Jazyk MicroPython nemá k dispozícii modul na logovanie. Preto vytvorte samostatný modul log.py, ktorý chýbajúcu funkcionalitu poskytne. V tomto module vytvorte (minimálne) tieto funkcie:

    • debug() – funkcia na vypísanie logovacích správ úrovne DEBUG,
    • info() – funkcia na vypísanie logovacích správ úrovne INFO,
    • warning() – funkcia na vypísanie logovacích správ úrovne WARNING,
    • error() – funkcia na vypísanie logovacích správ úrovne ERROR,
    • critical() – funkcia na vypísanie logovacích správ úrovne CRITICAL.

    Pre potreby logovania vytvorte globálnu premennú LOG_LEVEL, pomocou ktorej bude možné nastaviť úroveň logovania globálne pre celé zariadenie. To znamená, že ak bude nastavená vyššia úroveň logovania (napr. úroveň ERROR), nebudú sa zobrazovať logovacie správy nižších úrovní (napr. v prípade úrovne logovania ERROR sa nebudú zobrazovať správy úrovní DEBUG, INFO a WARNING).

Ďalšie zdroje

  1. Introduction to Raspberry Pi Pico: LEDs, buzzers, switches, and dials – sada návodov na prácu s RPi Pico

  2. Quick reference for the RP2 – (krátka) dokumentácia jazyka MicroPython pre dosku Raspberry Pi Pico

  3. MicroPython: Controlling NeoPixels

  4. MicroPython: Quick reference for the RP2: NeoPixel and APA106 driver

  5. Random Nerd Tutorials: MicroPython: WS2812B Addressable RGB LEDs with ESP32 and ESP8266 – This tutorial shows how to control WS2812B addressable RGB LEDs (neopixels) with the ESP32 and ESP8266 using MicroPython.

  6. Last Minute Engineers: Interfacing DHT11 and DHT22 Sensors with Arduino

  7. Last Minute Engineers: Controlling WS2812B Addressable LEDs with Arduino

  8. Real Python: What Are Mixin Classes in Python?