Processing — oscyloskop (finał)

Oscyloskop

Kontynuujemy tworzenie oscyloskopu w processingu. Skupimy się na ostatnich barkujących elementach -kreska przesuwająca się w trakcie zbierania pomiarów, napisy na ekranie oraz obsługa myszy (historia wartości). 

Animacja kreski

Chcę dodać kreskę (od góry ekranu do dołu, u mnie kolor żółty, linia bardzo cienka – na 1 pixel) przesuwającą się z lewa do prawa podczas działania naszego oscyloskopu. Przedstawia to sekwencja poniższych obrazków:

     

Rozwiązań jest wiele, a my proponujemy następujące:

  1. kasujemy starą kreskę w pozycji xPos (czyli rysujemy ją kolorem tła),
  2. rysujemy dalszą część właściwego wykresu, 
  3. rysujemy nową kreskę w pozycji xPos+skokX (naszym ulubionym kolorem).

Te trzy kroki powodują, że nie musimy „odświerzać” całego ekranu dla każdego kawałka krzywej na oscyloskopie (tj. nie musimy rysować całego wykresu od początku, bazując na wcześniej zapamiętanych wartościach otrzymywanych z Arduino). To rozwiązanie jest proste i działa fajnie, dlatego je stosujemy.

void draw(){
 int x=getValue(); 
 print("z Arduino x=",x);
 y=wspA*x;
 
  strokeWeight(1);  //grubość kreski = 1 pixel
  stroke(0);  //kasowanie = kolor ekranu 
  line(xPos, 0, xPos, height);

  strokeWeight(10);// grubość kreski dla wykresu
  stroke(0,0,255); //kolor wykresu
  line(xPos - skokX, poprzedniY, xPos, y);

  strokeWeight(1); //ponownie grubość kreski
  stroke(0,0,200);// moj ulubiony kolor
  line(xPos+skokX, 0, xPos+skokX, height);
   
   xPos=xPos+skokX;
   if (xPos>=width){
     xPos=0;
     background(0);
   }
}

Na uwagę zasługuje

  • ciągła „zabawa” z kolorami i grubościami linii – oddzielnie dla kreski, oddzielnie dla tworzonego wykresu (no i kasowanie kreski musi mieć tą samą grubość, co nasza pionowa kreska).

Napisy

Są niezbędne, aby szybko orientować się jakie wartości są rysowane – mamy wszak możliwość skalowania osi y, czyli zawężania/zwiększanie widocznego zakresu. Dodatkowo, można (a nawet wypada) pisać wartość aktualnie otrzymaną z Arduino. Modyfikujemy funkcję setup() dodając (gdzieś na koniec) następujące linie

PFont f;
f = createFont("Arial",16,true); // Arial, 16 point, anti-aliasing on
textFont(f,36);

W powyższym kodzie przygotowujemy processing do pisania teksów – posługujemy się pomocniczą zmienną f (bez większego znaczenia). Aby pisać napisy używamy 

fill(0,0,100); //zmieniamy kolor teksu, jeśli chcemy
textSize(36); //zmieniamy rozmiar tekstu, jeśli chcemy
text("gawryl", 10,100); //piszemy tekst "gawryl" w pozycji x=10, y=100

Funkcja text() ma spore możliwości, dlatego warto zajrzeć do jej instrukcji. Można nawet używać jej w ten sposób:

int i=17;
text(i, 10,100); //piszemy liczbę całkowitą i
text(3.0*i, 10, 200); //piszemy liczbę rzeczywistą
float f=3.14;
text(f, 10, 300);//piszemy liczbę rzeczywistą

Jak widać konwersja typów na napisy następuje automatycznie – to bardzo ułatwia sprawę pisania tekstów. Proponuję testować poznane możliwości – ale szybko dojdziemy do etapu, gdzie napisy nachodzą na siebie, przekrywają się, są nieczytelne i należy je wyczyścić.

Czyszczenie napisów

Postępowanie podobne do metody z animowaną linią (z początku zajęć) nie jest jednak zadawalające. Chodzi o to, że skoro zdecydowaliśmy się na wyeliminowanie czyszczenia ekranu do minimum i kasowanie poprzenich napisów polega na pisanie ich ponownie kolerm tła – to jednak pozostają jakieś pozostałości. Dlatego… proponuję ograniczyć obszar wykresu tylko do części okna, a nie do całej dostępnej szerokości x wysokości okienka. Dzięki temu zabiegowi będziemy mogli bezkarnie czyścić obszar przeznaczony do napisy (zielony prostokąt na rysunku), a pozostawiać w spokoju właściwy wykres (obszar z czarnym tłem). Aby to zobrazować popatrzmy na rysunek:

