====== 4. Zpracování dat ======
Před cvičení si prosím nainstalujte Python 3.x s následujícími moduly: [[https://pypi.org/project/pyserial/|pyserial]], [[https://matplotlib.org/|matplotlib]], [[https://pypi.org/project/PyQt6/|pyqt6]], [[https://pypi.org/project/pysqlite3/|pysqlite3]], [[https://bleak.readthedocs.io/en/latest/|bleak]], [[https://pypi.org/project/paho-mqtt/|paho-mqtt]], [[https://numpy.org/|numpy]]
Instalace modulů:
$ pip install pyserial matplotlib pyqt6 pysqlite3 bleak paho-mqtt numpy
V případě, že nemáte administrátorská práva na počítači:
$ pip install --user pyserial matplotlib pyqt6 pysqlite3 bleak paho-mqtt numpy
===== Cíle cvičení =====
- Vytvořit program pro Thunderboard, který bude v pravidelných intervalech odesílat data po UART
- Data v reálném čase vykreslovat pomocí aplikace v Pythonu
- Navrhout protokol, který umožní přenos dat z různých senzorů
- Data ukládat do databáze na PC k pozdějšímu použití
===== Ukázkové aplikace v Pythonu =====
Řada aplikací bude napsána v Pythonu, flexibilním programovacím jazyku vhodném pro rychlé prototypování. Vždy bude připravena kostra programu, kterou lze přímo použít pro vlastní projekt. Pokud byste se chtěli dovědět něco víc o Pythonu, je možné využít např. podklady předmětu BAB37ZPR: [[https://cw.fel.cvut.cz/b211/courses/bab37zpr/start|2021/22]], [[https://cw.fel.cvut.cz/b201/courses/bab37zpr/solutions/start|2020/21]]. Lze také doporučit volně dostupnou knihu [[https://pythonnumericalmethods.berkeley.edu/notebooks/Index.html|Python Programming And Numerical Methods: A Guide For Engineers And Scientists]].
Jako zdroj dat lze využít např. generátor harmonického signálu na [[https://gitlab.fel.cvut.cz/viteks/nsi-codes/-/tree/main/01-generator|gitlabu]]. Aplikace pro PC v jazyce Python jsou také k dispozici na [[https://gitlab.fel.cvut.cz/viteks/nsi-codes/-/tree/main/tut-04-local|gitlabu]].
==== Komunikace po rozhraní UART ====
Pro UART komunikaci mezi embedded zařízením a PC použijeme knihovnu [[https://pypi.org/project/pyserial/|PySerial]]. Připojení proběhne vytvoření instance třídy, parametry konstruktoru jsou název sériového portu a rychlost komunikace. Je celkem vhodné ošetřit připojení pro případ, že spojení nelze uskutečnit. V následujícím kódo je zařízení připojeno k portu ''COMXXX'' (za XXX dosaďte podle potřeby) a komunikuje rychlostí 115200 baudů.
import serial
try:
ser = serial.Serial('COMXXX', 115200)
ser.flushInput()
except:
print("unable to open COM port")
Se zařízením je možné komunikovat na úrovni bytů nebo znaků. Pokud zařízení posílá data ve formě znaků (tj. posílá byty s významem ASCII kódů), lze data přijmout třeba takto:
data = ser.readline().decode()
print(data)
Pokud je z hlediska aplikace výhodnější používat byty ve s významem čísla (např. v rozsahu 0-255), je možné přijmout a dekódovat takto:
byte = ser.read()
decoded_bytes = int.from_bytes(byte, byteorder = 'little')
Argumentem metody ''read()'' je počet bytů, které jsou najednou přečteny. Lze tedy např. předpokládat, že embedded zařízení posílá dva byty současně a tyto dva byty buď mohou reprezentovat číslo typu ''short'', nebo dva samostatné byty ''a'' a ''b'', které je třeba dekódovat:
byte = ser.read(2)
decoded_bytes = int.from_bytes(byte, byteorder = 'little')
a = decoded_bytes & 255;
b = (decoded_bytes >> 8) & 255;
==== Přenos struktrovaných dat ====
Pokud bychom potřebovali přenést po UART hodnoty různých datových typů, můžeme je na straně mikrokontroléru uložit do struktury.
BufferedSerial pc(USBTX, USBRX, 115200);
//--
typedef struct data {
unsigned int Si7021_t;
unsigned int Si7021_h;
int Si7210_t;
float BMP280_t;
};
// ---
data a;
// --
pc.write(&a, sizeof(a));
V Pythonu pak lze pro dekódování dat ve struktuře využít knihovny [[https://docs.python.org/3/library/ctypes.html|ctypes]] nebo [[https://docs.python.org/3/library/struct.html|struct]].
from ctypes import Structure, c_uint, c_int, c_float, sizeof
class DataPoint(Structure):
_pack_ = 1
_fields_ = [
("Si7021_t", c_uint),
("Si7021_h", c_uint),
("Si7210_t", c_int),
("BMP280_t", c_float)
]
#
byte = ser.read(16)
buffer = bytearray(byte)
Point = DataPoint.from_buffer(buffer)
# teplota z Si7021 je pak dostupná jako Point.Si7021_t/1000,
==== Vykreslování dat ====
K vykreslení dat přicházejících po UART rozhraní použijeme knihovnu [[https://matplotlib.org/stable/plot_types/index.html|matplotlib]]. Data pro vykreslení jsou uložena ve vektoru vytvořeném pomocí knihovny [[https://numpy.org/doc/stable/reference/arrays.html|NumPy]]. Vektor má délku odpovídající délce okna, a v cyklu jsou na začátek vektoru přidávána data metodou ''append''. V cyklu jsou současně vykreslována data ze začátku vektoru.
import matplotlib.pyplot as plt
import numpy as np
plot_window = 20
y_var = np.array(np.zeros([plot_window]))
plt.ion()
fig, ax = plt.subplots()
line, = ax.plot(y_var)
ax.set_ylim([0, 255])
while True:
# zde by mel byt kod pro ziskani a dekodovani dat
y_var = np.append(y_var, data)
y_var = y_var[1:plot_window+1]
line.set_ydata(y_var)
fig.canvas.draw()
fig.canvas.flush_events()
Pokud bychom chtěli vykreslovat dvě křivky, je třeba v původním kódu udělat následující úpravy:
# konfigurace
fig, ax = plt.subplots(2)
line0, = ax[0].plot(y0_var)
line1, = ax[1].plot(y1_var)
ax[0].set_ylim([0, 255])
ax[1].set_ylim([0, 255])
# samotne vykreslovani
y0_var = np.append(y0_var, a)
y1_var = np.append(y1_var, b)
y0_var = y0_var[1:plot_window+1]
y1_var = y1_var[1:plot_window+1]
line0.set_ydata(y0_var)
line1.set_ydata(y1_var)
==== Ukládání dat do SQL databáze ====
Pro uložení dat můžeme využít [[https://www.sqlite.org/index.html|SQLite]] databázi. Její výhodou je, že pracuje s lokálním binárním souborem a má minimální nároky na paměť a výkon počítače - veškerá potřebná funkcionalita je totiž vytvářena knihovnou, která pracuje s binárním souborem. Pro jednoduchost můžete využít modul [[https://gitlab.fel.cvut.cz/viteks/nsi-codes/-/blob/main/tut-04-local/nsidb.py|nsidb]], který vytváří třídu pro ukládání dvou hodnot typu ''float'' reprezentující teplotu a vlhkost přečtenoou ze senzoru Si7021. Hodnoty jsou funkci pro zápis předávány jako tuple.
Samotný hlavní program pro uložení dat může vypadat třeba takto:
from nsidb import *
conn = db_conn("iot.db")
db_create(conn)
db_insert (conn, (3.14, 25.5))
rows = db_fetch (conn)
for row in rows:
print(row)