Lab #6: Webové rozhranie zariadenia
o prístupovom bode, webovom rámci Microdot, šablónach a konfigurácii zariadenia cez webové rozhranie
Motivácia
Ciele
naučiť sa vytvoriť z chytrého zariadenia prístupový bod
naučiť sa vytvoriť webové rozhranie pomocou rámca Microdot a šablón
naučiť sa overiť konfiguráciu zariadenia pred jeho uvedením do prevádzky
naučiť sa ochrániť prístup k nastaveniam heslom
Postup
Prístupový bod
Pri prvom spustení zariadenie nemá žiadnu konfiguráciu, takže sa nevie pripojiť k žiadnej WiFi sieti. WiFi modul preto prejde do režimu prístupového bodu (z angl. access point, AP) a vytvorí vlastnú WiFi sieť, ku ktorej sa používateľ pripojí telefónom alebo počítačom.
V tomto kroku vytvoríme nové zariadenie, ktoré bude reprezentovať práve prístupový bod. Na prácu s ním vytvoríme aj samostatný príkaz pre servisný terminál.
Prácu s prístupovým bodom opisuje kapitola Práca s WiFi modulom.
Task
Vytvorte triedu zariadenia, ktoré bude reprezentovať prístupový bod WiFi modulu.
Trieda network.WLAN pracuje s jedným z dvoch sieťových
rozhraní WiFi modulu, ktoré určíte konštantou pri vytvorení objektu.
Trieda CYW43439, ktorú už máte, používa rozhranie
network.STA_IF na pripojenie k existujúcej sieti.
Prístupový bod bude používať rozhranie network.AP_IF, cez
ktoré zariadenie vytvorí vlastnú sieť. Rozhrania môžu byť zapnuté
naraz.
O triede bude platiť nasledovné:
- jej implementácia sa bude nachádzať v module
cyw43439_ap.py, - názov triedy bude
CYW43439AP, - názov zariadenia bude
"CYW43439 AP", - opis zariadenia bude
"Access point of the built-in WiFi module (Raspberry Pi Pico 2 W)", - bude implementovať mixin
NetworkMixina bude potomkom triedyBaseDevice.
Zariadenie zaregistrujte v správcovi zariadení pod aliasom
ALIAS_AP.
Poznámka
Pri senzore DHT sme jednu perifériu opísali jednou triedou s
dvomi mixinmi, a tu z jedného čipu vznikli dve triedy. Rozdiel je v tom,
že teplota a vlhkosť sú dve rôzne schopnosti jedného senzora, kým tu ide
o rovnakú schopnosť dvakrát. Každé rozhranie má vlastnú IP adresu a
nastavenia a nezávisle sa zapína. V jednej triede by metódy mixinu
NetworkMixin, ako napr. .active(), nevedeli,
ktoré rozhranie majú použiť.
Task
V module commands/ap.py vytvorte triedu AP,
ktorá bude reprezentovať príkaz ap.
O tejto triede bude platiť:
- bude potomkom triedy
BaseCommand, - názov príkazu bude
"ap", - opis príkazu bude
"Manage access point.", - prvým parametrom príkazu bude názov podpríkazu (
start,stopalebostat), - ak príkaz nedostane žiadny parameter alebo dostane neznámy
podpríkaz, vypíše správu
"Error: Wrong usage.".
Jednotlivé podpríkazy implementujte ako operácie príkazu .start(),
.stop() a .stat(), pretože ich neskôr použije
aj stav Configuration. Operácia .stat() vráti
slovník so stavom prístupového bodu. Ak je zapnutý, bude obsahovať aj
názov siete, zabezpečenie, IP adresu a počet pripojených zariadení,
ktorý zistíte pomocou .status('stations').
Príkaz zaregistrujte rovnako ako ostatné príkazy.
Task
Rozšírte používateľské nastavenia o heslo prístupového bodu.
Heslo bude v novej triede AP v členskej premennej
password s predvolenou hodnotou None. V triede
Settings bude trieda dostupná v členskej premennej
ap. Vytvorte aj validátor, ktorý overí, že heslo má aspoň 8
znakov, pretože kratšie heslo protokol WPA2 nepovoľuje.
Task
Vytvorte v príkaze ap operácie .ssid() a
.password(), ktoré vrátia názov a heslo siete
zariadenia.
Názov siete bude v tvare
<device_type>-<device_id>, napr.
thsensor-ab123cd, aby sa zariadenia v jednej miestnosti
dali rozlíšiť. Za <device_id> použite konštantu
DEVICE_ID z modulu identity.py.
Operácia .password() vráti heslo z nastavení. Ak
nastavenia ešte neexistujú alebo heslo nie je nastavené, vráti hodnotu
None. V takom prípade operácia .start()
vytvorí otvorenú sieť bez hesla
(security=network.WLAN.SEC_OPEN), inak sieť zabezpečenú
heslom (security=network.WLAN.SEC_WPA_WPA2).
Upozornenie
Otvorená sieť je vedomý kompromis. Pri prvom spustení zariadenie žiadne heslo nepozná a potrebujeme sa k nemu pripojiť. V otvorenej sieti však môže ktokoľvek v dosahu zachytiť všetko, čo do formulára zadáte, a zariadenie môže nastaviť niekto iný ako vy. Preto bude otvorená sieť zapnutá len krátko a len pri prvom spustení. Používateľ si hneď pri prvom pripojení bude musieť nastaviť heslo, ako uvidíte v ďalších krokoch.
Task
Overte funkčnosť prístupového bodu.
V servisnom termináli zapnite prístupový bod:
> ap start
Access point thsensor-ab123cd started.
> ap stat
Active : yes
SSID : thsensor-ab123cd
Security : open
IP : 192.168.4.1
Clients : 0
Vyskúšajte oba prípady: bez hesla a po nastavení hesla podpríkazom
settings set ap.password <heslo>. Pripojte sa k sieti
telefónom alebo počítačom a overte, že zariadenie odpovedá na adrese
192.168.4.1 (napr. príkazom ping) a že v
položke Clients pribudlo pripojené zariadenie. Nakoniec
prístupový bod vypnite podpríkazom ap stop.
Stav
Configuration
Zariadenie sa v tomto stave prepne do režimu prístupového
bodu s webovým rozhraním, kde môže používateľ nastaviť správanie
zariadenia, ako napr. údaje na pripojenie do WiFi siete a k
MQTT brokeru. Počas zotrvávania v tomto stave bude diagnostická
LED dióda svietiť farbou Color.ORANGE.
Zariadenie do stavu Configuration prejde zo stavu
Init (pozri obrázok 1), ak nastavenia chýbajú alebo sú
poškodené, alebo po krátkom stlačení ovládacieho tlačidla.
Po ukončení konfigurácie zariadenie overí novú konfiguráciu prechodom
do stavu Commissioning. V tomto kroku vytvoríme tento stav,
zabezpečíme prechod doň a zapneme v ňom prístupový bod.
---
# 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("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Init</div>")
init --> measurement("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Measurement</div>")
init -- "<code>SVC_PIN</code><br>grounded" --> terminal("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Service<br>Terminal</div>")
init -- "long button<br>press" --> reset("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Factory<br>Reset</div>")
init -- "no/faulty<br><code>settings.json</code> /<br>short button press" --> configuration("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Configuration</div>")
measurement --> connect("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Connect<br>Network</div>")
connect -- "connected" --> publish("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Publish<br>Data</div>")
connect -- "connection<br>failed" --> disconnect("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Disconnect<br>Network</div>")
publish --> disconnect
disconnect --> sleep("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Sleep</div>")
sleep --> sleepEnd@{ shape: fr-circ }
terminal --> terminalEnd@{ shape: fr-circ }
reset --> resetEnd@{ shape: fr-circ }
configuration --> configurationEnd@{ 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,connect,publish,disconnect,sleep state
class terminal service
class reset reset
class configuration config
class start initial
class sleepEnd,terminalEnd,resetEnd,configurationEnd final
Obr. 1: Stavový diagram zariadenia so stavom
Configuration
Task
Vytvorte stav Configuration, ktorý zapne prístupový
bod.
Stav vytvorte v module configuration.py balíka
states. V metóde .enter() rozsvieťte
diagnostickú LED diódu a zapnite prístupový bod. Metóda
.exec() zatiaľ len vráti hodnotu None, takže
sa stavový stroj hneď ukončí.
Poznámka
Prístupový bod po skončení programu zostane zapnutý. Beží totiž na
samostatnom WiFi čipe, ktorý pracuje nezávisle od programu v
mikrokontroléri. Vypne sa až volaním .active(False) alebo
tvrdým reštartom, mäkký reštart ho nevypne (pozri Práca
s WiFi modulom).
Task
Zabezpečte prechod zo stavu Init do stavu
Configuration.
Zariadenie prejde do stavu Configuration za týchto
podmienok:
- ak súbor s nastaveniami neexistuje,
- ak je súbor s nastaveniami poškodený,
- po krátkom stlačení ovládacieho tlačidla.
Za krátke stlačenie považujeme stlačenie aspoň na čas
SHORT_PRESS_DURATION, ale kratšie ako
LONG_PRESS_DURATION.
V prvých dvoch prípadoch vytvorte predvolené nastavenia, aby ich mal
stav Configuration k dispozícii, a prejdite do stavu
Configuration:
try:
SettingsCommand(self.context).load()
except Exception as ex:
print(f'Settings not loaded: {ex}')
self.context.settings = Settings()
return Configuration(self.context)Poznámka
Stav Configuration bude neskôr používať webový rámec,
ktorý zaberá pamäť. V bežnom cykle zariadenia ho netreba, preto stav
Configuration importujte až vo funkcii, kde ho
potrebujete.
Task
Upravte stav FactoryReset tak, aby len zmazal nastavenia
a reštartoval zariadenie.
Predvolené nastavenia už nevytvára. Po reštarte stav
Init zistí, že súbor s nastaveniami chýba, a prejde do
stavu Configuration.
Task
Overte správnosť svojej implementácie.
Pri overovaní nesmie byť uzemnený pin SVC_PIN, inak
zariadenie prejde do servisného terminálu. Vyskúšajte tieto
situácie:
- krátke stlačenie tlačidla pri štarte zariadenia,
- chýbajúci súbor s nastaveniami,
- poškodený súbor s nastaveniami,
- dlhé stlačenie tlačidla, po ktorom zariadenie zmaže nastavenia a
prejde do stavu
Configuration, - štart bez stlačenia tlačidla s platnými nastaveniami, ktorý prejde
do stavu
Measurement.
Vo všetkých situáciách s prechodom do stavu
Configuration sa pripojte telefónom alebo počítačom do
siete zariadenia a overte, že odpovedá na adrese
192.168.4.1, napr. príkazom ping.
Webový server
Keď sa používateľ pripojí do siete zariadenia, potrebuje na ňom
niečo, čo si otvorí vo webovom prehliadači. Zariadenie preto bude
poskytovať webový server. Vytvoríme ho pomocou rámca Microdot, ktorý je navrhnutý
pre zariadenia s malým množstvom pamäte. Knižnicu nájdete v priečinku
lib/ kostry projektu, takže ju netreba inštalovať.
Poznámka
Aby prostredie PyCharm rozpoznalo importy
z priečinka lib/ a ponúkalo dopĺňanie kódu, označte
priečinok src/lib ako zdrojový priečinok (pravé tlačidlo
myši na priečinku → Mark Directory as → Sources Root). Na
zariadení to nič nemení, MicroPython hľadá moduly v priečinku
/lib automaticky.
Webová aplikácia sa skladá z trás (z angl. route). Trasa je
adresa spolu s metódou HTTP, napr. GET /. Každú
trasu spracuje obsluha trasy (z angl. route
handler), teda asynchrónna funkcia označená dekorátorom, ktorá
dostane požiadavku a vráti odpoveď. Môže to vyzerať nasledovne:
@app.get('/ping') # dekorátor: určuje trasu
async def ping(request): # obsluha trasy: funkcia s požiadavkou v parametri
return 'pong' # odpoveď pre klientaDekorátor @app.get() zaregistruje funkciu pod ním ako
obsluhu trasy. Keď klient pošle požiadavku na adresu /ping,
rámec zavolá funkciu ping() a jej návratovú hodnotu pošle
ako odpoveď. Názov funkcie sa pritom nemusí zhodovať s adresou trasy. O
tom, ktorá funkcia sa zavolá, rozhoduje len dekorátor. Viac sa dozviete
v dokumentácii webového
rámca.
Task
Vytvorte v module routes.py balíka web
webovú aplikáciu s úvodnou stránkou.
Vytvorte objekt app triedy Microdot a
obsluhu trasy /, ktorá vráti krátky text. Môže to vyzerať
nasledovne:
from microdot import Microdot
app = Microdot()
@app.get('/')
async def index(request):
return 'Hello from the smart device!'Task
Rozšírte stav Configuration o spustenie webového
servera.
V metóde .exec() spustite webový server a až potom
vráťte hodnotu None. Doplňte aj metódu
.exit(), ktorá prístupový bod vypne operáciou
.stop() príkazu ap. Tieto metódy môžu vyzerať
nasledovne:
from web.routes import app
# ...
def exec(self) -> BaseState:
app.run(port=80)
return None
def exit(self):
AP(self.context).stop()Poznámka
Metóda .run() blokuje vykonávanie programu, kým sa
server neukončí. Korektné ukončenie webového servera vyriešime neskôr.
Zatiaľ ho ukončíte klávesovou skratkou CTRL+C. Metóda
.exit() sa vtedy nezavolá, preto prístupový bod zostane
zapnutý.
Task
Overte funkčnosť úvodnej stránky.
Prejdite do stavu Configuration, pripojte sa do siete
zariadenia a otvorte vo webovom prehliadači adresu
http://192.168.4.1/. Ak ste postupovali správne, zobrazí sa
text z úvodnej stránky.
Task
Vytvorte obsluhu trasy /static/<path:path>, ktorá
bude posielať súbory z priečinka web/static/.
Na odoslanie súboru použite funkciu send_file() z
knižnice Microdot.
Parametrom max_age povoľte prehliadaču súbor si uložiť, aby
ho zariadenie neposielalo pri každej stránke znova. Súbor so štýlom
pico.min.css je v tomto priečinku súčasťou kostry
projektu.
Ak súbor neexistuje, funkcia send_file() vyvolá výnimku
OSError. Zachyťte ju a vráťte kód 404.
Upozornenie
Parameter path zadáva používateľ. Ak by obsahoval
.., mohol by sa dostať mimo priečinka
web/static/, napríklad k súboru
/data/settings.json s heslami. Preto ak cesta obsahuje
.., vráťte kód 404.
Obsluha trasy môže vyzerať nasledovne:
from microdot import Microdot, send_file
# ...
@app.get('/static/<path:path>')
async def static(request, path):
# directory traversal is not allowed
if '..' in path:
return 'Not found', 404
try:
return send_file(f'web/static/{path}', max_age=86400)
except OSError:
return 'Not found', 404Task
Overte funkčnosť trasy pre statické súbory.
Prejdite do stavu Configuration a overte, že:
- adresa
http://192.168.4.1/static/pico.min.cssvráti súbor so štýlom, - adresa neexistujúceho súboru vráti kód
404, - pokus o prístup k súboru s nastaveniami vráti kód
404.
Prehliadač sekvenciu .. z adresy sám odstráni, preto
tretiu možnosť overte z príkazového riadku, napr. pomocou nástroja
curl:
$ curl --path-as-is -i http://192.168.4.1/static/../../data/settings.jsonŠablónovací systém
Rámec Microdot podporuje šablóny (z angl. template), vďaka ktorým oddelíme kód HTML od kódu v jazyku MicroPython. Pohľad na údaje je tak oddelený od logiky aplikácie, čo je základná myšlienka návrhového vzoru Model-View-Controller (skrátene MVC).
Na prácu so šablónami použijeme knižnicu utemplate,
ktorá je súčasťou kostry projektu. Šablóna sa pri prvom použití preloží
do modulu jazyka MicroPython, ktorý sa uloží vedľa nej, napr.
index_html.py. Zobrazenie stránky je preto rýchle aj na
mikrokontroléri.
Vzhľad stránok zabezpečí CSS rámec Pico CSS, ktorý nepotrebuje takmer žiadne triedy v kóde HTML.
Task
Vytvorte priečinok web/templates/ a inicializujte v
module routes.py šablónovací systém.
Priečinok web/templates/ bude obsahovať všetky šablóny
webového rozhrania. Rámcu Microdot je potrebné
oznámiť, kde sa tieto šablóny nachádzajú. Treba tiež nastaviť predvolený
typ odpovede na HTML, aby prehliadač vrátené stránky správne
zobrazil. To zabezpečíme týmto fragmentom kódu:
from microdot import Microdot, Response, send_file
from microdot.utemplate import Template
app = Microdot()
Response.default_content_type = 'text/html'
Template.initialize(template_dir='web/templates')Task
Vytvorte šablónu index.html a zobrazte ju na úvodnej
stránke.
Šablóna index.html bude zatiaľ vyzerať takto:
{% args title %}
<h1>{{ title }}</h1>Šablóna začína príkazom args,
ktorý určí, aké hodnoty dostane. Hodnotu do textu vložíte v dvojitých
zložených zátvorkách, napr. {{ title }}.
V obsluhe trasy / šablónu zobrazte a odovzdajte jej
hodnotu:
@app.get('/')
async def index(request):
return Template('index.html').render(title='Smart device')Overte, že sa v stave Configuration na adrese
http://192.168.4.1/ zobrazí nadpis.
Task
Vytvorte šablóny header.html a footer.html
so spoločnou časťou stránok a použite ich v šablóne
index.html.
Knižnica utemplate
nepodporuje dedenie šablón, ale príkazom include
vie vložiť inú šablónu. Spoločný začiatok a koniec stránok tak napíšete
len raz. Hlavička bude dostávať názov stránky ako parameter:
{% args title %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="/static/pico.min.css">
<title>{{ title }}</title>
</head>
<body>
<main class="container">
<nav>
<ul>
<li><strong>{{ title }}</strong></li>
</ul>
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>Pätička uzavrie otvorené značky:
</main>
</body>
</html>V šablóne index.html ich použite takto:
{% args title %}
{% include "header.html" title %}
<h1>{{ title }}</h1>
{% include "footer.html" %}Ak ste postupovali správne, stránka bude mať štýl Pico CSS a navigáciu.
Task
Sprístupnite obsluhám trás kontext zariadenia.
Objekt app vzniká pri importe modulu, a preto o kontexte
nevie. Stránky ho však budú potrebovať, napr. na prácu s nastaveniami
alebo zariadeniami. Preto ho v stave Configuration uložte
do objektu app ešte pred spustením servera:
def exec(self) -> BaseState:
app.context = self.context
app.run(port=80)
return NoneObsluha trasy sa ku kontextu dostane pomocou
request.app.context.
Task
Zobrazte na úvodnej stránke v tabuľke identifikátor zariadenia, názov siete a verziu softvéru.
Názov siete získate operáciou .ssid() príkazu
ap a verziu z konštanty VERSION. Do šablóny
odovzdajte zoznam dvojíc (názov, hodnota) a zobrazte ich v
cykle:
{% args title, items %}
{% include "header.html" title %}
<table>
{% for name, value in items %}
<tr>
<th>{{ name }}</th>
<td>{{ value }}</td>
</tr>
{% endfor %}
</table>
{% include "footer.html" %}Ak ste postupovali správne, úvodná stránka zobrazí tabuľku so skutočnými hodnotami zo zariadenia.
Prvé nastavenie
Pri prvom spustení je sieť zariadenia otvorená a pripojiť sa k nej môže ktokoľvek. Prvým krokom preto bude vytvorenie účtu pre administrátora. Kým ho používateľ nevytvorí, zariadenie mu nedovolí urobiť nič iné.
Na tejto jednoduchej stránke si vyskúšame prácu s formulárom: zobrazenie, spracovanie odoslaných údajov aj zobrazenie chyby.
Task
Rozšírte používateľské nastavenia o účet administrátora.
Vytvorte triedu Admin s členskými premennými
username a password s predvolenou hodnotou
None. V triede Settings bude dostupná v
členskej premennej admin. Vytvorte validátory, ktoré
overia, že meno nie je prázdne a heslo má aspoň 8 znakov.
Task
Vytvorte šablónu setup.html s formulárom na vytvorenie
účtu administrátora.
Šablóna dostane parametre title a error a
chybu zobrazí len vtedy, ak nastala. Formulár odošle údaje metódou
POST na adresu /setup:
{% args title, error %}
{% include "header.html" title %}
<h1>{{ title }}</h1>
{% if error %}
<p><mark>{{ error }}</mark></p>
{% endif %}
<form method="post" action="/setup">
<label>
Username
<input type="text" name="username" required>
</label>
<label>
Password
<input type="password" name="password" minlength="8" required>
</label>
<button type="submit">Save</button>
</form>
{% include "footer.html" %}Task
Vytvorte obsluhy trasy /setup, ktoré zobrazia formulár a
spracujú odoslané údaje.
Metódy HTTP na rovnakej adrese môžu mať samostatné obsluhy.
Zatiaľ sme používali dekorátor @app.get() pre metódu
GET, údaje z formulára sa odosielajú metódou
POST, pre ktorú slúži dekorátor
@app.post().
Hodnoty z formulára získate zo slovníka request.form. Z
nich vytvorte objekt triedy Admin a uložte ho do nastavení.
Ak validátor vyvolá výnimku ValueError, zobrazte formulár
znova s textom chyby. Po úspešnom vytvorení presmerujte používateľa na
úvodnú stránku funkciou redirect(). Ak administrátor už
existuje, stránka používateľa rovno presmeruje:
def setup_page(error=None):
return Template('setup.html').render(title='First setup', error=error)
@app.get('/setup')
async def setup_form(request):
if request.app.context.settings.admin.username is not None:
return redirect('/')
return setup_page()
@app.post('/setup')
async def setup_save(request):
settings = request.app.context.settings
if settings.admin.username is not None:
return redirect('/')
try:
settings.admin = Admin(
username=request.form.get('username', ''),
password=request.form.get('password', '')
)
except ValueError as ex:
return setup_page(error=str(ex))
return redirect('/')Poznámka
Nastavenia zostávajú zatiaľ len v pamäti. Do súboru ich uloží až stav
Commissioning po ich overení. Ak zariadenie reštartujete
skôr, administrátora budete musieť vytvoriť znova.
Task
Zabezpečte, aby sa používateľ bez vytvoreného administrátora dostal
len na stránku /setup.
Webový rámec Microdot
obsahuje špeciálny dekorátor s názvom @app.before_request.
Ak je funkcia dekorovaná týmto dekorátorom, vykoná sa pred každou
obsluhou trasy. Výsledok môže dopadnúť dvoma spôsobmi:
- ak funkcia vráti
None, vykonávanie pokračuje a zavolá sa obsluha trasy, - ak funkcia vráti HTTP odpoveď, tak je rovno preposlaná ako odpoveď klientovi.
Kontrolu tak napíšete na jednom mieste. Výnimkou musia byť adresa
/setup a statické súbory, inak by sa stránka zacyklila
alebo by nemala štýl:
@app.before_request
async def require_setup(request):
is_open = request.path == '/setup' or request.path.startswith('/static/')
if not is_open and request.app.context.settings.admin.username is None:
return redirect('/setup')Task
Overte správnosť svojej implementácie.
Overte tieto situácie:
- adresa
http://192.168.4.1/používateľa presmeruje na stránku/setup, - príliš krátke heslo zobrazí chybu a administrátor sa nevytvorí,
- správne údaje presmerujú používateľa na úvodnú stránku,
- opätovné otvorenie stránky
/setuppresmeruje na úvodnú stránku.
Formulár s nastaveniami
V tomto kroku vytvoríte stránku /settings, na ktorej
používateľ nastaví zariadenie. Navrhnite formulár tak, aby umožnil
zmeniť všetky používateľské nastavenia: WiFi sieť, MQTT broker,
jednotky, NTP server a heslo prístupového bodu.
Pri návrhu myslite na tieto situácie:
- Hodnoty z formulára sú vždy reťazce. Pred uložením ich preto musíte previesť na správny typ, napr. port na číslo. Zaškrtávacie pole sa odošle len vtedy, keď je zaškrtnuté.
- Formulár sa pri zobrazení vyplní aktuálnymi hodnotami, heslá však do stránky nikdy nevypisujte. Prázdne pole pre heslo bude znamenať, že sa heslo nemení.
- O kontrolu hodnôt sa postarajú validátory triedy
Settings. Ak niektorý z nich vyvolá výnimkuValueError, zobrazte formulár znova s textom chyby a so zadanými hodnotami, aby ich používateľ nemusel písať odznova. - Neplatné údaje nesmú zmeniť aktuálne nastavenia, preto hodnoty najprv overte a až potom ich uložte.
- Nové nastavenia uložte len do členskej premennej kontextu
settings, nie do súboru. Takto sa prenesú do stavuCommissioning, ktorý ich do súboru uloží až po ich overení.
Rozloženie formulára je na vás. Inšpirujte sa ukážkami formulárov štýlu Pico CSS.
Stav
Commissioning
V stave Commissioning zariadenie overí nastavenia zadané
používateľom. Ak overenie prebehne úspešne, nastavenia uloží. V oboch
prípadoch sa potom vráti do stavu Configuration, kde
používateľ uvidí výsledok overenia. Ak sa overenie nepodarilo,
používateľ môže nastavenia opraviť. Ak sa podarilo, reštartuje
zariadenie tlačidlom na webovej stránke, takže pri ďalšom spustení začne
bežnú činnosť stavom Measurement.
Stavový diagram zariadenia s novým stavom Commissioning
sa nachádza na obrázku 2.
---
# 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("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Init</div>")
init --> measurement("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Measurement</div>")
init -- "<code>SVC_PIN</code><br>grounded" --> terminal("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Service<br>Terminal</div>")
init -- "long button<br>press" --> reset("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Factory<br>Reset</div>")
init -- "no/faulty<br><code>settings.json</code> /<br>short button press" --> configuration("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Configuration</div>")
measurement --> connect("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Connect<br>Network</div>")
connect -- "connected" --> publish("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Publish<br>Data</div>")
connect -- "connection<br>failed" --> disconnect("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Disconnect<br>Network</div>")
publish --> disconnect
disconnect --> sleep("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Sleep</div>")
sleep --> sleepEnd@{ shape: fr-circ }
terminal --> terminalEnd@{ shape: fr-circ }
reset --> resetEnd@{ shape: fr-circ }
configuration --> commissioning("<div style='width:164px;height:48px;display:flex;align-items:center;justify-content:center;line-height:1.2'>Commissioning</div>")
commissioning -- "verification<br>result" --> configuration
configuration -- "restart" --> configurationEnd@{ 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,connect,publish,disconnect,sleep state
class terminal service
class reset reset
class configuration,commissioning config
class start initial
class sleepEnd,terminalEnd,resetEnd,configurationEnd final
Obr. 2: Stavový diagram zariadenia so stavom
Commissioning
Task
Zabezpečte ukončenie webového servera po odoslaní nastavení.
Keď obsluha trasy /settings nastavenia overí a uloží do
kontextu, musí ukončiť server. Ak by ho ukončila hneď, klient by
nedostal obsah odpovede. Preto server ukončíme s krátkym oneskorením v
samostatnej asynchrónnej úlohe:
import asyncio
# ...
async def shutdown_later(server):
await asyncio.sleep(1)
server.shutdown()Obsluha trasy túto úlohu spustí a oznámi to používateľovi:
asyncio.create_task(shutdown_later(request.app))
return Template('commissioning.html').render(title='Commissioning')Stránku commissioning.html vytvorte sami. Po ukončení
servera sa vykonávanie vráti z volania app.run(). Stav
Configuration už nevracia hodnotu None, ale
prejde do stavu Commissioning.
Upozornenie
Prístupový bod sa po odoslaní formulára vypne a používateľ stratí
spojenie so zariadením. Po skončení overenia sa prístupový bod znova
zapne a používateľ sa k nemu musí znova pripojiť, aby videl výsledok
overenia. Preto mu to treba oznámiť už na stránke
commissioning.html.
Task
Vytvorte stav Commissioning, ktorý overí a uloží
nastavenia.
Stav vytvorte v module commissioning.py balíka
states. Overované nastavenia nájde v členskej premennej
kontextu settings, kam ich uložila stránka
/settings. Počas overovania bude diagnostická LED dióda
svietiť farbou Color.CYAN. Stav postupne:
- pripojí zariadenie do WiFi siete operáciou
.connect()príkazunet, - synchronizuje čas operáciou
.sync()príkazurtc; ak sa to nepodarí, vypíše príčinu a pokračuje, - pripojí zariadenie k brokeru operáciou
.connect()príkazumqtta zverejní stav zariadeniaonline, - odpojí zariadenie od brokera aj od siete.
Ak overenie zlyhá s výnimkou NetworkError alebo
MQTTError, stav sa musí tiež odpojiť. Text chyby uloží do
novej členskej premennej kontextu commissioning_error a
prejde do stavu Configuration.
Ak overenie prebehne úspešne, stav uloží nastavenia do súboru
operáciou .save() príkazu settings. Až teraz
sa pôvodný súbor s nastaveniami prepíše. Úspech overenia stav poznačí v
novej členskej premennej kontextu commissioned nastavením
na hodnotu True a prejde do stavu
Configuration.
Poznámka
Stavy Configuration a Commissioning sa
navzájom používajú, čo by spôsobilo kruhový import. V stave
Commissioning preto importujte stav
Configuration až v metóde, kde ho potrebujete.
Task
Zobrazte používateľovi výsledok overenia nastavení.
Ak overenie zlyhalo, stránka /settings zobrazí text z
členskej premennej kontextu commissioning_error a nastaví
ju na hodnotu None, aby sa pri ďalšom zobrazení už
neukazovala.
Ak overenie prebehlo úspešne, teda členská premenná kontextu
commissioned má hodnotu True, stránka to
používateľovi oznámi a ponúkne mu tlačidlo na reštart zariadenia.
Task
Vytvorte trasu /restart, ktorá reštartuje
zariadenie.
Tlačidlo na reštart odošle formulár metódou POST na
adresu /restart. Obsluha trasy zobrazí stránku s oznámením,
že sa zariadenie reštartuje, a samotný reštart vykoná s krátkym
oneskorením, podobne ako pri ukončení servera, aby klient stihol dostať
odpoveď. Zariadenie reštartujte natvrdo funkciou
machine.reset().
Task
Overte správnosť svojej implementácie.
Vyskúšajte tieto situácie:
- správne nastavenia – zariadenie sa pripojí, v téme
<base_topic>/statussa objaví stavonline, nastavenia sa uložia, na stránke/settingssa zobrazí výsledok overenia a po stlačení tlačidla na reštart začne zariadenie bežnú činnosť, - nesprávne heslo k WiFi sieti – zariadenie sa vráti do stavu
Configurationa na stránke/settingssa zobrazí chyba, - nesprávne prihlasovacie údaje k brokeru – zariadenie sa zachová rovnako ako v predchádzajúcom prípade.
Prihlásenie administrátora
Administrátora sme už vytvorili, ale stránka s nastaveniami je stále prístupná každému, kto je pripojený do siete zariadenia. V tomto kroku ju preto chránime menom a heslom.
Použijeme overenie HTTP Basic, ktoré poskytuje rozšírenie
microdot.auth. Prehliadač sa používateľa sám opýta na meno
a heslo a pri ďalších požiadavkách ich posiela automaticky, takže
netreba vytvárať vlastnú prihlasovaciu stránku.
Upozornenie
Prihlasovacie údaje sa pri overení HTTP Basic posielajú nešifrovane, kódované len pomocou Base64. Na stránky preto pristupujeme cez HTTP len v sieti prístupového bodu chránenej heslom. V reálnom zariadení by bolo potrebné použiť HTTPS.
Task
Vytvorte funkciu, ktorá overí meno a heslo administrátora.
V module routes.py vytvorte objekt triedy
BasicAuth. Funkcia označená dekorátorom
@auth.authenticate dostane zadané meno a heslo. Ak sú
správne, vráti meno používateľa, inak nevráti nič:
from microdot.auth import BasicAuth
auth = BasicAuth()
@auth.authenticate
async def verify(request, username, password):
admin = request.app.context.settings.admin
if username == admin.username and password == admin.password:
return usernameTask
Chráňte obsluhy trás /settings a /restart
heslom administrátora.
Dekorátor @auth pridajte pod dekorátor trasy všetkým
týmto obsluhám, teda pri trase /settings pre metódy
GET aj POST:
@app.get('/settings')
@auth
async def settings_form(request):
# ...Upozornenie
Chráňte aj obsluhu metódy POST. Inak by sa dali
nastavenia zmeniť priamo odoslaním požiadavky bez prihlásenia.
Task
Overte správnosť svojej implementácie.
Overte tieto situácie:
- stránka
/settingssi vypýta meno a heslo, - nesprávne heslo prístup nepovolí a server vráti kód
401, - so správnymi údajmi sa stránka zobrazí,
- úvodná stránka
/zostáva verejná.
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
Presuňte spoločný kód tried
CYW43439aCYW43439APdo spoločného predka.Doplňte do stavu
Configurationčasový limit nečinnosti. Ak sa počas neho nič nestane, zariadenie konfiguráciu ukončí.Vytvorte stránku Info so súhrnnými vlastnosťami zariadenia.
Vytvorte stránku Measurements s neodoslanými meraniami a tlačidlom na nové meranie.
Vytvorte stránku Devices so zoznamom zariadení a možnosťou ich otestovania.
Doplňte do formulára výber WiFi siete zo zoznamu dostupných sietí.
Zabezpečte, aby sa po pripojení k prístupovému bodu webové rozhranie otvorilo samo (captive portal).
Ďalšie zdroje
Microdot – dokumentácia webového rámca
Pico CSS – dokumentácia štýlu
Práca s WiFi modulom – vytvorenie prístupového bodu
Zoznam zmien
2026-11-04
- Krok „Stav
Commissioning“: stav po úspešnom overení nastavenia uloží, nastaví členskú premennú kontextucommissionedna hodnotuTruea vráti sa do stavuConfiguration, namiesto toho, aby zariadenie reštartoval. - Krok „Stav
Commissioning“: stránka/settingszobrazí výsledok overenia a po úspešnom overení ponúkne tlačidlo na reštart. - Krok „Stav
Commissioning“: nová trasa/restart, ktorá zariadenie reštartuje. - Krok „Prihlásenie administrátora“: heslom sa chráni aj trasa
/restart.