Proponuję więc, aby każdorazowo funkcja draw() rysowała prostokąt (u mnie w kolorze zielonym) w określonych pozycjach, kasując całą zawartość tam się znajdującą. Następnie nowe napisy moga być już wypisywane wewnątrz tego prostokątu, bez brzydkich artefaktów z poprzedniej metody. Dalej następuje rysowanie właściwego wykresu i pionowej kreski, od której rozpoczęliśmy nasze zajęcia.

Pomysł z zielonym obszarem oczywiście wymaga zmiany współczynników skalowania wspAwspB prostej dopasowującej wczytany x z Arduino na współrzędną y okienka processinga. Powodzenia!

Historia wykresu

Chodzi o to, aby można było zatrzymać kreślenie wykresu (to już mamy obsługiwane) a dodatkowo aby można było lewym klawiszem myszy kliknąć i sprawdzić (wypisać w zielonym obszarze) wartość wykresu. Musimy więc pamiętać poprzednie pomiary – będziemy je przechowywać w pomocniczych tablicach. Dlatego wprowadzamy zmienne globalne

int[] values;
float[] times;

void setup(){

Deklarację tablic values oraz times umieszczamy gdzieś na początku kodu, aby zmienne te były widoczne wewnątrz wszystkich funkcji poniżej – a więc setup(), getValue() oraz draw(). Następnie modyfikujemy funkcję setup() o utworzenie konkretnych rozmiarów tych tablic:

values = new int[width];
times = new float[width];

Nie wnikając w zawiłości dynamicznego przydzielania pamięci – powyższe linie tworzą teyle elementów tablicy values oraz times, jaka jest szerokość naszego okienka z wykresem. Przy okazji widać, że skoro Arduino nadaje do nas liczby z zakresu 0..1023 to na ich przechowywanie przygotowałem tablicę liczb całkowitch, natomiast druga tablica przechowuje liczby rzeczywiste. Ta druga tablica będzie notować mi czas odebrania (w sekundach) przez processing konkretnej liczby – bardzo przydatne w późniejszej pracy z oscyloskopem.

Modyfikujemy funkcję odbierającą dane z portu szeregowego o fragment wypełniający nasze tablice:

int getValue(){
  int value = -1;
  while (port.available() >= 3){
    if (port.read() == 0xff){
       value = (port.read() <<8) | (port.read());
       values[xPos]=value;
       times[xPos]=millis()/1000.0;
    }//if
  }//while
  return value;
}//getValue

Zdecydowałem się na zapisywanie liczb do tablic z wykorzystaniem zmiennej xPos – takie rozwiązanie jest sprytne, ale będzie pociągało za sobą pewne następstwa. Przy okazji poznajemy nową funkcję processinga millis() – która, podobnie jak w Arduino, zwraca czas działania programu w milisekundach – dzięki temu zapisujemy czas „odebrania” danej nadesłanej z Arduino. Liczbę tą dzielę przez 1000.0 aby mieć wartość w sekundach (z przecinkiem – jak w Polsce lubimy).

Obsługa myszy – historia

Mówiliśmy o obsłudze myszy sprawdzając zmienną mousePressed – dzięki czemu wiemy, że wciśnięto przycisk myszy. Ale taki sposób stosujemy wewnątrz funkcji draw(), a my chcemy zatrzymać kreślenie wykresu (pomijając wywołanie funkcji draw()) a jednocześnie obsłużyć zdarzenia z myszy. Dlatego proponuję zrobić to tak:

void mousePressed() {
  int i = (mouseX/skokX)*skokX;
//pomocnicze linie
  print("x= ",mouseX,i); 
  print(" historia= ",values[i]); 
  println(" ", i]);
//właściwe wypisywanie
  fill(0,200,0);//kolor czyszczenia prostokata
  rect(0,height-50, width, height);
  fill(255);//kolor dlugopisu
  text(times[i], 300, height-20);
  text(values[i], 200, height-20);
}

