Lab #3: Servisný terminál
diagnostický režim zariadenia, sériová linka, interpreter príkazov
Upozornenie
Na cvičenie si prineste vlastné RPi Pico 2 WH so stavovým strojom, ktorý ste vytvorili na minulom cvičení.
Motivácia
Naše zariadenie vybavíme špeciálnym stavom, v ktorom budeme môcť vykonávať diagnostiku zariadenia. Jeho podstatou bude, že sa do neho budeme môcť dostať hneď po zapnutí zariadenia a tento stav bude úplne mimo hlavného cyklu nášho zariadenia.
- diagnostický režim
- servisný terminál
- komunikácia pomocou sériovej linky
- príkazový riadok (REPL)
Ciele
Vytvoriť diagnostický režim zariadenia ako samostatný stav stavového stroja.
Komunikovať so zariadením pomocou sériovej linky.
Aplikovať návrhové vzory Príkaz (z angl. Command) a Register (z angl. Registry) v jazyku MicroPython.
Osvojiť si návrhový vzor iterátor.
Porozumieť významu návrhového vzoru
Null Object.
Postup
Stav
ServiceTerminal
Diagnostický režim zariadenia bude reprezentovaný samostatným stavom
s názvom ServiceTerminal.
Do servisného terminálu sa zariadenie dostane zo stavu
Init a po jeho ukončení sa vykonávanie stavového stroja
skončí. Stavový diagram zariadenia s novým stavom sa nachádza na obrázku
1.
---
# 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)
init -- "<code>SVC_PIN</code><br>grounded" --> terminal("Service<br>Terminal")
measurement --> sleep(Sleep)
sleep --> sleepEnd@{ shape: fr-circ }
terminal --> terminalEnd@{ 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 terminal service
class start initial
class sleepEnd,terminalEnd final
Obr. 1: Stavový diagram zariadenia so stavom
ServiceTerminal
Aby technik na prvý pohľad videl, že sa zariadenie nachádza v servisnom režime, vyhradíme pre tento stav vlastnú farbu diagnostickej LED diódy.
Task
V balíku states vytvorte v module
service_terminal.py triedu ServiceTerminal,
ktorá bude reprezentovať servisný terminál.
O triede bude platiť nasledovné:
trieda bude potomkom triedy
BaseState..enter()– Rozsvieti diagnostickú LED diódu farbou vyhradenou pre servisný režim, ktorá bude(5, 5, 5). Táto farba sa nesmie zhodovať so žiadnou farbou, ktorú používajú ostatné stavy..exec()– Vypíše uvítaciu správu servisného terminálu, napr."# Welcome to Service Terminal", a ako nasledujúci stav vráti hodnotuNone. Tým sa vykonávanie stavového stroja zatiaľ ukončí..exit()– V tejto metóde diagnostickú LED diódu zhasnite.
Task
Overte vytvorenú implementáciu.
Stav zatiaľ nie je prepojený so žiadnym iným stavom, takže sa doň
stavový stroj nemá ako dostať. Pri overovaní preto využijeme to, že
kontext dostáva počiatočný stav ako parameter. V súbore
main.py dočasne zmeňte počiatočný stav zo stavu
Init na stav ServiceTerminal:
if __name__ == "__main__":
context = Context(register_devices(), ServiceTerminal)
context.run()Ak ste postupovali správne, po spustení sa diagnostická LED dióda rozsvieti farbou servisného režimu, v termináli sa zobrazí uvítacia správa a vykonávanie stavového stroja sa ukončí.
Po overení vráťte v súbore main.py počiatočný stav späť
na stav Init.
Poznámka
Spustenie stavového stroja z ľubovoľného stavu je užitočné aj neskôr. Nový stav tak môžete vyvíjať a ladiť samostatne, bez toho, aby ste museli zariadenie zakaždým dostať do neho cez všetky predchádzajúce stavy.
Prechod do servisného
terminálu zo stavu Init
Do servisného režimu zariadenia sa používateľ nemôže dostať len tak. A nemal by sa do neho dostať ani náhodou. Ak sa chce aj napriek tomu používateľ do tohto režimu dostať, obyčajne to znamená, že potrebuje zariadenie rozobrať a skratovať dva piny pomocou jumpera. A presne toto riešenie vytvoríme aj my.
Do servisného terminálu sa zariadenie dostane priamo zo stavu
Init, ak bude servisný pin (v konštantách označený ako
SVC_PIN) uzemnený (prepojený priamo s pinom GND).
Task
Zaregistrujte v správcovi zariadení servisné tlačidlo.
Z pohľadu programu sa skratovaný jumper správa rovnako ako stlačené
tlačidlo, preto na jeho reprezentáciu použijeme triedu
Button. Servisné tlačidlo bude pripojené na pin
SVC_PIN a pri registrácii do správcu zariadení dostane
alias ALIAS_SERVICE_BTN.
Task
Zabezpečte, aby v stave Init došlo k prechodu do stavu
ServiceTerminal, ak je pri štarte stlačené servisné
tlačidlo.
Poznámka
Je dôležité, aby sa prechod vykonal ako prvý pred akoukoľvek inou činnosťou. Vykonať sa musí aj vtedy, ak napr. chýba súbor s používateľskými nastaveniami, a ešte pred samotestovaním hardvéru.
Task
Overte svoju implementáciu.
Ak ste postupovali správne, po zapnutí zariadenia dôjde k odmeraniu
teploty a vykonávanie sa ukončí. Ak však uzemníte SVC_PIN,
diagnostická LED dióda zmení farbu a v termináli si prečítate uvítaciu
správu.
Poznámka
Nezabudnite v module main.py vrátiť počiatočný stav
stavového stroja na stav Init.
Sériová linka
Na komunikáciu so servisným terminálom použijeme sériovú linku (z angl. serial line). Práca s ňou je veľmi jednoduchá a zvláda ju každý mikrokontrolér. Aj preto majú servisný (konzolový) port napr. aj sieťové zariadenia, priemyselné riadiace systémy či 3D tlačiarne.
So servisným terminálom nebudeme komunikovať cez USB kábel, cez ktorý zariadenie programujeme. Vyhradíme mu samostatné hardvérové sériové rozhranie UART. K počítaču ho pripojíme pomocou USB-TTL kábla, ktorý sa v počítači tvári ako ďalší sériový port.
Task
Prepojte svoj počítač s chytrým zariadením pomocou USB-TTL kábla.
USB-TTL kábel má na jednej strane USB konektor a na druhej strane štyri vodiče zakončené dutinkami (pozri obrázok 2). Význam jednotlivých vodičov podľa farieb je nasledovný:
| farba | význam |
|---|---|
| červená | napájanie +5V – nezapájať |
| čierna | zem (GND) |
| zelená | TX – kábel údaje odosiela |
| biela | RX – kábel údaje prijíma |
Poznámka
Dosky RPi Pico používajú logiku s napätím 3,3 V. Prevodník USB-TTL by preto mal na pinoch TX a RX pracovať s rovnakou napäťovou úrovňou. Pôvodná doska RPi Pico s mikrokontrolérom RP2040 by mohla napätím 5 V utrpieť poškodenie.
Mikrokontrolér RP2350 na doske RPi Pico 2 je v tomto zhovievavejší. Podľa dokumentácie [1] sú jeho digitálne piny označené ako Fault Tolerant, takže napätie 5 V zvládnu – no len vtedy, keď je doska napájaná, a netýka sa to pinov s analógovým vstupom (GP26 až GP29).
Doska RPi Pico 2 WH sa s
káblom prepojí len tromi vodičmi: GND, TX a RX. Dátové vodiče sa pritom zapájajú do
kríža – vysielač (TX) jednej strany je pripojený na prijímač (RX) druhej
strany a naopak. Zelený vodič kábla preto pripojte na pin
UART_RX_PIN dosky, biely vodič na pin
UART_TX_PIN a čierny vodič na pin GND. Zapojenie ilustruje obrázok 3.
Z USB-TTL kábla sa môžete priamo napojiť na mikrokontrolér alebo na dutinky Grove prepojovacieho kábla. Rozloženie jeho pinov je zobrazené na obrázku 4.
Zariadenie sériovej linky
Prístup k sériovej linke je v jazyku MicroPython zabezpečený pomocou
triedy UART.
Mikrokontroléry RP2040 a RP2350 obsahujú dve rozhrania
UART označené číslami 0 a 1. To,
ktoré z nich použijeme a na ktorých pinoch bude prebiehať komunikácia,
je uvedené v module constants.py v konštantách s prefixom
UART_.
V tomto kroku prístup k sériovej linke zabalíme do samostatnej triedy.
Task
Vytvorte triedu SerialPort, ktorá implementuje
SerialMixin a je potomkom triedy
BaseDevice.
Rovnako ako ostatné zariadenia dostane trieda svoje nastavenia ako
parametre konštruktora: číslo rozhrania UART, piny pre
vysielanie a príjem a rýchlosť prenosu. V konštruktore z nich vytvorí
inštanciu triedy UART:
def __init__(self, uart_id: int, tx_pin: int, rx_pin: int, baudrate: int, alias=None):
BaseDevice.__init__(self, alias)
self.pins = {'tx': tx_pin, 'rx': rx_pin}
self.uart = UART(uart_id, tx=tx_pin, rx=rx_pin, baudrate=baudrate)Pri vytváraní inštancie potom použijete konštanty z modulu
constants.py, napr.
SerialPort(UART_ID, UART_TX_PIN, UART_RX_PIN, UART_BAUDRATE).
Mixin opisujúci správanie sériovej linky obsahuje metódy
.read(), .write() a .any(). Ich
volanie trieda SerialPort prenechá inštancii triedy UART,
ktorá ich už implementuje.
Súčasťou mixinu SerialMixin je aj implementovaná metóda
.input(). Tá je postavená na metódach .read(),
.write() a .any(). Načítava vstup zo sériovej
linky a každý prijatý znak pošle späť, aby používateľ videl, čo píše.
Túto metódu preto implementovať nemusíte.
Okrem toho bude mať trieda vlastnú metódu .deinit(),
ktorá sériovú linku uvoľní.
O triede SerialPort bude platiť nasledovné:
- jej implementácia sa bude nachádzať v module
serial_port.py, - názov zariadenia bude
"SerialPort", - opis zariadenia bude
"Serial port (UART)", - bude implementovať mixin
SerialMixina bude potomkom triedyBaseDevice, - konštruktor bude mať parametre
uart_id,tx_pin,rx_pin,baudratea voliteľnýalias, - piny uloží do členskej premennej
pinsako{'tx': tx_pin, 'rx': rx_pin}, - metódy
.read(),.write()a.any()prenechajú volanie inštancii triedyUART, - metóda
.deinit()uvoľní sériovú linku zavolaním metódy.deinit()inštancie triedyUART.
Task
Overte funkčnosť vytvorenej triedy SerialPort.
Pripojte sa k sériovej linke pomocou vhodného klienta, napr.:
- rozšírenie Serial Port Monitor pre vývojové prostredie PyCharm
- ak používate OS Windows, môžete použiť klienta PuTTY
- ak používate OS Linux, môžete použiť klienta picocom (je tiež súčasťou
Docker obrazu
bletvaska/iotbox)
Triedu si potom môžete vyskúšať v REPL jazyka MicroPython cez USB. Vytvorte inštanciu sériovej linky, pošlite do klienta správu a následne si od používateľa vyžiadajte vstup:
>>> from constants import UART_ID, UART_TX_PIN, UART_RX_PIN, UART_BAUDRATE
>>> from hw.serial_port import SerialPort
>>> port = SerialPort(UART_ID, UART_TX_PIN, UART_RX_PIN, UART_BAUDRATE)
>>> port.write('Hello from RPi Pico!\r\n')
22
>>> port.input('> ') # v klientovi napíšte hello a stlačte Enter
'hello'
>>> port.capabilities()
['SerialMixin']
>>> port.deinit()Ak ste postupovali správne, v klientovi sa zobrazí správa
"Hello from RPi Pico!" a výzva "> ". Text,
ktorý v klientovi napíšete, sa v ňom zároveň zobrazí a po stlačení
klávesu Enter ho metóda .input() vráti v
REPL ako reťazec.
Servisný terminál cez sériovú linku
Sériovú linku máme pripravenú ako samostatnú perifériu. V tomto kroku ju zaregistrujeme v správcovi zariadení a tým z nej spravíme súčasť chytrého zariadenia. Následne ju použijeme v servisnom termináli na komunikáciu s používateľom.
Task
Zaregistrujte sériovú linku v správcovi zariadení.
Sériová linka je periféria ako každá iná, preto ju zaregistrujeme
spolu s ostatnými vo funkcii register_devices(). Pri jej
vytváraní použite konštanty UART_ID,
UART_TX_PIN, UART_RX_PIN a
UART_BAUDRATE a pri registrácii jej priraďte alias
ALIAS_SERIAL.
Task
V metóde .enter() stavu ServiceTerminal si
od správcu zariadení vyžiadajte sériovú linku a uložte ju do členskej
premennej self.serial.
Uvítaciu správu v metóde .exec() potom vypíšte do
sériovej linky:
print('# Welcome to Service Terminal', file=self.serial)Ak ste postupovali správne, po reštartovaní zariadenia v servisnom režime sa vo vašom klientovi sériovej linky zobrazí uvítacia správa.
Interaktívna slučka (REPL)
REPL je akronym pre Read–Eval–Print Loop. Konzola v slučke prečíta (read) príkaz, vyhodnotí ho (eval), vypíše výsledok (print) a celý postup opakuje (loop). Práve takúto slučku spolu s niekoľkými jednoduchými príkazmi vytvoríte v tomto kroku.
Task
V metóde .exec() vytvorte nekonečnú slučku
REPL, ktorá bude zatiaľ fungovať ako jednoduché echo: načítaný
vstup len pošle späť.
V každom prechode slučkou:
- vypíšte výzvu
"> "a načítajte vstup od používateľa pomocou metódy.input()nad objektom sériovej linky, - zo vstupu odstráňte zbytočné biele znaky,
- ak je vstup prázdny, nerobte nič a pokračujte ďalším prechodom,
- inak vstup vypíšte späť do sériovej linky pomocou funkcie
print(..., file=self.serial).
Task
Overte svoju implementáciu.
Pripojte sa k zariadeniu pomocou klientskej aplikácie a reštartujte
ho v servisnom režime. Po uvítacej správe sa zobrazí výzva
"> ". Ak napíšete ľubovoľný text a stlačíte
Enter, zariadenie vám ho vráti späť.
Slučku je zatiaľ možné ukončiť len reštartovaním zariadenia, prípadne
stlačením CTRL+C v konzole REPL cez USB.
Stlačenie CTRL+C v klientovi sériovej linky ju neukončí –
do slučky príde ako obyčajný znak.
Task
Vytvorte príkaz version, ktorý zobrazí verziu firmvéru
zariadenia, a overte ho.
Je to jeden zo základných príkazov servisného terminálu, pretože
technik na zariadení zvyčajne najprv zisťuje, akú verziu firmvéru
obsahuje. Príkaz vypíše hodnoty konštánt z modulu
version.
Rozšírte slučku v metóde .exec() o vetvu pre príkaz
version. Ak zariadenie zadaný príkaz nepozná, vypíše správu
"Unknown command".
Výsledná naivná implementácia slučky môže vyzerať nasledovne:
from version import VERSION, BUILD_DATE, BOARD
# ...
def exec(self):
while True:
line = self.serial.input('> ').strip()
if line == '':
continue
elif line == 'version':
print(f'Version : {VERSION}', file=self.serial)
print(f'Build date: {BUILD_DATE}', file=self.serial)
print(f'Board : {BOARD}', file=self.serial)
else:
print('Unknown command', file=self.serial)Poznámka
Príkazy pridané v ďalších úlohách budú ďalšími vetvami
elif.
Ak ste postupovali správne, v konzole servisného terminálu sa vypíše aktuálna verzia firmvéru:
>
> hello world
Unknown command
>
> version
Version : 2026.9
Build date: 2026-10-12T21:54:57Z
Board : Raspberry Pi Pico 2 W
Task
Vytvorte príkaz na reštartovanie zariadenia a svoju implementáciu overte.
Reštartovaním sa zároveň ukončí servisný režim. Je to zatiaľ jediný spôsob, ako servisný terminál opustiť.
Zariadenie môžeme reštartovať dvoma spôsobmi:
mäkký reštart (z angl. soft reset) – reštartuje len interpreter jazyka MicroPython (vymaže premenné a znova spustí
boot.pyamain.py), zatiaľ čo mikrokontrolér beží ďalej a spojenie cez USB zostane zachovanétvrdý reštart (z angl. hard reset) – reštartuje celý mikrokontrolér rovnako ako jeho vypnutie a zapnutie, vrátane všetkých periférií, takže sa zariadenie od počítača na chvíľu odpojí
Mäkký reštart je možné vyvolať stlačením klávesovej skratky
CTRL+D v konzole REPL alebo zavolaním funkcie
machine.soft_reset(). Tvrdý reštart je možné vyvolať
funkciou machine.reset().
Vytvorte príkaz reset s jedným parametrom, ktorý určí
spôsob reštartu:
reset soft– mäkký reštart,reset hard– tvrdý reštart.
Obe varianty pridajte do slučky ako ďalšie vetvy elif.
Neznámy príkaz sa naďalej ohlási správou
"Unknown command".
Ak ste postupovali správne, zariadenie sa po zadaní ktorejkoľvek z oboch variant príkazu reštartuje.
Task
Vytvorte príkaz uptime a svoju implementáciu overte.
Pomocou príkazu uptime zistíte, ako dlho je zariadenie
spustené od posledného reštartu.
Na tento účel môžeme s výhodou použiť funkciu
time.ticks_ms(), ktorá vráti počet milisekúnd od zapnutia
mikrokontroléra. Pri tvrdom reštarte sa toto počítadlo vynuluje, pri
mäkkom reštarte však nie.
Nás ale zaujíma, ako dlho zariadenie beží od svojho posledného
reštartu – mäkkého aj tvrdého. Preto si hodnotu funkcie
ticks_ms() zapamätáme pri spustení zariadenia a uptime
vypočítame ako rozdiel aktuálnej a zapamätanej hodnoty. Do triedy
Context pridajte členskú premennú started_at,
ktorú pri inicializovaní kontextu zariadenia nastavíte na hodnotu
funkcie ticks_ms().
Upozornenie
Hodnoty z funkcie ticks_ms() neodčítajte bežným
operátorom -. Počítadlo má obmedzený rozsah a po čase
pretečie (na RPi Pico približne po 12 dňoch), čím by výsledok
odčítania stratil zmysel. Rozdiel dvoch hodnôt preto vždy počítajte
funkciou time.ticks_diff(), ktorá s pretečením počíta:
elapsed = ticks_diff(ticks_ms(), self.context.started_at)Ak ste postupovali správne, po zadaní príkazu sa zobrazí počet uplynutých milisekúnd od posledného mäkkého alebo tvrdého reštartu zariadenia.
Parser príkazov
Servisný terminál môže obsahovať množstvo príkazov, ktoré môžu mať
vlastné podpríkazy alebo ďalšie prepínače. Ak by sme pokračovali v
implementácii interpretera príkazov pomocou if-elif-else
tak, ako doteraz, náš kód by sa veľmi rýchlo stal neprehľadným a ťažko
udržiavateľným. Každý nový príkaz by znamenal ďalšiu vetvu v jednej
stále rastúcej metóde a kvôli každému z nich by sme museli meniť triedu
ServiceTerminal. To je v rozpore s princípom otvorenosti a
uzavretosti.
Tento problém vyriešime pomocou dvoch návrhových vzorov:
- Príkaz (z angl.
Command) – každý príkaz bude reprezentovaný samostatnou triedou
v balíku
commands, ktorá pozná svoj názov a opis a vie sa vykonať, - Register (z angl. Registry) – parser si príkazy zaregistruje a pre načítaný riadok vyhľadá príkaz podľa jeho názvu.
Pridanie nového príkazu bude potom znamenať len vytvorenie novej
triedy a jej zaregistrovanie – bez zásahu do servisného terminálu. A
keďže parser pozná všetky zaregistrované príkazy, môžeme ich kedykoľvek
vypísať alebo k nim zobraziť pomoc. Na tento účel neskôr vytvoríme
príkazy commands a help.
Task
Rozšírte triedu Context o členskú premennú
parser.
Súčasťou kostry chytrého zariadenia je aj trieda Parser
v module parser.py. Práve inštancia tejto triedy bude
slúžiť ako parser servisného terminálu.
Trieda Parser poskytuje tieto metódy:
.register(command)– zaregistruje príkaz pod názvom z jeho premennejname,.parse(line)– rozoberie riadok a vráti dvojicu(príkaz, parametre).
Parser implementuje návrhový vzor iterátor,
vďaka čomu dokáže pomocou cyklu for prechádzať
zaregistrované príkazy. Okrem toho pomocou operátora in
dokáže overiť, či je príkaz s daným názvom zaregistrovaný, a pomocou
operátora indexovania [] ho podľa názvu dokáže získať.
Tieto vlastnosti využijete neskôr pri tvorbe príkazov
commands a help.
>>> parser.parse('reset soft')
(<Reset object at …>, ['soft'])
>>> 'reset' in parser
True
>>> parser['reset']
<Reset object at …>Inštanciu parsera vytvorte pri inicializácii kontextu.
Task
Vytvorte príkaz version, ktorý vypíše verziu firmvéru
zariadenia.
Každý príkaz servisného terminálu je definovaný v balíku
commands a je potomkom triedy BaseCommand z
modulu base.py. V triede príkazu:
- v triednej premennej
nameuvediete názov, ktorým sa príkaz v termináli spúšťa, - v triednej premennej
descriptionuvediete jeho krátky opis, - v metóde
.__call__()implementujete samotné vykonanie príkazu; parameterparamsobsahuje zoznam parametrov, ktoré používateľ zadal za názvom príkazu, a parameteroutprúd, do ktorého príkaz zapisuje svoj výstup (predvolenesys.stdout).
O príkaze version bude platiť:
- jeho implementácia sa bude nachádzať v module
version.py, - názov triedy, ktorá ho bude implementovať, bude
Version, - názov príkazu bude
"version", - opis príkazu bude
"Show firmware version."
Ku kontextu zariadenia má príkaz prístup cez členskú premennú
self.context, ktorú nastaví konštruktor triedy
BaseCommand.
Poznámka
Metóda .__call__()
je v jazyku Python špeciálna. Ak ju trieda implementuje, jej
inštancia sa stane tzv. callable
objektom – dá sa zavolať rovnako ako funkcia. Príkaz cmd
preto namiesto cmd.__call__(params) vykonáme jednoducho ako
cmd(params), prípadne cmd(params, out), ak
chceme jeho výstup poslať inam ako na štandardný výstup.
Implementácia príkazu version môže vyzerať
nasledovne:
import sys
from version import VERSION, BUILD_DATE, BOARD
from .base import BaseCommand
class Version(BaseCommand):
name = 'version'
description = 'Show firmware version.'
def __call__(self, params: list, out=sys.stdout) -> None:
print(f'Version : {VERSION}', file=out)
print(f'Build date: {BUILD_DATE}', file=out)
print(f'Board : {BOARD}', file=out)Telo metódy .__call__() je takmer rovnaké ako vetva
elif z naivnej implementácie. Príkaz však nepíše priamo do
sériovej linky, ale do prúdu out, ktorý mu odovzdá ten, kto
ho volá. Príkaz tak nemusí vedieť, kam jeho výstup smeruje.
Task
Vytvorte príkaz devices, ktorý zobrazí zoznam všetkých
zariadení zaregistrovaných v správcovi zariadení.
O tomto príkaze bude platiť:
- jeho implementácia sa bude nachádzať v module
devices.py, - názov triedy, ktorá ho bude implementovať, bude
Devices, - názov príkazu bude
"devices", - opis príkazu bude
"List registered devices."
Správca zariadení je dostupný v kontexte
(self.context.devices) a implementuje návrhový vzor iterátor,
takže zaregistrované zariadenia môžete prechádzať cyklom
for. Informácie o každom zariadení vráti jeho metóda
.info().
Príkaz zaregistrujete a overíte spolu s ostatnými príkazmi v nasledujúcich úlohách. Jeho výstup by mal byť podobný tomuto:
> devices
[0] DHT22 (temp, humidity)
description : Temperature and humidity sensor
pins : data=3
capabilities: TemperatureMixin, HumidityMixin
[1] WS2812B (diag_led)
description : RGB LED controller
pins : data=18
capabilities: OnOffMixin, ColorlightMixin
[2] Button (service_btn)
description : Push button
pins : data=15
capabilities: ButtonMixin
[3] SerialPort (serial)
description : Serial port (UART)
pins : tx=16, rx=17
capabilities: SerialMixin
Task
Vytvorte implementáciu príkazov uptime a
reset ako triedy Uptime a Reset v
balíku commands.
Postupujte podobne ako v prípade príkazu version: každý
príkaz vytvorte v samostatnom module ako potomka triedy
BaseCommand, nastavte mu názov a opis a jeho vykonanie
presuňte z vetvy elif do metódy .__call__().
Pri implementácii myslite na to, že:
- príkaz
uptimepotrebuje kontext zariadenia – čas spustenia nájde v členskej premennejself.context.started_at, - príkaz
resetdostane spôsob reštartu (softalebohard) ako prvú položku zoznamuparams.
O príkaze uptime bude platiť:
- jeho implementácia sa bude nachádzať v module
uptime.py, - názov triedy, ktorá ho bude implementovať, bude
Uptime, - názov príkazu bude
"uptime", - opis príkazu bude
"Show time elapsed since last restart."
O príkaze reset bude platiť:
- jeho implementácia sa bude nachádzať v module
reset.py, - názov triedy, ktorá ho bude implementovať, bude
Reset, - názov príkazu bude
"reset", - opis príkazu bude
"Reset the device (soft or hard)."
Task
Zaregistrujte vytvorené príkazy version,
devices, uptime a reset do
parsera v metóde .enter() servisného terminálu.
Parser pozná len tie príkazy, ktoré sú v ňom zaregistrované. Preto je potrebné všetky príkazy pred ich použitím najprv zaregistrovať.
Príkazy zaregistrujte v metóde .enter() stavu servisného
terminálu, teda ešte predtým, ako sa v metóde .exec()
spustí jeho slučka. Registráciu príkazov ilustruje nasledovný fragment
kódu:
from commands.version import Version
from commands.devices import Devices
from commands.uptime import Uptime
from commands.reset import Reset
# ...
parser = self.context.parser
parser.register(Version(self.context))
parser.register(Devices(self.context))
parser.register(Uptime(self.context))
parser.register(Reset(self.context))Task
Aktualizujte REPL slučku vášho servisného terminálu.
Upravte slučku v metóde .exec() tak, aby namiesto vetiev
elif vyhľadala príkaz pomocou metódy .parse()
parsera a nájdený príkaz zavolala s jeho parametrami. Ako výstupný prúd
mu odovzdajte sériovú linku self.serial, aby výstup príkazu
smeroval do servisného terminálu.
Rozpoznávanie príkazov teraz prevzal parser, takže slučka o konkrétnych príkazoch nič nevie. Zodpovedá len za načítanie riadka a spustenie nájdeného príkazu. Vykonanie príkazu je zodpovednosťou jeho triedy, čo je podstatou princípu jedinej zodpovednosti (SRP). Nový príkaz pridáme vytvorením novej triedy a jej registráciou, bez zmeny slučky. Slučka je tak otvorená pre rozširovanie, ale uzavretá voči zmenám, ako to vyžaduje princíp otvorenosti a uzavretosti (OCP).
Task
Overte správnosť svojej implementácie.
Ak ste postupovali správne, po prechode zariadenia do servisného
terminálu budú príkazy version, uptime a
reset fungovať rovnako ako pred úpravou a k dispozícii bude
aj nový príkaz devices.
Vyskúšajte aj stlačiť len Enter alebo zadať príkaz,
ktorý neexistuje. Zariadenie sa zachová správne – na prázdny riadok
nezareaguje a na neznámy príkaz odpovie správou
"Unknown command: <názov>" – hoci ste v slučke žiadnu
podmienku pre tieto prípady nenapísali. Prečo to tak je, si vysvetlíme v
nasledujúcom kroku.
Príkaz measure
Chytré zariadenie meria teplotu a vlhkosť v stave
Measurement. Technik však niekedy potrebuje zistiť aktuálne
hodnoty hneď, napríklad keď overuje, či je senzor správne zapojený,
alebo porovnáva namerané hodnoty s referenčným teplomerom. V tomto kroku
preto vytvoríme príkaz measure, ktorý meranie vykoná na
požiadanie priamo zo servisného terminálu.
Rovnakú operáciu potrebujeme na zariadení vykonávať na dvoch
miestach: v stave Measurement a v príkaze
measure. Neskôr môžu dokonca pribudnúť ďalšie, napr. webové
rozhranie zariadenia. Pri tvorbe príkazu bude preto potrebné, aby bola
logika merania napísaná len raz a aby ju mohol použiť ktokoľvek, kto ju
potrebuje.
Task
Vytvorte a overte príkaz measure, ktorý zmeria a vypíše
aktuálnu teplotu a vlhkosť.
O tomto príkaze bude platiť:
- jeho implementácia sa bude nachádzať v module
measure.py, - názov triedy, ktorá ho bude implementovať, bude
Measure, - názov príkazu bude
"measure", - opis príkazu bude
"Measure temperature and humidity."
Príkaz zaregistrujte rovnako ako ostatné príkazy.
Senzory teploty a vlhkosti získate od správcu zariadení pomocou
aliasov ALIAS_TEMP a ALIAS_HUMIDITY, rovnako
ako v stave Measurement.
Ak ste postupovali správne, po zadaní príkazu sa zobrazia aktuálne namerané hodnoty:
> measure
Temperature : 23.4 °C
Humidity : 41 %
Task
Upravte stav Measurement tak, aby na meranie použil
príkaz measure.
Cieľom je, aby bol kód merania v zariadení napísaný len na jednom mieste. Stav aj príkaz dostávajú v konštruktore kontext, takže príkaz môžete v stave vytvoriť a zavolať rovnako, ako ho volá servisný terminál.
Príkaz je síce možné zo stavu zavolať, ale namerané hodnoty len vypíše, a stav s nimi preto nemôže ďalej pracovať. Jediný spôsob, ako sa k nim stav dostane, je, že meranie vykoná sám, teda skopíruje kód merania z príkazu. Kód merania by tak bol v zariadení dvakrát, čím by sme porušili princíp DRY (z angl. Don’t Repeat Yourself). Príčinou je to, že príkaz má dve zodpovednosti – vykonáva meranie a zároveň vypisuje jeho výsledok – čo je v rozpore s princípom jedinej zodpovednosti (SRP).
V triede príkazu teda rozdelíme zodpovednosti:
- vytvoríme samostatnú metódu
.measure(), ktorá vykoná meranie a namerané hodnoty vráti (napr. ako slovník), ale nič nevypíše, a - metóda
.__call__()zavolá metódu.measure()a vrátené hodnoty vypíše.
Metódu, ktorá vykoná samotnú činnosť príkazu, vráti výsledok a nič nevypíše, budeme nazývať operácia príkazu.
Príkaz potom môže vyzerať nasledovne:
import sys
from constants import ALIAS_TEMP, ALIAS_HUMIDITY
from hw.mixins.sensors.temperature import TemperatureUnit
from .base import BaseCommand
class Measure(BaseCommand):
name = 'measure'
description = 'Measure temperature and humidity.'
def measure(self) -> dict:
unit = TemperatureUnit.METRIC
return {
'temperature': self.context.devices.get(ALIAS_TEMP).temperature(unit),
'unit': unit,
'humidity': self.context.devices.get(ALIAS_HUMIDITY).humidity(),
}
def __call__(self, params: list, out=sys.stdout) -> None:
values = self.measure()
print(f"Temperature : {values['temperature']} °C", file=out)
print(f"Humidity : {values['humidity']} %", file=out)Stav Measurement potom použije len metódu
.measure() a s nameranými hodnotami môže ďalej
pracovať:
from commands.measure import Measure
# ...
class Measurement(BaseState):
# ...
def exec(self) -> BaseState:
values = Measure(self.context).measure()
print(f"Temperature: {values['temperature']}°C | Humidity: {values['humidity']}%")
return Sleep(self.context)Kód merania je tak v zariadení len raz – v metóde
.measure(). Ak ho bude potrebné zmeniť, zmena sa prejaví v
stave aj v príkaze naraz.
Ak ste postupovali správne, príkaz measure bude v
servisnom termináli fungovať rovnako ako pred úpravou a zariadenie v
stave Measurement vypíše namerané hodnoty rovnako ako
doteraz.
Task
Rozdeľte na operáciu a výpis aj ďalšie príkazy, pri ktorých to má zmysel.
Operácia má zmysel tam, kde by výsledok príkazu mohol potrebovať aj
niekto iný ako servisný terminál, napríklad stav zariadenia. Operáciu,
ktorá vracia informácie, pomenujte podľa toho, čo vracia (napr.
.get() alebo .list()), a operáciu, ktorá
vykonáva činnosť, pomenujte slovesom (napr. .measure()
alebo .connect()).
Špeciálne príkazy
Okrem príkazov určených na správu zariadenia bude servisný terminál
obsahovať aj príkazy, ktoré používateľovi pomôžu zorientovať sa v
samotnom termináli. Na tento účel vytvoríme príkazy
commands a help.
V balíku commands sa však už od začiatku nachádzajú aj
dve pripravené triedy – EmptyCommand a
UnknownCommand. Práve vďaka nim servisný terminál správne
zareaguje na prázdny riadok aj na neznámy príkaz. V tomto kroku sa na ne
pozrieme bližšie.
Task
Vytvorte a overte príkaz commands, ktorý zobrazí zoznam
všetkých zaregistrovaných príkazov spolu s ich krátkym opisom.
O tomto príkaze bude platiť:
- jeho implementácia sa bude nachádzať v module
commands.py, - názov triedy, ktorá ho bude implementovať, bude
Commands, - názov príkazu bude
"commands", - opis príkazu bude
"Show list of available commands."
Príkaz zaregistrujte rovnako ako ostatné príkazy.
Zaregistrované príkazy je možné získať z inštancie parsera, ktorý
implementuje návrhový vzor iterátor.
Pri výpise využite triedne premenné name a
description jednotlivých príkazov.
Ak ste postupovali správne, po zadaní príkazu sa zobrazí zoznam podobný tomuto:
> commands
List of available commands:
version Show firmware version.
devices List registered devices.
uptime Show time elapsed since last restart.
reset Reset the device (soft or hard).
commands Show list of available commands.
Task
Vytvorte a overte príkaz help, ktorý zobrazí opis
zadaného príkazu.
O tomto príkaze bude platiť:
- jeho implementácia sa bude nachádzať v module
help.py, - názov triedy, ktorá ho bude implementovať, bude
Help, - názov príkazu bude
"help", - opis príkazu bude
"Show usage of a given command."
Príkaz zaregistrujte rovnako ako ostatné príkazy.
Názov príkazu, ktorého použitie chce používateľ zobraziť, dostane ako
svoj jediný parameter. Ak príkaz s takým názvom nie je zaregistrovaný,
vypíše správu "Command not found."
Ak príkaz so zadaným názvom zaregistrovaný je, vypíše jeho názov a
opis v tvare <názov>: <opis>. Ak používateľ
zadá nesprávny počet parametrov, vypíše správu
"Wrong number of parameters.".
Ak ste postupovali správne, výpis opisu príkazov bude vyzerať podobne ako tento:
> help
Wrong number of parameters.
> help blabla
Command not found.
> help version
version: Show firmware version.
> help reset
reset: Reset the device (soft or hard).
Task
Preskúmajte triedy EmptyCommand a
UnknownCommand a metódu .parse() triedy
Parser.
Servisný terminál zareaguje na prázdny riadok a neznámy príkaz správne, hoci jeho slučka o týchto prípadoch nič nevie. Odpoveď na otázku, prečo je to tak, sa skrýva v kóde, ktorý ste dostali spolu s kostrou zariadenia. V tejto úlohe ju nájdete sami.
Aby ste riešeniu lepšie porozumeli, pokúste sa nájsť odpovede na tieto otázky:
- Čo vráti metóda
.parse()pre prázdny riadok a čo pre príkaz, ktorý nie je zaregistrovaný? - Čo sa stane, keď slučka servisného terminálu takýto príkaz zavolá?
- Prečo sa tieto príkazy nezobrazia vo výpise príkazu
commands? - Prečo nie sú tieto príkazy v parseri zaregistrované?
Svoje odpovede si môžete overiť priamo v REPL jazyka MicroPython. Stačí vytvoriť inštanciu parsera a neregistrovať do nej žiadne príkazy:
>>> from parser import Parser
>>> parser = Parser()
>>> parser.parse('')
(<EmptyCommand object at …>, [])
>>> parser.parse('blabla')
(<UnknownCommand object at …>, [])Ak ste na otázky našli odpovede, zistili ste, že parser nikdy nevráti
hodnotu None. Aj pre prázdny riadok či neznámy príkaz vráti
plnohodnotný príkaz, ktorý sa dá zavolať rovnako ako ktorýkoľvek iný.
Obe triedy sú teda ukážkou návrhového vzoru Null Object.
Namiesto hodnoty None, ktorú by musel každý, kto parser
používa, kontrolovať, vráti parser objekt s rovnakým rozhraním ako
ostatné príkazy. Ten sa v danej situácii zachová správne: prázdny príkaz
neurobí nič a neznámy príkaz vypíše chybovú správu.
Ani jeden z týchto príkazov však nie je v parseri zaregistrovaný.
Parser ich pri každom volaní metódy .parse() vytvorí ako
novú inštanciu, takže ich používateľ nemôže zavolať podľa názvu a
nezobrazia sa ani vo výpise príkazu commands. Príkaz
UnknownCommand navyše pri vytvorení dostane názov neznámeho
príkazu, aby ho mohol vypísať v chybovej správe.
Keďže parser vždy vráti príkaz, ktorý sa dá zavolať, slučka servisného terminálu nepotrebuje žiadnu podmienku:
cmd, params = parser.parse(line)
cmd(params, self.serial)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
Upravte implementáciu príkazu
uptimetak, aby výsledok zobrazoval podobným spôsobom, ako je to v prípade príkazuuptimev linuxových systémoch.Rozšírte príkaz
reseto kontrolu servisného pinu. Ak jeSVC_PINv čase zadania príkazu stále uzemnený, zariadenie by sa po reštarte znova ocitlo v servisnom termináli. V takom prípade reštart nevykonajte a upozornite používateľa vhodnou správou, že pred reštartom je potrebné servisný pin odpojiť, napr.:> reset soft SVC_PIN is still grounded, disconnect it first. >Ak
SVC_PINuzemnený nie je, príkaz zariadenie reštartuje rovnako ako doteraz. Stav pinu zistite rovnakým spôsobom ako v staveInit.Rozšírte príkaz
helptak, aby okrem názvu a opisu príkazu zobrazil aj spôsob jeho použitia.Spôsob použitia príkazu je uložený v triednej premennej
usagerodičovskej triedy. Ak príkaz túto premennú nenastaví, má hodnotuNone. Inak obsahuje n-ticu dvojíc(použitie, opis). Pre príkazyresetahelpju nastavte na tieto hodnoty:# Reset usage = ( ('reset soft', 'Soft reset.'), ('reset hard', 'Hard reset.'), ) # Help usage = ( ('help <cmd>', 'Show usage of the command <cmd>.'), )Ak má príkaz premennú
usagenastavenú, príkazhelppod riadok s jeho názvom a opisom vypíše všetky možnosti jeho použitia. Príklad takéhoto výpisu môže vyzerať nasledovne:> help reset reset: Reset the device (soft or hard). Usage: reset soft Soft reset. reset hard Hard reset.Ak používateľ zadá príkaz
helps nesprávnym počtom parametrov, vypíše príkazhelpaj spôsob svojho použitia:> help Wrong number of parameters. help: Show usage of a given command. Usage: help <cmd> Show usage of the command <cmd>.Ak sa niektorý príkaz spustí s nesprávnym parametrom, vypíše chybovú správu s textom
"Error: Wrong usage."a odkáže používateľa na príkazhelp:> reset blabla Error: Wrong usage. Type 'help reset' for usage.
Ďalšie zdroje
Quick reference for the RP2 – (krátka) dokumentácia jazyka MicroPython pre dosku Raspberry Pi Pico
class
UART– duplex serial communication bus – UART implements the standard UART/USART duplex serial communications protocol.Reštartovanie zariadenia – mäkký a tvrdý reštart mikrokontroléra v jazyku MicroPython
Iterátor – návrhový vzor iterátor