Kurs Arduino – logger, czyli rejestrator danych CSV
W jedenastym odcinku kursu Arduino zbudujemy użyteczny logger, czyli rejestrator. Poznamy też i wykorzystamy bardzo popularny i łatwy do analizy format baz danych CSV (Comma-separated values).
W poprzednich odcinkach kursu poznaliśmy wszystkie „cegiełki”, które są potrzebne do budowy rejestratora. Aż prosi się, żeby teraz połączyć te umiejętności i zrealizować logger, czyli rejestrator danych.
Mając w zestawie moduł RTC, który zawiera zarówno kostkę „zegarową” DS3231, jak i pamięć nieulotną EEPROM, można byłoby zrealizować rejestrator, zapisujący wyniki pomiarów właśnie do tej pamięci. Okolicznością sprzyjającą jest fakt, że pamięć Atmel 24C32, jak każda pamięć EEPROM/FLASH, ma wprawdzie ograniczoną liczbę cykli zapisu, ale według katalogu może być zapisywana aż 1 milion razy. Mówić najprościej, rok ma pół miliona minut, a ograniczenie liczby zapisów do miliona dotyczy jednej komórki (każdej z osobna), a przecież komórki zapisujemy kolejno, więc sumaryczna liczba cykli zapisu całej kostki jest tysiące razy większa.
Świetnie, tylko jest inny problem. Problem sumarycznej pojemności: kostka 24C32 zawiera 32 kilobity, a więc tylko 4 kilobajty, czyli 4096 bajtów. Gdybyśmy przykładowo co 10 minut zapisywali wyniki pomiarów i aktualny czas w porcjach (rekordach), zawierających powiedzmy 30 bajtów, to w ciągu godziny potrzebujemy 180 bajtów i 4096-bajtową pamięć zapełnimy po 22 godzinach – pamięć 24C32 nie wystarczy nawet na dobę takiej skromnej rejestracji. Dlatego trzeba rozejrzeć się za nośnikiem bardziej pojemnym. Aż prosi się wykorzystać kosztującą kilkanaście złotych kartę (micro) SD, która też zawiera pamięć nieulotną, ale nie EEPROM, tylko pokrewną i podobną FLASH, o nieporównanie większej pojemności, już nie kilku kilobajtów, nie kilku megabajtów, a kilku czy nawet kilkudziesięciu gigabajtów.
Zanim jednak zaczniemy działać, powinniśmy omówić bardzo ważne zagadnienie podstawowe dotyczące rejestratorów.
Rejestracja danych
Otóż niewątpliwie rejestrator ma zapisywać na jakimś trwałym nośniku dane. Jakie dane? W naszym przypadku praktycznie zawsze będą to wyniki pomiarów, czyli jakieś liczby.
A jak wiemy, liczby można zapisywać w różny sposób, a w języku C(++) i w Arduino mamy do tego różne typy zmiennych. I tak mamy różnej długości, 8-, 16- i 32-bitowe liczby całkowite ze znakiem (minus) i bez znaku, a także (32-bitowe) liczby zmiennoprzecinkowe. Pojedyncza „paczka danych” zapisywana na nośniku, czyli tak zwany rekord, mogłaby zawierać różniej długości liczby całkowite i zmiennoprzecinkowe.
Problem w tym, że w zależności od potrzeb, rekord mógłby zawierać więcej lub mniej „dłuższych lub krótszych liczb”. W sumie byłby to ciąg bajtów, czyli mówiąc najprościej, liczb dwójkowych ośmiobitowych, i poszczególne bajty należałoby traktować jako ośmiobitowe liczby dwójkowe, a inne bajty byłyby częścią „dłuższych liczb”. Zapewne przy zapisie poszczególne liczby nie byłyby od siebie oddzielone, odseparowane, tylko ułożone jedna za drugą. Liczby mogą zawierać wszelkie możliwe kombinacje bitów, więc niestety nie można wybrać jakiejś „nieużywanej” kombinacji, która pełniłaby funkcję separatora, oddzielającego poszczególne liczby. Dlatego przy takim binarnym sposobie rejestracji konieczne byłoby jasne określenie, co znaczą poszczególne bajty. Albo trzeba byłoby w nagłówku, na początku pliku z danymi, jakoś podać informacje, jak zestawiać poszczególne bajty w liczby, co wydaje się sensownym, choć nieco kłopotliwym rozwiązaniem. Albo też należałoby się umówić i ustalić jakiś powszechnie obowiązujący standard – wtedy zapis mógłby nie zawierać nagłówka, a ciąg bajtów zawsze byłby „rozdzielany” i interpretowany według przyjętego standardu. Na nośniku zapisane byłyby wyłącznie rejestrowane dane, bez żadnych informacji dodatkowych. Pomysł interesujący, ale trudny do wprowadzenia z uwagi na rozmaite potrzeby co do „długości liczb” i potrzebę zapisu danych innych niż liczby.
Ale jest też zupełnie inna koncepcja. Wprawdzie rejestrowane dane to wyniki pomiarów, czyli liczby, ale można je zapisać NIE w postaci „różnej długości liczb” całkowitych i zmiennoprzecinkowych przedstawionych dwójkowo. Otóż liczby można zapisać inaczej, niejako w postaci „słownej”, „tekstowej” i to nie dwójkowo, tylko dziesiętne. Mianowicie można podawać i zapisywać w postaci tekstu wartość kolejnych cyfr dziesiętnej postaci liczby. Mając na przykład liczbę 253, podalibyśmy: dwa, pięć, trzy, a ściślej trzy znaki/symbole: '2′, '5′, '3′.
W przypadku mikroprocesorowego rejestratora danych, zapisującego wyniki pomiarów na nośniku, oznacza to sporą komplikację programu. Wyniki pomiarów będą uzyskiwane albo z przetworników analogowo-cyfrowych, albo z innych urządzeń (np. liczników czy zegarów), gdzie pierwotnie będą mieć postać dwójkową. Najpierw należałoby więc przekształcić postać dwójkową na dziesiętną, a potem „przeliterować” poszczególne cyfry tak uzyskanych wyników i zapisać nie „prawdziwe liczby”, tylko zapisać tekst, zawierający poszczególne cyfry rejestrowanych liczb. Taki zapis tekstowy jest znacznie bardziej rozwlekły, czyli do zapisania wymaga więcej pamięci niż sposób binarny.
Oczywiście potem, przy odczycie, nie uzyskujemy „prawdziwych liczb”, tylko teksty, które trzeba najpierw zamienić na „prawdziwe liczby” i dopiero potem tak uzyskane liczby przetwarzać czy interpretować.
Czy taka komplikacja ma sens?
Otóż okazuje się, że ma! W rejestratorach danych liczbowych bardzo często, a wręcz powszechnie wykorzystujemy taki właśnie tekstowy sposób zapisu liczb!
Wymaga to wprawdzie przeprowadzenia szeregu wspomnianych dodatkowych operacji, ale ma ogromne zalety. Po pierwsze, oprócz „tekstowego zapisu liczb”, można też zapisywać dowolne inne teksty i nie będzie problemu z jednoznaczną interpretacją. A jeśli chodzi o „tekstowy zapis liczb”, nie ma problemu z ich reprezentacją i rozdzieleniem, ponieważ wystarczy wybrać jakiś („nieużywany”) znak/symbol, który będzie rozdzielał poszczególne liczby (pola) rekordu. I tak oto doszliśmy do…
CSV
CSV to skrót od Comma-separated values, co znaczy wartości rozdzielone przecinkami.
Zasada jest beznadziejnie prosta: mamy plik tekstowy, gdzie w jednej linii mamy jeden rekord – porcję danych, a przecinek pełni funkcję separatora poszczególnych pól rekordu. Pola mogą być dowolnej długości i mogą zawierać dowolne dane, w tym w szczególności mogą zawierać „tekstowy zapis liczb”.
A teraz pewne istotne szczegóły: Większość podstawowych rozwiązań informatycznych powstała w USA, gdzie separatorem dziesiętnym w liczbach jest kropka, natomiast przecinek jest tylko „znakiem pomocniczym”, stosowanym w zapisie liczb do podziału na grupy trzycyfrowe. Na przykład „amerykańska” liczba 1,234,568.9 to nasze 1 234 567,9. I właśnie twórcy formatu CSV ten stosunkowo rzadko używany „pomocniczy znak” wykorzystali jako separator pól rekordu.
W Europie przecinek jest znakiem dziesiętnym i dlatego mówimy o liczbach zmiennoprzecinkowych. A w klasycznych plikach CSV przecinek pełni funkcję separatora pól, więc nie może być znakiem (separatorem) dziesiętnym w „tekstowym zapisie liczb” – tę rolę odgrywa kropka. W Europie mamy kłopot, ale możliwe są modyfikacje reguł, na przykład użycie jako separatora znaku średnika (;) albo najbardziej tu pasującego znaku tabulatora, a nawet „zamiana znaczenia kropki i przecinka”, ale wiąże się to z pewnymi komplikacjami i ryzykiem błędów, dlatego my na początek takie możliwości pominiemy i w pierwszych ćwiczeniach zaakceptujemy „amerykański” sposób i klasyczne reguły CSV.
Druga istotna sprawa to zapis przecinka w treści pól. Pola mogą zawierać dowolny tekst, w tym przecinki należące do tekstu. W pewnych przypadkach treść pola umieszczamy w cudzysłowach, co na przykład zachować spacje na początku i końcu pola, np.: „ tekst odsuniety „. I właśnie jeżeli w treści pola ma wystąpić przecinek, takie pole należy wziąć w cudzysłowy, na przykład: „pole, ktore zawiera przecinek„. W zasadzie w polach mogą pojawić się też „polskie litery”, np. kodowane w popularnym UTF-8, ale może wystąpić problem z interpretacją takich danych – w te szczegóły nie będziemy się zagłębiać. Wspomnijmy tylko, że cudzysłów umieszczamy w treści pola (obowiązkowo wziętego w cudzysłów), pisząc go dwukrotnie, np.: „pole, w ktorym „”zakopalismy”” cudzyslow„.
Rejestracja danych i Arduino
Jak już mówiliśmy, wykorzystując niezbyt duże zasoby Arduino moglibyśmy ustalić jakiś „prywatny standard”, wykorzystujący binarny sposób reprezentacji liczb, stosownie do specyfiki zapisywanych liczb – wyników. Można tak zrobić i czasem może to być optymalne rozwiązanie. Można ustalić „prywatny standard”, ale od razu nasuwa się pytanie, co będziemy robić z danymi zapisanymi w pliku?
Otóż może pojawić się kłopot z interpretacją. Ponadto gdyby trzeba było coś zmienić albo podzielić się wynikami z kimś innym, taki „prywatny standard” okaże się poważną przeszkodą. Wielkie zalety ma wykorzystanie popularnego standardu CSV. Teoretycznie jest on bardziej „pracochłonny”, ale w praktyce w ogóle tego nie dostrzegamy. Program i procesor Arduino zestawi treść rekordów i zapisze je w postaci tekstu za pomocą metody .print().
A co potem z takim tekstem?
Na pewno można go przetworzyć, niekoniecznie za pomocą dedykowanej aplikacji. CSV to popularny standard, obsługiwany m.in. przez wszystkie arkusze kalkulacyjne, jak Excel z Microsoft Office oraz darmowe OpenOffice, Libre Office i GoogleDocs (Sheet). W praktyce oznacza to, że kartę (micro) SD możemy wyjąć z rejestratora, włożyć do czytnika kart np. w laptopie, zgrać plik .CSV i go w komputerze dowolnie przetworzyć, a na koniec zobrazować jako wykres.
Spróbujmy to zrobić!
Rejestrator, czyli… data logger
Wykorzystamy układ zestawiony na potrzeby dwóch poprzednich ćwiczeń, gdzie od razu dalekowzrocznie uwzględniliśmy moduł czytnika kart micro SD. A co będziemy mierzyć?
W kilku poprzednich odcinkach realizowaliśmy różne pomiary i możesz samodzielnie zmodyfikować układ i rejestrować, co tylko zechcesz. Ale my w ramach tego odcinka kursu wykorzystamy poznane ostatnio czujniki temperatury DS18B20.
Jak pokazuje rysunek 1, układ z poprzedniego ćwiczenia z dwoma modułami i trzema czujnikami DS18B20 wzbogacamy tylko o przełącznik START/STOP sterujący rejestracją. Zależnie od stanu styku S1 świeci albo dioda niebieska (stan wysoki), albo czerwona (stan niski). Nie wykorzystujemy wyświetlacza LCD. Mój model pokazany jest na fotografii 2.
Tworząc program – szkic, jak zwykle skorzystamy z gotowych bibliotek. Potrzebne będą biblioteki do obsługi układu RTC, czytnika kart (micro)SD oraz dwie biblioteki obsługujące czujnik temperatury DS18B20. Wszystkie je już wykorzystywaliśmy wcześniej.
U mnie po uruchomieniu ArduinoIDE pojawił się komunikat, że dostępne są nowe wersje bibliotek, między innymi SD (rysunek 3) oraz RTC by Makuna. Uaktualniłem je.
Jeżeli chodzi o program, znów dla ułatwienia zadania skorzystajmy z przykładowego szkicu Datalogger, dołączonego do biblioteki SD (rysunek 4).
Szkic 1 pokazuje jego uproszczoną wersję, gdzie prawie wszystko jest jasne.
Szkic 1:
//uproszczona wersja przykładowego szkicu „Datalogger”
#include <SPI.h>
#include <SD.h> //dołączamy biblioteki
void setup() { Serial.begin(9600);
if (!SD.begin(10)) { //inteligentna inicjalizacja
Serial.println(„Brak karty lub uszkodzenie”);
while (1); }} //czekaj jeśli błąd inicjalizacji
void loop() { //tworzymy „pusty” obiekt typu String:
String dataString = „”; //to będzie nasz rekord
//w pętli for odczytujemy napięcia pinów A0…A2
for (int analogPin =0; analogPin <3; analogPin++) {
int sensor = analogRead(analogPin);
//i po prostu dopisujemy wynik pomiaru do „Stringu”:
dataString += String(sensor);
//po dwóch pierwszych wartościach dodajemy przecinek:
if (analogPin < 2) {dataString += „,”;}
} //koniec pętli for; potem zapisujemy jeden rekord:
// otwieramy plik na karcie do zapisu:
File dataFile = SD.open(„datalog.txt”, FILE_WRITE);
//zapisujemy jeden rekord i zamykamy plik na karcie
dataFile.println(dataString);
dataFile.close(); } //koniec pętliTrzeba tylko podkreślić, że w linii:
String dataString = „”;
NIE tworzymy zmiennej tekstowej typu string, tylko tworzymy inteligentny obiekt „tekstowy” o nazwie dataString, który jest typu String. Na razie jest on pusty. Wielka litera S na początku ogromnie dużo zmienia: nie jest to prosty łańcuch znaków (string), tylko obiekt tekstowy (String) o cennych właściwościach, znakomicie ułatwiających pisanie programu. Właśnie dlatego możemy zastosować proste „dodawanie kawałków tekstu”, co czynimy w linii:
dataString += String(sensor);
gdy w pętli for trzykrotnie dopisujemy wyniki pomiaru napięcia na pinach A0…A2. Funkcja analogRead() wpisuje do zmiennej sensor typu int liczbę 0…1023, ale potem bez naszego udziału ta liczba całkowita ze zmiennej liczbowej sensor zostaje wpisana do inteligentnej zmiennej tekstowej dataString już jako tekst. Pomiędzy odczytanymi wartościami zostają wstawione dwa przecinki i mamy jeden rekord, gotowy do zapisania na karcie SD. W tym celu dalej w programie tworzymy obiekt „karciany”, a raczej „plikowy” typu File o nazwie dataFile. I ten obiekt pozwala zapisać nasz rekord, zawarty w zmiennej dataString, do pliku tekstowego datalog.txt na karcie SD.
My do praktycznej realizacji wykorzystamy połączenie tego przykładowego szkicu z naszym wcześniejszym szkicem A1003.ino, z poprzedniego odcinka kursu. Połączony i „spolonizowany” program znajdziesz w szkicu 2 (i w pliku A1101.ino dostępnym także tutaj = ZIP A11.
Szkic 2:
#include <OneWire.h>
#include <DallasTemperature.h>
#include <SPI.h>
#include <SD.h>
const int chipSelect = 10; //wykorzystujemy pin 10
OneWire obiektOneWire(A0); //wykorzystujemy pin A0
DallasTemperature czujniki(&obiektOneWire);
int liczbaCzujnikow; DeviceAddress tmpAdresyCzujnikow;
void setup(void) { //inicjalizacja obiektów:
Serial.begin(9600); czujniki.begin();
liczbaCzujnikow = czujniki.getDeviceCount();
Serial.print(„inicjalizacja karty SD…”);
if (!SD.begin(chipSelect)) {
Serial.println(„Blad karty SD!”); while (1);}
Serial.println(„Inicjalizacja SD prawidłowa.”); }
void loop(void) {
if (digitalRead(A2) == LOW)
{Serial.println(„Rejestracja wstrzymana”);
delay(3000); } else {
String mojRekord = „”; //obiekt typu String
czujniki.requestTemperatures();
for(int i=0;i<liczbaCzujnikow; i++) {
czujniki.getAddress(tmpAdresyCzujnikow, i);
float tempC =czujniki.getTempC(tmpAdresyCzujnikow);
Serial.print(„Czujnik „); Serial.print(i,DEC);
Serial.print(„: „); Serial.print(tempC);
Serial.print(„\xC2\xB0”); Serial.print(„C; „);
mojRekord += String(tempC);
mojRekord += „,”; } //koniec odpytywania czujników
Serial.println(); Serial.print(„Zapiszemy na karcie SD: „);
Serial.println(mojRekord);
File mojPlik = SD.open(„plik_rej.txt”, FILE_WRITE);
if(mojPlik) {mojPlik.println(mojRekord); mojPlik.close();}
else {Serial.println(„Blad zapisu do pliku!”);} } }
Gdy przy zaświeconej czerwonej lampce (styk S1 zwarty) uruchomisz ten program i otworzysz konsolę monitora, a po kilku sekundach rozewrzesz S1 (zaświecisz niebieską lampkę), to zapewne zobaczysz obraz, jak na rysunku 5. Wszystko wygląda dobrze, tylko nie ma zapisu na kartę.
Czy już widzisz, w czym problem?
Jeżeli nie, to proponuję, żebyś nie czytał dalszej części artykułu, tylko poszukał błędu samodzielnie. Mamy tu typowy drobny, „głupi” błąd. Do jego odszukania niestety nie wystarczą informacje z wcześniejszych odcinków kursu. Trzeba poszukać „na zewnątrz”.
Można na przykład zajrzeć do opisu biblioteki SD, np. na stronie
www.arduino.cc/en/Reference/SD.
Jeśli znalazłeś błąd – GRATULUJĘ!
Poprawioną, działającą wersję znajdziesz tutaj = ZIP A11 w szkicu A1102.ino. Ja w ramach tego ćwiczenia przygotowałem dwie szklanki: jedną z mieszaniną wody z lodem, drugą ze świeżo nalanym wrzątkiem – fotografia 6.
Najpierw jedną z sond DS18B20 umieściłem za oknem. Na początku rejestracji, gdy rozwarłem styk S1, wkładałem jedną z sond na przemian do tych dwóch szklanek.
Po zakończeniu rejestracji, czyli po zwarciu styku S1, wyjąłem kartę microSD, przełożyłem do przejściówki SD i włożyłem do czytnika w laptopie. Oczywiście można „na piechotę” przeanalizować zarejestrowane dane, ale generalnie o wiele bardziej miarodajne jest przedstawienie ich w sposób graficzny.
Aby uzyskać odpowiedni wykres zgrałem plik RSTRATOR.TXT na dysk komputera i otworzyłem go za pomocą Notatnika. Problem w tym, że dla „polskich komputerów” punktami dziesiętnymi nie są „kropki w liczbach”, tylko przecinki, które akurat w naszym pliku są separatorami. Dlatego w Notatniku (lub innym „prostym” edytorze) za pomocą polecania Zamień (Ctrl+H) trzeba zamienić najpierw wszystkie przecinki na średniki (rysunek 7), a potem podobnie zamienić wszystkie kropki na przecinki. I zapisać plik jako RSTRATOR.csv.
Plik z rozszerzeniem .csv możesz otworzyć za pomocą pakietu MicrosoftOffice (jeśli masz) lub za pomocą darmowych OpenOffice czy LibreOffice. Po wykonaniu w Notatniku opisanych właśnie zmian nie trzeba nic kombinować z filtrem importu, bo program sam rozpozna, że separatorami są średniki. Po otwarciu pliku wystarczy z menu wybrać polecenie dodaj/wstaw wykres. Po wybraniu typu wykresu (dla takich danych najlepszy jest wykres liniowy) od razu pojawi się piękny wykres z trzema krzywymi. Cała operacja tworzenia wykresu z odpowiednio przygotowanego pliku .csv trwa dosłownie kilka sekund. Rysunek 8 pokazuje taki „surowy” wykres, błyskawicznie uzyskany w Microsoft Office.
Wykres warto wzbogacić o lepszy opis. Przykład pokazany na rysunku 9 został zrealizowany w darmowym OpenOffice (Calc).
Po wstawieniu na arkusz wykresu można edytować jego elementy (po podwójnym kliknięciu wykresu i pojedynczym kliknięciu danego elementu). Można dodać opisy, wyskalować osie. Trochę kłopotu jest z edycją opisu krzywych w legendzie oraz z wydrukiem. Chyba najprościej zrealizować wydruk, zaznaczając wykres w programie OpenOfficeCalc, kopiując go (Ctrl+C), a następnie wklejając (Ctrl+V) do pustego… dokumentu tekstowego OpenOfficeWriter i drukując albo eksportując („niewidzialną”) stronę.
Z rysunku 9 widać, że u mnie czujnik 0 najpierw mierzył temperaturę w pomieszczeniu, potem został włożony do wody z lodem. Czujnik 1 najpierw wystawiony był za okno „na mróz”, a potem powrócił do pomieszczenia, gdzie zaczął się powoli nagrzewać. Dużo szybciej zmieniał temperaturę czujnik 2, który był zanurzany na przemian w szklance z gorącą i z zimną wodą.
Z rysunku 9 można odczytać kilka dalszych interesujących szczegółów. Tak widać, że wrzątek w szklance dość szybko stygnie, natomiast mieszanina wody z lodem zasadniczo powinna przez długi czas utrzymywać temperaturę dokładnie 0°C (do całkowitego stopnienia lodu). Tak, ale pod warunkiem, że była to woda dejonizowana (destylowana). W praktyce woda się nagrzewa i temperatura czujnika waha się podczas mieszania, co wyraźnie widać na rysunku 10, który też powstał na podstawie wybranego fragmentu danych z pliku RSTRATOR.csv.
Lepszą stałość temperatury bliskiej zera stopni można uzyskać, gdy nie są to duże kawałki lodu pływające w wodzie (jak na fotografii 6), tylko gdy lód rozbity jest na maleńkie kawałki, tworząc papkę o konsystencji topniejącego śniegu.
Podsumowanie
W tym odcinku opanowaliśmy ogromnie ważne zagadnienia: potrafimy zapisywać wyniki pomiarów w pliku na karcie SD i potrafimy te wyniki zobrazować w formie graficznej.
To naprawdę bardzo ważna umiejętność, dlatego zachęcam, byś we własnym zakresie poćwiczył, by nabrać wprawy. Najlepiej byłoby, gdybyś zrealizował własny rejestrator i zebrał własne dane. Pomoże w tym szkic A1102.ino, który możesz na różne sposoby modyfikować. Najprostsza modyfikacja to dodanie opóźnienia i pomiar w długich odcinkach czasu, co pozwoli zbadać cykle dobowe, np. temperatury, wilgotności czy światła.
Ale nawet jeśli nie zgromadzisz w ten sposób własnych danych, poćwicz chociaż tworzenie wykresów. Jeśli ściągniesz ZIP z plikami, znajdziesz tam nie tylko dwa omawiane szkice .ino, ale też plik tekstowy z „surowym” zapisem z karty oraz plik .csv ze zmienionymi separatorami i znakiem punktu dziesiętnego.
Nie przejmuj się, jeśli czegoś nie rozumiesz. A jeśli napotkasz kłopoty – szukaj pomocy w Internecie.
A my w następnym odcinku UR012 nadal będziemy zajmować się rejestracją.
Piotr Górecki