Specjalna funkcja o nazwie mousePressed() będzie wywoływać się automatycznie za każdym razem, gdy mysz będzie klikana. My musimy wpisać tylko odpowiedni kod.

Mój kod ma aż 3 linie pomocniczych wypisywań — do usunięcia w finalnej wersji programiku. Zmienna i potrzeba mi jest po to, aby przeliczyć współrzędną x-ową kursora myszki na odpowiedni indeks w tablicy – wszak nasz oscyloskop wyposażyliśmy w możliwość rysowania punktów rzadko lub gęsto (decyduje o tym zmienna skokX, obsługiwana z klawiatury). Skoro więc dwuktornie odwołuję się do tego samego indeksu, wypisując wartość times oraz values – nawet w finalnej wersji, pozbawionej pomocniczych print-ów –  to nie liczę tego każdorazowo, tylko przechowuję go w zmiennej.

Ja swoje napisy rysuję 20 pikseli od dołu ekranu, w pozycjach 200 i 300 (licząc od  lewej strony). To jest w obszarze mojego „zielonego prostokąta”.

Czego się dziś nauczyłeś

  • pisanie tekstów
  • tworzenie dynamicznych tablic
  • (inna) obsługa myszy

(ciągle) pozostaje kilka kwestii do rozwiązania:

  • poprawne wczytywanie liczb (z Arduino wysyłamy 0..1023, a processing „widzi” tylko jakieś małe liczby, <30?)
  • skalowanie wykresu – czyli chcemy mieć tak, by liczby z zakresu 0..1023 zawsze mieściły się na ekranie, a nie „wyskakiwały” poza aktualną wysokość ekranu
  • skalowanie minimalnej liczby (więcej skalowań)
  • przesuwanie wykresu góra/dół (więcej skalowań)
  • obsługa myszki i wyświetlanie dowolnych wartości z historii wykresu
  • pisanie napisów w oknie processinga (np. liczb z wartościami na wykresach)
  • pisanie skali na wykresie (np. mL i ML z brzegu okienka)
  • podpis osi x – można użyć czasu do opisu tej osi, będzie widać kiedy odebrano konkretną daną
  • ładniejsza grafika

Zapraszam na kolejne zajęcia, na których podłączymy nasz oscyloskop do czujki pola magnetczynego oraz do fotorezystora.

Processing — oscyloskop (starcie nr 3)

Oscyloskop

Kontynuujemy tworzenie oscyloskopu w processingu. Tym razem chcemy dodać (proste) skalowanie wykresu oraz obsługę klawiatury. Na koniec słów kilka o optymalizacji kodu – uczmy się tworzyć mądre oprogramowanie. Uruchamiamy Arduino ze skeczem z poprzednich zajęć i koncentrujemy się na processingu.

Skalowanie

Stosujemy funkcję liniową y=ax+b aby dopasować otrzymywane liczby z Arduino (nazwijmy je x-ami) do wysokości okienka graficznego Processinga (nazwijmy je y-kami). Arduino „nadaje” nam x z przedziału 0..1023, a okienko z naszym wykresem ma height pikseli wysokości, czyli y jest z przedziału 0..height. Rysujemy prostokąty dla każdej otrzymanej liczby – choć dla jasności kodu powróćmy na chwilę do rysowania tych prostokątów od góry ekranu. 

Funkcja rect(x0,y0,szer,wys) – przypomnienie

Przypominam, że używamy funkcji rect(x0,y0, szer, wys) do rysowania prostokąta. Para liczb x0,y0 to współrzędne lewego górnego rogu, natomiast szer,wys to odpowiednio szerokość i wysokość prostokąta. Ostatnia para liczb liczona jest względem x0,y0 więc współrzędne przeciwległego rogu prostokąta (prawy dolny róg) wynosi (x0+szer, y0+wys). Warto zaznaczyć, że liczby szer,wys mogą być ujemne, co wykorzystamy za chwilę.

Wracamy do skalowania

Po przypomnieniu o funkcji rect wracamy do rysowania przeskalowanych prostokątów – żądamy więc, aby spełnione było:

  1. dla wczytanego x=zero mamy mieć minimalną wysokość ekranu, czyli y=zero, oraz
  2. dla wczytanego x=1023 (maksymalna liczba nadawana z Arduino) mamy mieć maksymalną współrzędną y, czyli h=height.

