Lab #2: Stavové stroje

chytré zariadenie ako stavový stroj, návrhový vzor State, prechody medzi stavmi, princípy SOLID

Videá pre cvičenie

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

  1. SOLID princípy.

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

  3. 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.

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 - 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.

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:

THsensor: Stavový diagram jednorazového merania

Naše zariadenie bude fungovať nasledovne:

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

  2. po prejdení 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 DHT11, 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.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. 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()

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

  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).

  2. Logovanie o prechodoch do jednotlivých stavov sa pokúste zjednodušiť tak, aby ste nemuseli logovaciu sprá

Ď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 Chapters: Súborový systém

  4. MicroPython Chapters: Dátové triedy

  5. MicroPython: Controlling NeoPixels

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

  7. 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.

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

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