Przejdź do głównej zawartości

Arduino Custom Data Station #3 - prezentacja danych

· 5 min aby przeczytać
Arduino Custom Data Station #1 - wysyłanie danych

Etap #3, czyli dodajmy możliwość prezentacji danych w formie wykresów przy użyciu popularnej biblioteki Chart.js.

Etap #3

Trzeci etap projektu zakłada dodanie obsługi wykresów na którym będą prezentowane w czasie rzeczywistym odczytane z Arduino wartości temperatury oraz wilgotności powietrza.

Arduino Custom Data Station - schemat ideowy komunikacji za pomocą Web Serial API
Arduino Custom Data Station - schemat ideowy komunikacji za pomocą Web Serial API

Chart.js

Do prezentacji danych wykorzystam popularną bibliotekę Chart.js. Na początku należy zainstalować pakiet chart.js w tym celu z poziomu terminala wykonujemy poniższe komendy:

npm install chart.js --save
npm install @types/chart.js --save-dev

Pierwsza komenda odpowiada za zainstalowanie oraz dodanie pakietu chart.js do naszego pliku package.json do sekcji dependencies. Druga natomiast odpowiada za dodanie pliku z definicją typów, które są używane przez pakiet chart.js, dzięki temu nasze IDE będzie rozpoznawało typy zdefiniowane w nowym pakiecie. Pakiet ten jest opcjonalny i potrzebny tylko w fazie developmentu, dlatego został dodany przełącznik "--save-dev", który odpowiada za dodanie pakietu do pliku package.json w sekcji devDependencies.

TimeSeriesChart - komponent do prezentacji danych

Dobrą praktyką jest stworzenie własnego komponentu, aby uniknąć w przyszłości duplikacji kodu (DRY), dlatego stworzyłem komponent TimeSeriesChart, który będzie odpowiedzialny za prezentację danych w formie wykresu, gdzie na osi Y będą prezentowane wartości odczytane z Arduino (temperatura i wilgotność powietrza), natomiast na osi X będzie umieszczona wartość czasu.

Komponent TimeSeriesChart posiada 6 właściwości (ang. properties):

  • header - nagłówek wykresu,
  • label - etykieta danych,
  • color - kolor wykresu,
  • xAxisLabel - etykieta osi X,
  • AxisLabel - etykieta osi Y,
  • size - maksymalna liczba punktów (rozmiar bufora na dane).

Dodatkowo zdefiniowałem lokalny stan do przechowywania poszczególnych punktów (x,y), które wyświetlane są na wykresie:

  • data - stan, zdefiniowany jako słownik przechowywujący wartości X oraz Y, które reprezentowane są jako tablice typu string oraz number.

Inicjalizacja oraz konfiguracja komponentu Chart odbywa się w metodzie componentDidLoad(), gdzie tworzony jest nowy obiekt Chart. Podczas jego tworzenia przekazywana jest odpowiednia konfiguracja, która jest tworzona na podstawie przekazanych przez nas właściwości.

componentDidLoad() {
this.canvas = this.el.querySelector('canvas');
this.context = this.canvas.getContext('2d');
const chartConfig: any = {
type: 'line',
data: {
labels: this.data.x,
datasets: [{
label: this.label,
backgroundColor: this.color,
borderColor: this.color,
fill: false,
data: this.data.y,
}]
},
options: {
responsive: true,
title: {
display: true,
text: this.header
},
scales: {
xAxes: [{
display: true,
scaleLabel: {
display: true,
labelString: this.xAxisLabel
}
}],
yAxes: [{
display: true,
scaleLabel: {
display: true,
labelString: this.yAxisLabel
}
}]
}
}
};

this.chart = new Chart(this.context, chartConfig);
}

Punkty do wykresu dodawane są za pomocą metody addValue(value: number), która jako argument przyjmuje wartość typu number. W naszym przypadku jest to wartość temperatury lub wilgotności powietrza. Natomiast wartość X, czyli czas, obliczany jest już w samej metodzie za pomocą statycznej metody TimeSeriesChart.getCurrentTime().

@Method()
async addValue(value: number) {
// update X values
this.data.x = [...this.data.x, TimeSeriesChart.getCurrentTime()];
if (this.data.x.length > this.size) {
this.data.x.shift();
}
this.chart.data.labels = this.data.x;

// update Y values
this.data.y = [...this.data.y, value];
if (this.data.y.length > this.size) {
this.data.y.shift();
}
this.chart.data.datasets.forEach((dataset: any) => {
dataset.data = this.data.y;
});

// display new data
this.chart.update();
}

Metoda addValue wywoływana jest w momencie, gdy zostanie wyemitowane zdarzenie serialNewDataAvailableEvent, które informuje nas, że zostały odczytane nowe dane z Arduino. Ważne jest, aby dane wysyłane z Arduino były za każdym razem inne od poprzednich, bo w przeciwnym wypadku zdarzenie to nie zostanie "złapane" przez nasz listener. Dlatego w strukturze ramki umieściłem dodatkowe pole przechowujące wartość timestamp, które gwarantuje, że każda kolejna ramka z danymi będzie miała inną wartośc w stosunku do poprzedniej.

@Listen('serialNewDataAvailableEvent', {target: 'document'})
serialNewDataAvailableEventListener(event) {
let array = event.detail.split('|');
this.sensor = new Sensor(array[0], Number(array[1]), Number(array[2]));
this.temperatureChart.addValue(this.sensor.temperature);
this.humidityChart.addValue(this.sensor.humidity);
}
Dlaczego nie Property Binding?

W tym przypadku nie mogłem zastosować zwykłego "Property Binding", ponieważ taka architektura nie sprawdziłaby się w tym przypadku, gdyż dane muszą być dodawane zawsze do wykresu, nawet jeśli są takie same jak poprzednie. Tradycyjne bindnowanie sprawdza się tylko, gdy chcemy wyrenderować komponent z nowymi danymi/wartościami, które są różne od poprzednich. Reasumując, "Binding state to props" sprawdza się tylko, gdy za każdym razem otrzymujemy różne wartości w stosunku do poprzednich, a w naszym przypadku tak nie jest (możemy odczytać kilkukrotnie tą samą wartość temperatury i wilgotności powietrza).

Cały kod komponentu TimeSeriesChart można znaleźć tutaj: time-series-chart.tsx, natomiast cały gotowy projekt, który możesz uruchomić na swoim komputerze dostepny jest na moim GitHubie.

Uruchomienie projektu

Poniżej zamieszczam krótki wideoporadnik pokazujący, jak w szybki i prosty sposób można uruchomić ten projektu na własnym komputrze.

git clone https://github.com/DevTomek-pl/Arduino-Custom-Data-Station.git
cd ./Arduino-Custom-Data-Station
git checkout display-time-series-chart-data
npm install
npm start

Podsumowanie

Na tym etapie mamy już stworzoną całą aplikację, która odczytuje dane z Arduino za pomocą Web Serial API oraz prezentuje odczytane dane w czasie rzeczywistym na wykresach.

Co dalej?

Kolejny post z tego cyklu będzie już dotyczył procesu deploymentu aplikacji na hosting Firebase.