Powyższe warunki dla skalowania liniowego y=ax+b przyjmują matematyczną postać (nie znamy jeszcze współczynników skalowania ab):

  1. 0=a*0+b
  2. height=a*1023+b

Musimy rozwiązać układ równań 1. i 2. wyznaczając ab. Nie jest to trudne, gdyż momentalnie otrzymujemy:

b=0 oraz a=heigh/1023.

Dlatego ulepszona funkcja rysująca prostokąty (od góry ekranu) wygląda następująco:

float y;
void draw(){
 int x=getValue(); 
 print("z Arduino x=",x);
 y=height/1023.0*x;
 rect(xPos, 0, 10, y);
 println(" y=", y);
 
 xPos=xPos+10;
 if (xPos>=width){
   xPos=0;
   background(0);
 }
}

Na uwagę zasługują:

  • utworzenie pomocniczej zmiennej y przechowującej wartość przeskalowanego x-a
  • wyliczanie y-ka wykorzystuje dzielenie liczb rzeczywistych – zwróć uwagę na liczbę 1023.0 pisaną z kropeczką. Bez takiego postępowania otrzymywalibyśmy zero, jeden lub dwa (liczby całkowite), gdyż dzielenie wartości height (liczba typu int) przez 1023 (bez kropeczki – też liczba typu int!) pomija część ułamkową. 
  • uzbrajam kod w wypisywanie na ekran różnych pomocniczych informacji, tutal: wczytanego „gołego” x-a, oraz przeskalowanego y-ka (aby wszystko dobrze kontrolować).

Mamy już działające skalowanie, dzięki czemu liczby nie „wsykoczą” poza ekran okienka!

Sukces! dlatego lekko to ulepszymy rysując prostokąty od dołu ekranu:

float y;
void draw(){
 int x=getValue(); 
 print("z Arduino x=",x);
 y=height/1023.0*x;
 rect(xPos, height, 10, -y);
 println(" y=", y);
 
 xPos=xPos+10;
 if (xPos>=width){
   xPos=0;
   background(0);
 }
}

Cała zmiana polega na „zaczepieniu” lewego-górnego rogu prostokąta na dole okienka graficznego – współrzędna (xPos, height), nadanie mu szerokości =10 pikseli a wysokości… właśnie -y (minus y). Nie powinno to zaskakiwać w świetle przypomnienia o funkcji rect (przeczytaj o tym wyżej), bo wówczas współrzędna prawego-dolnego rogu prostokąta wynosi właśnie (xPos+10, height-y). Otrzymujemy:

Możemy być już z siebie dumni 😉

Obsługa klawiatury 

Dodajemy nową funkcjonalność do naszego programu — chcemy, aby klawisz spacja zatrzymywał rysowanie wykresu. Arduino ciagle działa i nie przestaje nadawać liczb na port szeregowy, tym samym wszystkie liczby nie są odbierane przez processing i je „tracimy” – można pomyśleć o stworzeniu bufora na te liczby, ale dla prostoty my tak nie będziemy robić.

Funkcja keyReleased() 

Obsługuje klawiaturę w processingu – wystarczy, że w jej ciało wpiszemy obsługę konkretnych klawiszy, a wysztko będzie działać automatycznie – za to właśnie kochamy processing! Zacznijmy od prostego przykładu:

void keyReleased() {
  if (key == ' ') 
    println("SPACJA!")
}

Dodanie powyższej funkcji do naszego programu skutkuje tym, że przy każdym naciśnięciu spacji w konsoli tekstowej processinga pojawi się napis „SPACJA!”. Fajnie! i jeszcze raz podkreślę – jak prosto.

A co to jest tutaj zmienna key? Otóż jest to jedna z wielu zmiennych, których nie trzeba deklarować w processingu, a która przechowuje kod ostatnio wciśniętego klawisza na klawiaturze.

Jak więc uruchamiać i zatrzymywać tworzenie wykresu wciskając spację? W tym celu dodajemy pomocniczą zmienną stop i robimy coś takiego:

boolean stop=false;
void keyReleased() {
  if (key == ' ') {
    stop=!stop;
    if (stop)
      println("PAUZA");
    else
      println("GO!");
  }//if spacja
}

