Lab #2: Stavové stroje
chytré zariadenie ako stavový stroj, návrhový vzor
State, prechody medzi stavmi, princípy SOLID
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 DHT11, tlačítkom a NeoPixel (alebo RGB LED diódu)! Pokiaľ máte dosku Cytron Robo Pico alebo Cytron Maker Pi Pico Base, potrebujete len senzor DHT11.
Jednotlivé elektronické prvky pripojte nasledovne:
- GP3 - senzor DHT11
- GP20 - tlačidlo
- GP18 - NeoPixel
Na vaše RPi Pico 2 WH si nahrajte:
Motivácia
Minulé cvičenie 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
SOLID princípy.
Aplikovať návrhový vzor
Stavv jazyku MicroPython.Rozumieť stavovým diagramom.
Postup
Princípy SOLID
Pre 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ý dizajn (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- Dependendcy 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 vieme 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 diagramu stavov a v objektovom programovaní ho vieme 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:
Naše zariadenie bude fungovať nasledovne:
po zapnutí vojde do stavu
Init, kde rozsvieti diagnostickú LED diódu na zelenopo prejdení 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 DHT11, 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.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. Všetky ďalšie stavy už kontext existujúci majú, takže 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 na zeleno.
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 ju
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 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 zariadenie bude môcť meniť používateľ za behu, ako napr. údaje pre 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.
Výrobné nastavenia
Konfiguračný režim
Servisný terminál
Ď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).Logovanie o prechodoch do jednotlivých stavov sa pokúste zjednodušiť tak, aby ste nemuseli logovaciu sprá
Ď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 Chapters: Súborový systém
MicroPython Chapters: Dátové triedy
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