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
Upozornenie
Pred príchodom na cvičenie si doma nainštalujte editor PyCharm a na cvičenie si prineste vlastné RPi Pico 2 WH minimálne so senzorom DHT, tlačidlom a NeoPixel (alebo RGB LED diódu)! Pokiaľ máte dosku Cytron Robo Pico alebo Cytron Maker Pi Pico Base, potrebujete len senzor DHT.
Jednotlivé elektronické prvky pripojte nasledovne:
- GP3 – senzor DHT
- GP20 – tlačidlo
- GP18 – NeoPixel
Na vaše RPi Pico 2 WH si nahrajte:
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
Porozumieť princípom SOLID a aplikovať ich pri návrhu chytrého zariadenia.
Aplikovať návrhový vzor
Statev jazyku MicroPython.Rozumieť stavovým diagramom.
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.
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.
Stavový stroj je rovnako výborným príkladom aplikácie dvoch SOLID princípov:
- SRP – A class should have only one reason to change.
- 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:
po zapnutí vojde do stavu
Init, kde rozsvieti diagnostickú LED diódu nazelenopo prechode do stavu
Measurementdôjde k odmeraniu teploty a vlhkostipri prechode do stavu
Sleepdiagnostická LED dióda zhasne
Poznámka
Podrobný význam jednotlivých stavov nájdete v 1. zadaní.
Task
V priečinku src/ vytvorte modul context.py
a v ňom triedu Context.
Trieda Context predstavuje samotné zariadenie. Robí dve
veci:
- 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(). - 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á:
.enter()– príprava stavu,.exec()– vykonanie činnosti stavu, ktorá vráti nasledujúci stav,.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_stateKonš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()Poznámka
Inicializácia hardvéru priamo v kontexte je zatiaľ dočasné riešenie.
Kontext tak má na starosti dve veci, čo je v rozpore s princípom SRP.
Neskôr túto zodpovednosť presunieme do samostatnej triedy
DeviceManager.
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}Poznámka
Konštruktor rodičovskej triedy voláme explicitne ako
BaseDevice.__init__(self, alias), a nie pomocou funkcie
super(). Pri viacnásobnom dedení, ktoré čoskoro použijeme,
sa totiž super() v jazyku MicroPython nespráva vždy rovnako
ako v jazyku Python.
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 NotImplementedErrorAk 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
ButtonMixina bude potomkom triedyBaseDevice, - metóda
.is_pressed()vrátiTrue, ak je tlačidlo stlačené, inakFalse.
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é
TrueAk 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, aHumidityMixin– 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
TemperatureMixinaHumidityMixina bude potomkom triedyBaseDevice, - metóda
.temperature()vráti teplotu v jednotkách určených parametromunit, ktorého hodnoty sa nachádzajú v triedeTemperatureUnit(predvolene vráti teplotu v stupňoch Celzia –TemperatureUnit.METRIC), - metóda
.humidity()vráti relatívnu vlhkosť v percentách.
Upozornenie
Senzory DHT nedokážu merať ľubovoľne často. Medzi dvoma
meraniami musí pri senzore DHT11 uplynúť aspoň 1 sekunda a pri
senzore DHT22 aspoň 2 sekundy. Ak budete metódy
.temperature() a .humidity() volať hneď po
sebe a každá z nich spustí nové meranie, senzor skončí s výnimkou
OSError. Zabezpečte preto, aby sa nové meranie spustilo len
vtedy, ak od posledného merania uplynul dostatočný čas.
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, aColorlightMixin– 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
OnOffMixinaColorlightMixina bude potomkom triedyBaseDevice, - 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.
Poznámka
Ak namiesto diódy NeoPixel používate obyčajnú RGB LED diódu,
vytvorte triedu RGBLed v module rgbled.py.
Implementovať bude rovnaké mixiny, ale namiesto parametra
number bude mať tri piny (pre červenú, zelenú a modrú
farbu) a farbu bude nastavovať pomocou PWM.
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ýnimkuValueError, - 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
TrueAk 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.
Poznámka
Tlačidlo do správcu zariadení zatiaľ neregistrujeme, pretože ho ešte
nepoužívame. Zaregistrujeme ho až vtedy, keď ho naozaj budeme potrebovať
– stačí na to pridať jeden riadok do funkcie
register_devices().
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,btnadiag_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.
Poznámka
Nezabudnite aktualizovať aj spustenie stavového stroja v module
main.py.
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…
Upozornenie
Ak máte dosku RPi Pico 2 WH požičanú od cvičiaceho, pred odchodom z cvičenia prosím:
- Uložte si kód, ktorý ste na cvičení vytvorili.
- Zmažte obsah Flash pamäte mikrokontroléra nahratím firmvéru flash_nuke.uf2. Pozor! Mikrokontrolér neodpájajte od počítača, kým sa vo vašom počítači znova neobjaví ako USB zariadenie.
- Nahrajte na mikrokontrolér najnovší firmvér.
Ďalšie úlohy
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 úrovneDEBUG,info()– funkcia na vypísanie logovacích správ úrovneINFO,warning()– funkcia na vypísanie logovacích správ úrovneWARNING,error()– funkcia na vypísanie logovacích správ úrovneERROR,critical()– funkcia na vypísanie logovacích správ úrovneCRITICAL.
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 logovaniaERRORsa nebudú zobrazovať správy úrovníDEBUG,INFOaWARNING).
Ďalšie zdroje
Introduction to Raspberry Pi Pico: LEDs, buzzers, switches, and dials – sada návodov na prácu s RPi Pico
Quick reference for the RP2 – (krátka) dokumentácia jazyka MicroPython pre dosku Raspberry Pi Pico
MicroPython: Controlling NeoPixels
MicroPython: Quick reference for the RP2: NeoPixel and APA106 driver
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.
Last Minute Engineers: Interfacing DHT11 and DHT22 Sensors with Arduino
Last Minute Engineers: Controlling WS2812B Addressable LEDs with Arduino
Real Python: What Are Mixin Classes in Python?