Objaśnienia:

  • zmianna typu boolean przyjmuje dwie wartości – true lub false (inaczej mówiąc tak i nie, zero i jeden…) i informuje nas, czy zatrzymujemy wykres czy nie,
  • w funkcji keyReleased() sprawdzamy, czy wciśnięto spację czy nie, a jeśli tak to zmieniamy wartość zmiennej stop na przeciwną (instrukcja stop=!stop). Następnie wypisujemy odpowiedni komunikat.

Aby nie tylko wypisywany był odpowiedni komunikat ALE i faktycznie wykres przestawał się rysować, należy zmodyfikować funkcję draw, odpowiedzialną za rysowanie wykresu, w odpowiedni sposób (uwzględniający istnienie zmiennej stop):

boolean stop=false;
float y;
void draw(){
  if (!stop){
    int x=getValue(); 
    print("z Arduino x=",x);
    y=height/1023.0*x;
    rect(xPos, height, 10, -y);
    println(" y=", y);
 
    xPos=xPos+10;
    if (xPos>=width){
      xPos=0;
      background(0);
   }
  }//if stop
}

Jak widać zmiana polega na dodaniu ważnego if-a, który sprawdza, czy mam rysować wykres czy nie. Jeśli stop=false to negacja tego warunku jest prawdą, więc  wykonują się instrukcje z linii 5-14. Jeśli zaś stop=true (czyli po pierwszym wciśnięciu spacji) warunek z linii 4 nie jest spełniony (negacja prawdy to nieprawda!) i omijamy całą procedurę pobrania x-a z portu szeregowego, przeskalowanie, wypisywanie pomocniczych rzeczy w konsoli i kreślenie prostokątów (linie 5-14) i lądujemy w linii 16 (koniec if-a). Ale to już ostatnia linia funkcji draw więc… zgodnie z zasadami processinga funkcja draw wykonuje się ponownie. Przypomnę, że Arduino nie zaprzestaje nadawania liczb (nie sterujemy Arduino, tylko processingiem) więc dane te są tracone.

Rysowanie linii (zamiast prostokątów)

Zmieniamy rysowanie prostokątów na rysowanie linii. Ma to swoje uzasadnienie – w przyszłości kreślić będziemy nie dane z jednego portu analogowego Arduino, ale z dwóch, trzech… Prostokąty w takim przypadku pokrywałyby się i wykresy nie byłby czytelny. Dlatego kreślimy linię.

line(x1,y1, x2, y2) — pierwsza para liczb to współrzędne początku linii, druga para liczb – współrzędne końca linii. U nas linia oddalona jest o 10 pikseli w x-ach (czyli x2=x1+10) ale współrzędne y linii zależą od danych z Arduino: y1 to poprzednia dana, y2 to dana aktualnie otrzymana (oczywiście już po przeskalowaniu funkcją liniową). Skoro musimy pamiętać poprzednią daną to wprowadzamy pomocniczą zmienną do jej przechowywania poprzedniY i modyfikujemy kod w następujący sposób:

boolean stop=false;
float y, poprzedniY;
void draw(){
  if (!stop){
    int x=getValue(); 
    print("z Arduino x=",x);
    y=height/1023.0*x;
    line(xPos-10, poprzedniY, xPos, y);
    poprzedniY = y;
    println(" y=", y);
 
    xPos=xPos+10;
    if (xPos>=width){
      xPos=0;
      background(0);
   }
  }//if stop
}

Zwracam uwagę na:

  • dodanie pomocniczej zmiennej poprzedniY (linia 2)
  • rysowanie linii uwzględniające poprzedni pomiar (linia 8)
  • zamiana aktualnego y-ka na poprzedniY, ale już PO narysowaniu linii (linia 9)
  • ustaliłem grubość linii na 10 pikseli funkcją strokeWeight() – zrobiłem to na początku programu w funkcji setup()
  • ustaliłem kolor linii na czerwony funkcją stroke() – także w funkcji setup()


Przy pierwszym uruchomieniu programu wartość zmiennej poprzedniY jest nieustalona co nie jest dobrze, ale nam to nie przeszkadza – to tylko 1 pomiar (po nim wszystko już dobrze działa). Dlatego tym niuansikiem nie będziemy się dalej zajmować.

Ulepszenie skalowania (klawisze z/Z jak zoom)

Jak na razie wykorzystujemy potencjometr jako źródło danych do wyświetlania, więc u nas Arduino wysyła liczby z pełnego zakresu 0..1023 (odpowiadające napięciom z przedziału 0..5V). Ale przecież zależy nam na podłączaniu różnych czujek do Arduino i tworzenia wykresów, więc nie zawsze tak będzie. Może się bowiem okazać, że czujka „produkuje” napięcia z zakresu 0..1V i wówczas… bardzo duża część naszego wykresu nie jest wykorzystywana! Co więcej, jeśli w takim przypadku napięcia oscylują, to my tracimy te informacje – wykres jest za mało czytelny, bo „schowany” na dole ekranu a więc i te oscylacje są malutkie, nieczytelne. Przykład:

Jak temu zaradzić? Bardzo prosto – wprowadzamy dodatkową zmienną określającą maksymalną liczbę (zmienna ML) zamiast dotychczasowej liczby 1023 dla jakiej odbywa się skalowanie wysokości ekranu. Dzięki zmianie ML-a będziemy mogli „zajrzeć w głąb” wykresu, czyli ograniczyć zakres wykresu – powstanie „zoom” 😉

Kontrolę nad zmienną ML odbywa się z klawiatury za pomocą klawiszy 'z’ (małe zet) oraz 'Z’ (duże zet), w tan prosty sposób, że odpowiednio zmniejszamy/zwiększamy wartość ML dwa razy. Kod funkcji keyReleased pod zmianach:

float ML=1024;
boolean stop=false;
void keyReleased() {
  if (key == ' ') {
    stop=!stop;
    if (stop)
      println("PAUZA");
    else
      println("GO!");
  }//if spacja
  if (key == 'z') ML=ML/2;
  if (key == 'Z') ML=ML*2;
}

Dodatkowo należy zmienić skalowanie wartości y w funkcji draw() na następujące: 

y=(float)height/ML*w;

co wynika z nowych żądań dotyczących skalowania:

  1. dla wczytanego x=zero mamy mieć minimalną wysokość ekranu, czyli y=zero, oraz
  2. dla wczytanego x=ML (maksymalna liczba nadawana z Arduino) mamy mieć maksymalną współrzędną y, czyli h=height.

Warto też zmodyfikować wyświetlanie pomocniczych informacji w okienku terminala na uwzględniające wypisywanie aktualnej wartości ML-a, czyli:

    println(" y=", y, " ML=", ML);

Mamy teraz prostą możliwość zawężania widoku wykresu do wartości ML, którą kontrolujemy klawiszami z/Z. Oczywiście warto zadbać o to, aby zmniejszając wartość ML nie spaść na zero (co się może zdarzyć), więc należy dodać kolejnego if-a.

Większa kontrola nad skalowaniem wykresu

Warto też wprowadzić inne klawisze kontrolujące wawrtość ML-a, gdyż dzielenie/mnożenie przez 2 jest dobre tylko na początek zabawy. Inne klawisze mogą zmieniać ML np. o 100 – dbając także o to, aby nie uzyskać w ten sposób liczb ujemnych).

Dodatkowo można też rozważyć sytuację, że otrzymywane napięcie nie będzie z przedziału 0..1V a np. 0.5..0.7V — wówczas wykres może być ponownie nieczytelny, bo my kontrolujemy tylko maksymalną liczbę, a nie minimalną (u nas ona zawsze wynosi zero!). Można więc wprowadzić kolejną liczbę pomocniczą mL – minimalna liczba i ją zmieniać z klawiatury… Wówczas układ równań przy skalowaniu musi być zbudowany z żądań:

  1. dla wczytanego x=mL mamy mieć minimalną wysokość ekranu, czyli y=zero, oraz
  2. dla wczytanego x=ML (maksymalna liczba nadawana z Arduino) mamy mieć maksymalną współrzędną y, czyli h=height.

Tę modyfikację pozostawiam jako zadanie domowe 😉

Ulepszenie rysowania (klawisze x/X do historii wykresu)

Aktulanie nasze okienko ma szerokość 800 pikseli, a linie/prostokąty są rysowane co 10 pikseli. Oznacza to, że na jednym pełnym ekranie zmieścimy maksymalnie 80 punktów nadesłanych z Arduino. W ten sposób powstaje historia pomiarów o długości 80 pomiarów. Jeśli jednak zmienimy szerokość linii/prostokątów z 10 pikseli na 5, to ta historia wynosi już 160 pomiarów. Jeśli zmienimy szerokość na 2, to mamy już historię 400 punktów. Wiadomo, do czego zmierzam? Należy wprowadzić zmienną kontrolującą tą szerokość, nazwijmy ją skokX (o tyle punktów „skaczemy” w x-ach rysując linię/prostokąt) i wprowadźmy możliwość jej zmiany za pomocą przycisków x/X na klawiaturze.

int skokX=10;
boolean stop=false;
float y, poprzedniY;
void draw(){
  if (!stop){
    int x=getValue(); 
    print("z Arduino x=",x);
    y=height/1023.0*x;
    line(xPos-skokX, poprzedniY, xPos, y);
    poprzedniY = y;
    println(" y=", y);
 
    xPos=xPos+skokX;
    if (xPos>=width){
      xPos=0;
      background(0);
   }
  }//if stop
}

Zmiany są niewielkie:

  • wprowadziliśmy zmienną skokX z wartością początkową 10 (linia 1), oraz
  • zastąpiliśmy liczbę 10 zmienną skokX w dwóch miejscach – w rysowaniu (linia 9) oraz w miejscu zwiększania pozycji xPos (linia 13). 

Należy też zmodyfikować funkcję obsługi klawiatury o reakcję na wciskanie klawiszy x/X – powiedzmy odejmujemy/dodajemy wartość 1 do aktualnej wartości skokX. Należy jednak uwzględnić sytuację, żeby nie zmniejszyć tej zmiennej do zera (lub poniżej).

void keyReleased(){
  if (key == ' '){
    stop=!stop;
    if (stop)
      print("PAUZA");
    else
      print("WZNĂ“W");
    };
  if (key == 'z') ML=ML/2;
  if (key == 'Z') ML=ML*2;
  if (key == 'x') skokX-=1;  
  if (key == 'X') skokX+=1;
  //dodatkowe sprawdzenie
  if (skokX < 1) skokX=1;
}

Wynik działania programu może wyglądać tak (widzimy dłuuuugą historię pomiarów):

Optymalizacja

Czyli jak „kodować”, aby nasz program działał szybciej. Nawet jeśli teraz działa szybko, to i tak warto uczyć się optymalizacji. Jeśli tego nie będziemy robić, to będziemy marnować zasoby komputera (pamięć, CPU) na niepotrzebne instrukcje… Zaczynamy!

Przypatrzmy się funkcji keyReleassed() – mamy tam szereg if-ów. Jest raczej oczywiste, że muszą tam one być, ale nie w tej postaci. Bo jeśli wciśnięto klawisz Z to wykonujemy odpowiednią akcję, ale po co sprawdzać dalej, czy wciśnięto klawisz x albo X? przecież już obsłużyliśmy wciśnięcie przycisku Z. Dlatego wypada to zrobić z szeregiem if-elsów, jakoś tak:

void keyReleased(){
  if (key == 'z') ML=ML/2;
  else
    if (key == 'Z') ML=ML*2;
    else
      if (key == 'x') skokX-=1;  
      else
        if (key == 'X') skokX+=1;
  //dodatkowe sprawdzenie
  if (skokX < 1) skokX=1;
}

Teraz jest zdecydowanie lepiej! Wyobraźmy sobie, że jedno porównanie kosztuje 1$ (w komputerze walutą jest ułamek sekundy, jakie CPU musi spędzić na wykonaniu danej operacji – ale ja będę posługiwać się dolarami, bo mocno przemawia to do naszej wyobraźni). Widać więc, że jeśli wybrano klawisz 'z’ to zapłacimy za wykonanie funkcji tylko 1$, a nie 5$ jak w pierwotnym przykładzie – ogromna oszczędność!

Wygodnie posługiwać się instrukcją switch-case, zamiast dużej liczby par if-elsów, w ten oto sposób:

void keyReleased(){
 switch (key){
    case ' ': 
    stop=!stop;
    if (stop)
      print("PAUZA");
    else
      print("GO!");
    break;
    
    case 'z': ML=ML/2;break;
    case 'Z': ML=ML*2;break;
    case 'x': skokX-=1;break;
    case 'X': skokX+=1;break;
  }//switch
  //dodatkowe sprawdzenie
  if (skokX < 1) skokX=1;
}

Od razu zauważamy też, że nie ma co wykonywać sprawdzenia if (skokX<1) w każdym wywołaniu funkcji keyReleased(), gdyż to sprawdzenie dotyczy tylko przypadku zmiany zmiennej skokX – za pomocą klawisza 'x’. Dlatego to dodatkowe sprawdzenie przenosimy w odpowiednie miejsce:

   case 'x': skokX-=1; if (skokX < 1) skok=1; break;

i kasujemy z końca funkcji keyReleased(). Prosta sprawa, a poprawiamy wydajność naszego programu.

Kolejną optymalizacją jest samo skalowanie liczb: w funkcji draw() za każdym razem z odebranego x-a robimy odpowiedni (przeskalowany) y-k, co wymaga operacji dzielenia i mnożenia. Dużo – za dużo. To dzielenie wynika z wyliczenia współczynnika a dla prostej, który przecież zmienia się tylko wówczas, gdy zmieniamy zoom (klawisze z/Z). Jeśli nic nie zmieniamy, to po co wykonujemy dzielenie? Dzielenie jest drogie, więc warto tego unikać. Dlatego wprowadzę zmienną przechowującą wartość tego współczynnika (pamięć jest tania w porównaniu do dzielenia!), nazwę go wspA i będę go wyliczać tylko wówczas, gdy trzeba (klawisze z/Z).

float wspA;
void keyReleased(){
 switch (key){
 case ' ': 
   stop=!stop;
   if (stop)
     print("PAUZA");
   else
    print("GO!");
   break;
 
   case 'z': ML=ML/2;wspA=(float)height/ML;break;
   case 'Z': ML=ML*2;wspA=(float)height/ML;break;
   case 'x': skokX-=1;if (skokX < 1) skokX=1;break;
   case 'X': skokX+=1;break;
 }//swicth
}

oczywiście należy także zmodyfikować funkcję rysującą – draw()

void draw(){
 if (!stop){
   int x=getValue(); 
   print(x);
   y=wspA*w;
   line(xPos-skokX, poprzedniY, xPos, y);
   poprzedniY = y;
   println(" ", y, " ML=",ML, " skokX=", skokX);
 
   xPos=xPos+skokX;
   if (xPos >= width){
     xPos=0;
     background(0);
   }
 }//if stop
}

i widzimy, że wyznaczenie y-ka polega teraz już na wykonaniu tylko jednej operacji mnożenia – bez dzielenia. Sprytne, prawda? (uwaga: należy jeszcze dodać wyliczenie współczynnika wspA w funkcji setup(), aby wartość ta była ustalona na początku programu).

Będzie to tym bardziej efektywne, gdy nasze skalowanie będzie pełne i uwzględni nie tylko maksymalną wartość ML ale i minimalną mL (o czym pisałem gdzieś wyżej). W takim przypadku pojawi się niezerowy współczynnik b prostej, który wymaga kolejnych operacji mnożenia/dzielenia – dlatego trzeba wprowadzić kolejną pomocniczą zmienną, powiedzmy wspB i postępować podobnie.

Czego się dziś nauczyłeś

  • skalowanie
  • processing: rysowanie prostokątów
  • skalowanie 
  • obsługa klawiatury
  • skalowanie
  • instrukcje switch-case
  • optymalizacja
  • czy wspomniałem o skalowaniu? 😉

(ciągle) pozostaje kilka kwestii do rozwiązania:

  • poprawne wczytywanie liczb (z Arduino wysyłamy 0..1023, a processing „widzi” tylko jakieś małe liczby, <30?)
  • skalowanie wykresu – czyli chcemy mieć tak, by liczby z zakresu 0..1023 zawsze mieściły się na ekranie, a nie „wyskakiwały” poza aktualną wysokość ekranu
  • skalowanie minimalnej liczby (więcej skalowań)
  • przesuwanie wykresu góra/dół (więcej skalowań)
  • obsługa myszki i wyświetlanie dowolnych wartości z historii wykresu
  • pisanie napisów w oknie processinga (np. liczb z wartościami na wykresach)
  • pisanie skali na wykresie (np. mL i ML z brzegu okienka)
  • podpis osi x – można użyć czasu do opisu tej osi, będzie widać kiedy odebrano konkretną daną
  • ładniejsza grafika

Zapraszam na kolejne zajęcia!