Skocz do zawartości

Kurs programowania w Processing - #4 - OpenGL, Arduino!


Pomocna odpowiedź

Po miesiącu oczekiwania, w końcu połączymy świat rzeczywisty ze światem wirtualnym - albowiem połączymy dzisiaj Arduino wraz z Processingiem, robiąc razem 3 projekty.

Ten artykuł bierze udział w naszym konkursie! 🔥
Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł.

konkurs_forbot_nagrody_1-350x147.png

Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu!
Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły »

Zacznijmy od prostego scenariuszu: posiadamy urządzenie oparte o Arduino (lub inną płytkę), i dane z tego urządzenia chcemy wykorzystać w processingu - czy to do wyświetlenia ich, odczytania i zapisania w pliku, czy aktualizacji ich w bazie danych. Do połączenia takiego Arduino z komputerem posłuży nam (wirtualny) port szeregowy, znany z konsoli w Arduino IDE.

image.thumb.png.759c7cd8575e1f781b59cdccd3526dd4.png

Ogólnie rzecz biorąc - i co było w kursie Arduino - będziemy się komunikować przez UART, czyli będziemy wysyłać (tutaj) ośmiobitowe bajty. Informacja ta będzie dla nas bardzo ważna, gdy przejdziemy do kodowania. Ale, kontynuując: zrobimy sobie właśnie takie urządzenie, z którego dane będą wysyłane do Processingu w przystępnej formie. Zbudujemy sobie dość znany projekt radaru ultradźwiękowego. Składniki:

  • czujnik HC-SR04
  • płytka z mikrokontrolerem (Arduino, ESP, Nucleo)
  • konwerter usb - uart, jeżeli nasza płytka takiego nie posiada
  • serwo SG90 lub inne cyfrowe
  • przewody połączeniowe, płytka stykowa.

IMG20210321130141.thumb.jpg.086b13e3e2eea835f144ad2fca29f8e5.jpg

Prosiłbym o zignorowanie wyświetlacza, pierwotnie miał zostać użyty, lecz po badaniu wśród znajomych uznałem że nie wszyscy mogą takowy mieć. Jeżeli taki jednak masz, nie krępuj się go dodać!

Całość łączymy zgodnie ze schematem:

radar.thumb.png.688f5fc63f9aaf783175f032cb0adc27.png

Przykładowe wykonanie może wyglądać tak. Wydrukowałem uchwyt czujnika do serwa (równie dobrze można przykleić taśmą), a samo serwo przykleiłem przez taśmę do stołu. 

IMG20210321140241.thumb.jpg.a18d760be7771909cb746d925fdaf0a8.jpg

Przed kodowaniem, rozpiszmy sobie co chcemy osiągnąć, na razie na samym Arduino. Mikrokontroler powinien czekać na znak od komputera, zanim zacznie skanowanie. Skanować powinien co jeden stopień, a następnie dane o rotacji i zmierzonej odległości przesłać do Processingu. Tak więc; zaczynamy od zaimportowania potrzebnych bibliotek. Potrzebujemy biblioteki od serwa, i użytkownicy Plaftormio IDE muszą dodać bibliotekę Arduino.

#include <Arduino.h>
#include <Servo.h>

Następnie zdeklarujmy serwo, oraz 2 zmienne, przechowujące kąt obrotu, oraz jego kierunek.

Servo serwo;
byte obrot = 0;
bool kierunek = true;

Z racji tego że serwo będziemy obracać w zakresie 0-180 stopni, nie musimy użyć zmiennej typu int tylko byte - warto pamiętać o takiej optymalizacji, ponieważ pozwoli nam to wcisnąć więcej kodu przy rozbudowanych programach. Aby program uczynić uniwersalnym, zdefiniujmy piny:

#define PIN_SERVO 2
#define PIN_ECHO 4
#define PIN_TRIG 3

Następnie przechodzimy do funkcji setup(). Przede wszystkim musimy rozpocząć komunikację UART, podłączyć serwo i piny czujnika. 

Serial.begin(115200);
serwo.attach(PIN_SERVO);
pinMode(PIN_ECHO, INPUT);
pinMode(PIN_TRIG, OUTPUT);

Pozostało nam jedynie poczekać, aż dostaniemy znak od komputera aby rozpocząć pracę. Jest na to dość prosty trik:

while(Serial.available() > 0) Serial.read();// czyscimy bufor
while(!Serial.available());                 // czekamy
while(Serial.available() > 0) Serial.read(); // czyscimy bufor znowu

Najpierw w pętli while czyścimy bufor wejściowy, odczytując to co tam może siedzieć. Środkowa pętla while() będzie blokowała cały program, dopóki nie dostanie jakiegokolwiek znaku od komputera. Po odebraniu takowego, czyścimy bufor znowu - aby nie zaśmiecić go przypadkiem, co potem mogłoby spowodować błąd programu.

W funkcji loop() chcemy obracać serwo. Obróćmy je zatem:

serwo.write(obrot);

Aby jednak obracało się ono o 180 stopni, powinniśmy dodać kod który w zależności od kierunku - dodaje lub odejmuje stopień.

if(kierunek) obrot++;
else obrot--;
if(obrot >= 180) kierunek = 0;
if(obrot <= 0) kierunek = 1;

Następnie chcemy wykonać pomiar czujnikiem. Chcemy go także wysłać do komputera, pytanie w jakiej formie? Zwykłe wysłanie dwóch liczb nie będzie działało zbyt dobrze. Spróbujemy zrobić zatem "pakiet" danych, zbudowany z nagłówka, oraz samych danych.

Serial.print("ok:\t");
Serial.print(zmierz());
Serial.print("\t");
Serial.println(obrot);

Dane jakie wysyłamy zaczynają się od "ok:". Oczywiście, każdy może sobie zacząć jak chce; dla mnie oznacza to skrót od obrót i kąt. Znak \t to tabulator - dla programu to może być inny, jak spacja czy znak nowej linii, lecz tabulator pozwala na czytelne podejrzenie danych w konsoli - jak ktoś nie wierzy, może sam sprawdzić. Ostatni wiersz wysyła jeszcze znak nowej linii, który nam powie że to koniec pakietu. Żeby wszystko działało stabilnie (dać czas na pomiar i obrót serwa) do pętli możemy dodać małego delaya.

delay(5);

No i oczywiście, wysyłamy zmienną zmierz() typu int. Napiszmy zatem co ona zwraca. Jest to kod wzięty bezpośrednio z kursu Arduino, więc myślę że działanie jest jasne.

int zmierz()
{
  int czas = 0;
  int dystans = 0;

  digitalWrite(PIN_TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(PIN_TRIG, HIGH);
  delayMicroseconds(10);
  digitalWrite(PIN_TRIG, LOW);

  czas = pulseIn(PIN_ECHO, HIGH);
  dystans = czas / 58;

  return dystans;
}

Także cały program powinien wyglądać mniej więcej (z naciskiem na więcej) tak: (dodałem tylko kilka komunikatów i komentarzy)

#include <Arduino.h>
#include <Servo.h>

Servo serwo;
byte obrot = 0;
bool kierunek = true;

#define PIN_SERVO 2
#define PIN_ECHO 4
#define PIN_TRIG 3

int zmierz()
{
  int czas = 0;
  int dystans = 0;

  digitalWrite(PIN_TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(PIN_TRIG, HIGH);
  delayMicroseconds(10);
  digitalWrite(PIN_TRIG, LOW);

  czas = pulseIn(PIN_ECHO, HIGH);
  dystans = czas / 58;

  return dystans;
}

void setup()
{
  Serial.begin(115200);
  Serial.println("Inicjalizacja radaru...");
  Serial.println("Inicjalizuje serwo.");
  serwo.attach(PIN_SERVO);
  Serial.println("Inicjalizuje czujnik ultradzwiekowy.");
  pinMode(PIN_ECHO, INPUT);
  pinMode(PIN_TRIG, OUTPUT);
  Serial.println("Czekam na dowolny znak aby rozpoczac dzialanie.");

  while(Serial.available() && Serial.read()); // czyscimy bufor
  while(!Serial.available());                 // czekamy
  while(Serial.available() && Serial.read()); // czyscimy bufor znowu
}

void loop()
{
  if(kierunek) obrot++;
  else obrot--;
  if(obrot >= 180) kierunek = 0;
  if(obrot <= 0) kierunek = 1;

  serwo.write(obrot);
  Serial.print("ok:\t");
  Serial.print(zmierz());
  Serial.print("\t");
  Serial.println(obrot);
  delay(5);
}

Możemy teraz wgrać kod do Arduino, i przystąpić do pisania programu docelowego. Patrząc na radary w wyszukiwarce, chcemy utworzyć półkole w którym będziemy rysować odległości. Jednak! Chcemy przecież pracować z portem szeregowym - na sam początek skryptu musimy zatem dodać bibliotekę od tego. Jest ona domyślnie zainstalowana. Tworzymy od razu port szeregowy port.

import processing.serial.*;

Serial port;

Następnie tworzymy zmienne przechowujące odebrane dane. Ja sobie jeszcze stworzyłem zmienną pomocniczą do rysowania samego radaru.

float obrot = 0;
float odleglosc = 200;
int liczbaPolkoli = 5;

W funkcji setup, standardowo definiujemy okienko, oraz jego wielkość. Tworzymy nowy port szeregowy (znowu wchodzi tutaj składnia javy, więc może być ciężko):

void setup()
{
  size(800, 600); 
  port = new Serial(this, "COM1", 115200);
  background(0);
}

Port COM zastępujemy naszym portem w Windowsie, lub /dev/tty w linuxie. Jeżeli nie wiemy gdzie mamy nasze urządzenie, możemy to sprawdzić w menedżerze urządzeń, lub tym prostym kodem:

printArray(Serial.list());

Wypisze on w konsoli listę dostępnych portów. 

Przechodząc do funkcji draw() - zaczynamy od zapamiętania obecnego położenia okna, i ustawienia środka układu współrzędnych w prawie że centrum okienka. Pozwoli to nam na bardzo łatwe i przyjemne narysowanie wykresu. Zamiast wartości 100 jednak lepiej byłoby wpisać wyliczoną wartość, np. height/6.

pushMatrix();
translate(width/2, height-100);

Następnie definiujemy pozostałe rzeczy potrzebne do narysowania interfejsu:

strokeWeight(2);
noFill();
stroke(0,127,0);
textAlign(CENTER);

Teraz możemy przejść do właściwego narysowania całości. Chcemy osiągnąć efekt podobny do tego:

image.thumb.png.3272232f8a15d2ef200df9e4faa7b173.png

Zaczynamy zatem od narysowania linii poziomej wzdłuż osi X. Następnie liczmy zmienną pomocniczą, określającą promień najmniejszego półkola - pozostałe będą miały promień jego wielokrotności. Znowu, dodałem mały, stały margines - który także powinien zostać wyliczony, w celu zachowania responsywności.

line(-width/2,0,width/2,0);
float promien = (width-50)/liczbaPolkoli;

Teraz musimy się zastanowić, jak tutaj narysować 5 półkoli i się nie namęczyć. Chcemy na pewno zrobić to w pętli for. Chcemy także zastosować arc(), bo chcemy mieć połówkę koła, a nie stricte krzywą. Przydałoby się też narysować odległości, bo w końcu od tego mamy te półkole - żeby pozwoliły nam oszacować odległość.

  for(int i = 1; i <= liczbaPolkoli; i++)
  {
    arc(0,0, promien*i, promien*i, PI, TWO_PI); // domyślnie, czyli od srodka
  }

Ta pętla nam narysuje same półkola. Zauważ, że zaczynamy ją od 1 a nie 0; w końcu chcemy narysować 5 półkoli, a nie 4. Środek prostokąta, w który jest wrysowany nasz wycinek koła, znajduje się dokładnie w punkcie 0, 0. Szerokość i wysokość będą te same, jak mówiłem, wyliczony promień i jego wielokrotności. Następnie, z racji tego że po prawej stronie mamy 0 stopni, to chcemy wyświetlić wycinek od π radianów (180 stopni) do 2π radianów (360 stopni). Jednak aby narysować tekst, no, trzeba byłoby się nagimnastykować. Trzeba go usadowić odpowiednio względem półkola, obliczyć jego wysokość, odległość...

lub możemy przesunąć całą siatkę.

  for(int i = 1; i <= liczbaPolkoli; i++)
  {
    arc(0,0, promien*i, promien*i, PI, TWO_PI); // domyślnie, czyli od srodka
    pushMatrix();
    translate(promien*i/2, 20);
    fill(0,127,0);
    text(map(promien*i,0,width,0,200), 0,0);
    noFill();
    popMatrix();
  }

Musimy teraz narysować linie pokazujące obecny kąt. Chcemy ich 5, od 150 do 30 stopni co 30 stopni. Zrobimy więc taką samą pętlę for, ale... tutaj jednak trzeba trochę pokombinować. Między pętlami obracam całą siatkę o 210 stopni, czyli linia będzie narysowana na 150 stopniach. Skoro obracamy całą siatkę, to po prostu rysujemy linię wzdłuż osi Y. 

   
  //...
  popMatrix();
  }
  rotate(radians(210));
  
  for(int i = 1; i <= 5; i++)
  {
    line(0,0,width/2,0);

Następnie ponownie przesuwamy siatkę aby narysować tekst. Obracam jeszcze całość o 90 stopni, inaczej tekst także narysowałby się wzdłuż osi Y.

    pushMatrix();
    translate(width/2+10, 0);
    rotate(radians(90));
    fill(0,127,0);
    text(180 - (i*30) + "°", 0, 0);
    noFill();
    popMatrix();

Na końcu pętli wystarczy jeszcze dopisać obrót o 30 stopni. Po wykonaniu się całości, siatka się obróci o 210 + 5 * 30 = 360 stopni, czyli "wróci do domu".

  for(int i = 1; i <= 5; i++)
  {
    line(0,0,width/2,0);
    pushMatrix();
    translate(width/2+10, 0);
    rotate(radians(90));
    fill(0,127,0);
    text(180 - (i*30) + "°", 0, 0);
    noFill();
    popMatrix();
    rotate(radians(30));
  }

Zostało nam narysować samą linię radaru. 

  stroke(0,255,0);
  pushMatrix();
  rotate(PI + radians(obrot));
  line(0,0,width/2,0);
  stroke(255,0,0);
  line(constrain(odleglosc*2, 0, 200),0,width/2,0);
  popMatrix();

Znowu obracamy siatkę, tym razem o kąt odczytany z Arduino. Rysujemy zieloną linię (stroke 0,255,0), oraz czerwoną (stroke 255,0,0). Pierwszy punkt powinien być odsunięty o odległość, ja jeszcze przemnożyłem to x2 aby efekt był lepiej widoczny. Program jak widać jest prosty, kilka przekształceń siatki i tyle. Jak szukałem niegdyś gotowych projektów, to jak ludzie się trudzili; funkcje trygonometryczne; twierdzenie talesa, pitagorasa... Należy zatem zawsze szukać najprostszej drogi, i dążyć do optymalizacji.

Na końcu pętli zostało nam wrócić do lewego górnego rogu, oraz - aby linie się nie nakładały, możemy albo zrobić co klatkę nowe tło (background), lub narysować półprzezroczysty prostokąt.

  popMatrix(); //wracamy do lewego gornego rogu
  
  //symulujemy zanik linii
  noStroke();
  fill(0,4);
  rect(0,0,width,height);
}

Taki prostokąt da bardzo przyjemny efekt. Jeżeli cały obraz mamy gotowy do rysowania, to możemy przystąpić do omówienia kwestii połączenia z Arduino. Zacznijmy od napisania prostej funkcji, wysyłającej do płytki ten "znak", że ma zacząć pracę - i to dosłownie:

void keyPressed()
{
   port.clear();
   port.write('f');
}

Po naciśnięciu dowolnego klawisza zostanie wysłany znak, rozpoczynający pracę. Przed wysłaniem jedynie czyścimy profilaktycznie bufor. Zostało teraz odczytywać dane z Arduino. Za każdym razem, kiedy coś przychodzi do komputera, wykonuje się funkcja serialEvent. Podajemy jej tylko argument, port, wskazujący konkretnie z jakim portem mamy do czynienia. W środku, podobnie jak w Arduino, odczytujemy bufor:

void serialEvent(Serial port)
{
  String odczyt = port.readStringUntil('\n');
  if(odczyt != null) println(odczyt);
  else return;
}

Tylko tyle wystarczy, aby wypisać co tam nam Arduinen wysyła. Jednak my chcemy jeszcze odczytać wartości z naszego pakietu, i przypisać je do zmiennych. Akurat java ma taki plus, że oferuje szeroki asortyment funkcji związanych ze Stringami.

  if(odczyt.charAt(0) == 'o')
  {
    String[] wartosci = odczyt.split("\t");
    odleglosc = parseFloat(wartosci[1]);
    obrot = parseFloat(wartosci[2]);
  }

Na początku - sprawdzamy nagłówek pakietu. Powinienem napisać warunek sprawdzający całość (equals() ), ale z racji tego że wysyłamy tylko jeden pakiet, sprawdzanie pierwszej litery wystarczy. Następnie dzielimy sobie to co przyszło na pojedyncze kawałki (tutaj się przydaje oddzielanie tabulatorem), i przypisujemy odczytane dane do zmiennych. Muszą jednak one zostać przekonwertowane do typu float, lub int (warto zapamiętać, że wartości odległości zwykle przechowuje się w float).

Cały program może zatem wyglądać tak:

import processing.serial.*;

Serial port;

float obrot = 0;
float odleglosc = 200;
int liczbaPolkoli = 5;


void setup()
{
  size(800, 600); 
  port = new Serial(this, "COM1", 115200);
  background(0,0,0);
}

void draw()
{
  pushMatrix();
  translate(width/2, height-100); //srodek na srodku wykresu
  
  strokeWeight(2);
  noFill();
  stroke(0,127,0);
  textAlign(CENTER);
  
  line(-width/2,0,width/2,0);
  float promien = (width-50)/liczbaPolkoli;
  
  for(int i = 1; i <= liczbaPolkoli; i++)
  {
    arc(0,0, promien*i, promien*i, PI, TWO_PI); // domyślnie, czyli od srodka
    pushMatrix();
    translate(promien*i/2, 20);
    fill(0,127,0);
    text(map(promien*i,0,width,0,200), 0,0);
    noFill();
    popMatrix();
  }
  rotate(radians(210));
  
  for(int i = 1; i <= 5; i++) //opisujemy półkola
  {
    line(0,0,width/2,0);
    pushMatrix();
    translate(width/2+10, 0);
    rotate(radians(90));
    fill(0,127,0);
    text(180 - (i*30) + "°", 0, 0);
    noFill();
    popMatrix();
    rotate(radians(30));
  }
  
  //siatka narysowana, rysujemy linie
  stroke(0,255,0);
  pushMatrix();
  rotate(PI + radians(obrot));
  line(0,0,width/2,0);
  stroke(255,0,0);
  line(constrain(odleglosc*2, 0, 200),0,width/2,0);
  popMatrix();
  
  popMatrix(); //wracamy do lewego gornego rogu
  
  //symulujemy zanik linii
  noStroke();
  fill(0,4);
  rect(0,0,width,height);
}

void serialEvent(Serial port)
{
  String odczyt = port.readStringUntil('\n');
  if(odczyt != null) println(odczyt);
  else return;
  
  if(odczyt.charAt(0) == 'o')
  {
    String[] wartosci = odczyt.split("\t");
    odleglosc = parseFloat(wartosci[1]);
    obrot = parseFloat(wartosci[2]);
  }
}

void keyPressed()
{
   port.clear();
   port.write('f');
}

Działanie tego urządzenia najlepiej opisze poniższy film. Bardzo przepraszam za bałagan na biurku, ale nie mogę się wziąć jakoś za sprzątanie :<

 

No i teraz bardzo fajnie, pierwszy projekt zaliczony. Co jednak, jeżeli chcemy wysyłać dane z komputera do Arduino? Albo, co gorsza, mieć komunikację dwustronną? Jak się okazuje, nie jest to takie trudne. Zrobimy sobie kolejny projekt, tym razem trochę mniej efektowny. Przyjmujemy taki scenariusz: mamy prostego robota z czujnikiem nachylenia, nacisku, i potencjometrem do kalibracji; a za napęd stanowią 3 silniki. Aby nie komplikować sprawy i nie budować całego robota do wykonania takiego ćwiczenia, uprośćmy sprawę do 3 potencjometrów i 3 ledów:

robocik.thumb.png.2322ce85a2048c8cd00199b0c7071bd8.png

3 potencjometry do wejść analogowych, 3 ledy do wyjść cyfrowych PWM. Myślę że każdy będzie w stanie wykonać takie ćwiczenie w praktyce. Tak wygląda wykonanie u mnie:

IMG20210327160659.thumb.jpg.6cd892241fc54ff35425cebcf8291970.jpg

(nie polecam potencjometrów precyzyjnych, bo trzeba się nakręcić aby zmienić wartość jakoś znacznie)

Kod Arduino będzie wyglądał bardzo podobnie. Piny, zmienne, i bufor danych.

#include <Arduino.h>

#define PIN_CZUJNIK_WYCHYLENIA A0
#define PIN_CZUJNIK_NACISKU A1
#define PIN_POTENCJOMETR A2
#define PIN_SILNIK_A 9
#define PIN_SILNIK_B 10
#define PIN_SILNIK_C 11

int nacisk = 0;
int nastawienie = 0;
int wychylenie = 0;

int silnikA = 0;
int silnikB = 0;
int silnikC = 0;

char dane[64];

Funkcja setup() mówi sama za siebie, zastosowaliśmy znowu sztuczkę czekania na znak.

void setup()
{
  Serial.begin(115200);
  while(Serial.available() > 0) Serial.read();
  while(!Serial.available());
  while(Serial.available() > 0) Serial.read();
  pinMode(PIN_CZUJNIK_NACISKU, INPUT);
  pinMode(PIN_CZUJNIK_WYCHYLENIA, INPUT);
  pinMode(PIN_POTENCJOMETR, INPUT);

  pinMode(PIN_SILNIK_A, OUTPUT);
  pinMode(PIN_SILNIK_B, OUTPUT);
  pinMode(PIN_SILNIK_C, OUTPUT);
}

W pętli głównej, w kwestii samych peryferiów nie trzeba za wiele robić.

void loop()
{
  nacisk = analogRead(PIN_CZUJNIK_NACISKU);
  wychylenie = analogRead(PIN_CZUJNIK_WYCHYLENIA);
  nastawienie = analogRead(PIN_POTENCJOMETR);
  
  analogWrite(PIN_SILNIK_A, silnikA);
  analogWrite(PIN_SILNIK_B, silnikB);
  analogWrite(PIN_SILNIK_C, silnikC);

Napiszmy teraz standardowego ifa, sprawdzającego czy cokolwiek przyszło:

  if(Serial.available() > 0)
  {
    Serial.readBytesUntil('\n', dane, 64);

    switch(dane[0])
    {
      case 'a':
        silnikA = dane[1];
        break;
      case 'b':
        silnikB = dane[1];
        break;
      case 'c':
        silnikC = dane[1];
        break;
    }
  } 

Jeżeli mamy jakiekolwiek dane, wczytujemy je do bufora. Ramka danych wejściowych będzie wyglądała "xy\n", gdzie x to numer silnika, y to jego wartość. Mamy zatem 3 rodzaje pakietów. Z racji tego że operujemy tutaj na bajtach (co jest bardzo ważne), możemy zwyczajnie przypisać do zmiennej wartość z bufora, choć później dokładniej o tym powiem.

Następnie może dość banalny (i niezbyt optymalny) sposób na działanie, ale jeżeli Arduino nie odebrało danych, to zawsze je może wysłać:

else //jezeli nie ma danych, to wysylamy swoje dane
  {
    Serial.print("nwp");
    Serial.print('\t');
    Serial.print(nacisk);
    Serial.print('\t');
    Serial.print(wychylenie);
    Serial.print('\t');
    Serial.print(nastawienie);
    Serial.println('\t');

    Serial.print("abc");
    Serial.print('\t');
    Serial.print(silnikA);
    Serial.print('\t');
    Serial.print(silnikB);
    Serial.print('\t');
    Serial.print(silnikC);
    Serial.println('\t');
  }

Wysyłamy 2 pakiety. W pierwszym, nacisk, wychylenie, potencjometr (przypominam, że każdy to nazywa jak chce) wysyłamy po kolei te wartości. Drugi pakiet w zasadzie nam się nie przyda, ale jest przydatny przy debugowaniu, o czym już mówię. 

Cały kod Arduino powinien wyglądać tak:

#include <Arduino.h>

#define PIN_CZUJNIK_WYCHYLENIA A0
#define PIN_CZUJNIK_NACISKU A1
#define PIN_POTENCJOMETR A2
#define PIN_SILNIK_A 9
#define PIN_SILNIK_B 10
#define PIN_SILNIK_C 11

int nacisk = 0;
int nastawienie = 0;
int wychylenie = 0;

int silnikA = 0;
int silnikB = 50;
int silnikC = 255;

char dane[64];

void setup()
{
  Serial.begin(115200);
  while(Serial.available() > 0) Serial.read();
  while(!Serial.available());
  while(Serial.available() > 0) Serial.read();
  pinMode(PIN_CZUJNIK_NACISKU, INPUT);
  pinMode(PIN_CZUJNIK_WYCHYLENIA, INPUT);
  pinMode(PIN_POTENCJOMETR, INPUT);

  pinMode(PIN_SILNIK_A, OUTPUT);
  pinMode(PIN_SILNIK_B, OUTPUT);
  pinMode(PIN_SILNIK_C, OUTPUT);
}

void loop()
{
  nacisk = analogRead(PIN_CZUJNIK_NACISKU);
  wychylenie = analogRead(PIN_CZUJNIK_WYCHYLENIA);
  nastawienie = analogRead(PIN_POTENCJOMETR);
  
  analogWrite(PIN_SILNIK_A, silnikA);
  analogWrite(PIN_SILNIK_B, silnikB);
  analogWrite(PIN_SILNIK_C, silnikC);

  if(Serial.available() > 0)
  {
    Serial.readBytesUntil('\n', dane, 64);

    switch(dane[0])
    {
      case 'a':
        silnikA = dane[1];
        break;
      case 'b':
        silnikB = dane[1];
        break;
      case 'c':
        silnikC = dane[1];
        break;
    }
  } 
  else //jezeli nie ma danych, to wysylamy swoje dane
  {
    Serial.print("nwp");
    Serial.print('\t');
    Serial.print(nacisk);
    Serial.print('\t');
    Serial.print(wychylenie);
    Serial.print('\t');
    Serial.print(nastawienie);
    Serial.println('\t');

    Serial.print("abc");
    Serial.print('\t');
    Serial.print(silnikA);
    Serial.print('\t');
    Serial.print(silnikB);
    Serial.print('\t');
    Serial.print(silnikC);
    Serial.println('\t');
  }
}

 Przechodząc do processingu, nagłówek będzie wyglądał podobnie. Poza małymi tablicami bajtów. Są to nasze małe bufory, o których - i o wszystkim innym - zaraz powiem.

import processing.serial.*;

Serial port;

int nacisk = 000;
int nastawienie = 0;
int wychylenie = 0;

byte silnikA[] = {'a', '0', '\n'};
byte silnikB[] = {'b', '0', '\n'};
byte silnikC[] = {'c', '0', '\n'};

void setup()
{
  size(800, 600); 
  port = new Serial(this, "COM5", 115200);
}

Z racji tego że mi wystarczy jedynie obraz tego co się dzieje, to będę chciał narysować jedynie prostokąt, którego jeden bok będzie się ściskał. Wartość z potencjometru do kalibracji będzie zmieniała jego barwę, a "czujnik nachylenia", obróci go  odpowiednio. Oczywiście, w prawdziwym robocie możemy stworzyć bardziej zaawansowaną wizualizację, pomagającą w debugowaniu itd. Dlatego, standardowo już, środek daję do środka okienka, odpowiednio przekształcam przed rysowaniem siatkę danych o dane odebrane z Arduino, i rysuję prostokąt. Konkretnie rzecz biorąc, jest to kształt złożony z 3 prostych i krzywej.

void draw()
{
  background(200);
  pushMatrix();
  translate(width/2, height/2);
  rectMode(CENTER);
  stroke(0);
  rotate(radians(map(wychylenie, 0, 1023, -180, 180)));
  fill(map(nastawienie, 0, 1023, 0,  255));
  
  beginShape();
  vertex(-width/4,height/8);
  vertex(-width/4,-height/8);
  curveVertex(-width/4, -height/8 - map(nacisk, 0, 1023, 0, height));
  curveVertex(-width/4, -height/8);
  curveVertex(width/4, -height/8);
  curveVertex(width/4, -height/8 - map(nacisk, 0, 1023, 0, height));
  vertex(width/4, height/8);
  vertex(-width/4, height/8);
  endShape();
  
  popMatrix();
}

void keyPressed()
{
   port.clear();
   port.write('f');
}

Zauważ tylko, ile razy użyliśmy width i height. Dzieląc te liczby przez odpowiednie proporcje, bardzo łatwo zrobiliśmy elastyczny i responsywny program - będzie działał w każdej rozdzielczości.

Funkcję do odczytywania danych możemy przepisać praktycznie z poprzedniego przykładu. 

void serialEvent(Serial port)
{
  String odczyt = port.readStringUntil('\n');
  if(odczyt == null) return;
  else println(odczyt);
  
  if(odczyt.charAt(0) == 'n')
  {
    String[] wartosci = odczyt.split("\t");
    nacisk = parseInt(wartosci[1]);
    wychylenie = parseInt(wartosci[2]);
    nastawienie = parseInt(wartosci[3]);
  }

I teraz tłumaczę już, dlaczego Processing i Arduino mogą się nie lubić. Przede wszystkim, Arduino posiadające zaledwie 2KB ramu bardzo nie lubi się ze Stringami, o czym można poczytać w Internecie. Dlatego (przynajmniej ja) zalecam pracę na bajtach i znakach (byte i char), ostatecznie na łańcuchach znaków i funkcji z specyfikacji języka C, a nie biblioteki String. Dlatego chcemy do Arduino przesyłać bajty, a nie ciągi znaków (String). Ale jeden znak to jeden bajt, co nam może ułatwić sprawę, nie? Otóż i tak i nie. Na pewno ułatwi nam to, że do tych zmiennych możemy przypisać zarówno 'a', jak i 48. I tutaj kryje się pułapka, bo Processing razem z javą nie mają zmiennych przyjmujących 0-255, jak byte w Arduino - zamiast tego, przyjmują wartości od -128 do 127. Arduino w takim razie, aby wartość bezpośrednio dekodować, musi zmienić tą wartość z tego zakresu na 0-255, chociażby funkcją map. Czyli Arduino nie lubi się ze Stringami, z kolei processing z bajtami... a jak zechcemy wysłać

port.write("a" + silnikA + "\n");

to zgodnie z tablicą ASCII otrzymamy 3 bajty, po kolei 97, 48 (jeżeli silnik jest wyłączony), i 10, ponieważ Processing nam to połączy w Stringa. Z drugiej strony,

port.write('a' + silnikA + '\n');

Działa poprawnie, ale Processing działa bardzo wolno. Dlatego, korzystając z tego że funkcja write może przyjąć typ int, byte[] lub Stringa, stworzyłem właśnie te małe bufory, które wysyłam do Arduino. Wysyłam je za każdym razem kiedy Processing odebrał dane, więc działa to na zasadzie ping-pong. Fragment programu będzie wyglądał tak:

  silnikA[1] = parseByte(map(nastawienie, 0, 1023, 0, 255));
  silnikB[1] = parseByte(map(nacisk, 0, 1023, 0, 255));
  silnikC[1] = parseByte(map(wychylenie, 0, 1023, 0, 255));
  port.write(silnikA);
  port.write(silnikB);
  port.write(silnikC);

oczywiście, zamiast przepisywać wartości z potencjometrów do silników powinny się tu znaleźć zaawansowane algorytmy regulatorów, dbających o poprawne działanie robota. Całość będzie wyglądała tak:

import processing.serial.*;

Serial port;

int nacisk = 000;
int nastawienie = 0;
int wychylenie = 0;

byte silnikA[] = {'a', '0', '\n'};
byte silnikB[] = {'b', '0', '\n'};
byte silnikC[] = {'c', '0', '\n'};

void setup()
{
  size(800, 600); 
  port = new Serial(this, "COM5", 115200);
}

void draw()
{
  background(200);
  pushMatrix();
  translate(width/2, height/2);
  rectMode(CENTER);
  stroke(0);
  rotate(radians(map(wychylenie, 0, 1023, -180, 180)));
  fill(map(nastawienie, 0, 1023, 0,  255));
  
  beginShape();
  vertex(-width/4,height/8);
  vertex(-width/4,-height/8);
  curveVertex(-width/4, -height/8 - map(nacisk, 0, 1023, 0, height));
  curveVertex(-width/4, -height/8);
  curveVertex(width/4, -height/8);
  curveVertex(width/4, -height/8 - map(nacisk, 0, 1023, 0, height));
  vertex(width/4, height/8);
  vertex(-width/4, height/8);
  endShape();
  
  popMatrix();
}

void serialEvent(Serial port)
{
  String odczyt = port.readStringUntil('\n');
  if(odczyt == null) return;
  else println(odczyt);
  
  if(odczyt.charAt(0) == 'n')
  {
    String[] wartosci = odczyt.split("\t");
    nacisk = parseInt(wartosci[1]);
    wychylenie = parseInt(wartosci[2]);
    nastawienie = parseInt(wartosci[3]);
  }
  silnikA[1] = parseByte(map(nastawienie, 0, 1023, 0, 255));
  silnikB[1] = parseByte(map(nacisk, 0, 1023, 0, 255));
  silnikC[1] = parseByte(map(wychylenie, 0, 1023, 0, 255));
  port.write(silnikA);
  port.write(silnikB);
  port.write(silnikC);
}

void keyPressed()
{
   port.clear();
   port.write('f');
}

I jak zwykle, działanie zaprezentuje najlepiej film.

Teraz znowu wrócimy na chwilę do teorii. Wspominałem o tym, że Processing może wyświetlać także programy używając OpenGL. Pozwoli nam to rysować programy sprzętowo, pozwalając na turbodoładowanie, no i na wyświetlanie brył w 3D. Nie musimy pobierać żadnych bibliotek, wystarczy że w przy rozmiarze okna w size() dodamy:

void setup()
{
  size(600, 600, P3D);

I od teraz nasz program będzie działał w 3D. Wiąże się to jednak z pewnymi rzeczami. Przede wszystkim, nie możemy już określać odległości od lewego górnego rogu. Nie mamy już do czynienia z płótnem, a z przestrzenią. Zatem wszystkie kształty będziemy rysować przesuwając siatkę współrzędnych. No i pozostałe proste kształty, jak linia, elipsy, prostokąty, dalej mogą zostać narysowane. Dochodzą jednak nowe kształty:

box(bok);
sphere(promien);

kolejno pudełko i kulka. Możemy także tworzyć własne kształty za pomocą vertexów:

image.thumb.png.da796e23a04a0bc91dcc6c9cb8e87f44.png

Możemy także oteksturować nasze bryły. Jest to jednak dość trudne, do każdego vertexa dochodzą 2 argumenty: v, określające który kawałek obrazka ma być przyklejony na kształt. Aby załadować tekstury, ładujemy zwykły PImage i przed podaniem jakiegokolwiek vertexu po beginShape() wpisujemy texture():

image.thumb.png.059ad8099b608503969b32fec27807b5.pngimage.thumb.png.ebf49e55795dcee4a3105c48f0f8a4fa.png

Warto też zauważyć, że nie ma już rotate(), a rotateX , Y i Z, ponieważ teraz możemy obracać w 3 osiach. Kontynuując; w przypadku PShape wystarczy wywołać po prostu

x.setTexture(img);

Processing pozwala nawet zmienić oświetlenie sceny. lights() włączy ogólnie oświetlenie, noLights() je wyłączy.

lights();
noLights();

Do dyspozycji mamy 4 rodzaje oświetlenia, choć uważam że najbardziej potrzebne będą 2 pierwsze, a 2 kolejne tylko wymienię.

ambientLight(r, g, b);
directionalLight(r, g, b, a, b, c);
spotLight(r, g, b, x, y, z, a, b, c, r, k);
pointLight(r, g, b, x, y, z);

image.thumb.png.99c17abd61f78b23ff7e6a8a1097198f.png

Ambient light to oświetlenie ogólne, można powiedzieć że światło dzienne. Możemy ustalić jego kolor podając r, g i b. Directional light najlepiej opisać jako Słońce; świeci się w określonym kierunku, ale daleko. A, b i c określa kierunek, przyjmuje wartości -1 - 1. Spot light można określić jako latarkę, bo ma i kolor (rgb), i kierunek(abc), i pozycję (xyz), i kąt świecenia (r), i skupienie (k). Point light to taka żarówka, więc wpisujemy jej po prostu kolor i położenie.

Jeszcze jest ważna rzecz dotycząca 3D. Możemy włączyć "tryb rzutu prostokątnego": 

image.thumb.png.c229cdec511c7cf3454b82c86036d03e.png

 

I jeżeli chodzi o 3D, tyle powinno wystarczyć do napisania dość zaawansowanej aplikacji. Oczywiście, nie omówiłem dokładnie kształtów, shaderów, oświetlenia, kamery, lecz myślę że tyle wystarczy. Spróbujmy zatem stworzyć jeszcze jeden projekt, obracającą się kostkę. Do tego przyda się Arduino z żyroskopem i akcelerometrem, np. MPU6050:

mlotek.thumb.png.09c40f79b627998a650fd9424c0dc6a2.png

I tak wygląda moja aranżacja. Nazwałem to smartHammer™.

IMG20210321130201.thumb.jpg.2ce4565a43b6230406ee6e489dfd48b5.jpg

MPU jest połączone z Arduino przez I2C, i wymaga specjalnych bibliotek. 

#include <Arduino.h>
#include "I2Cdev.h" //biblioteka I2C
#include "MPU6050_6Axis_MotionApps20.h" //biblioteka MPU

Następnie tworzymy mpu, zmienne pomocnicze. 

MPU6050 mpu;
bool dmpOk = false;
byte dmpStatus; //status dmp
byte buforFIFO[64]; //odebrane dane z mpu

Teraz zdefiniujemy dość nietypowe typy danych, które przechowają nam odczyty z czujnika.

Quaternion q; // dosc smieszny typ danych
VectorFloat g; // wektor trójwymiarowy, przechowujący akceleracje
float ypr[3]; // tutaj będziemy trzymać kąty

Po tym - już klasycznie. Myślę że kod jest klarowny:

void setup()
{
    Wire.begin();
    Serial.begin(115200);

    Serial.println("Inicjalizacja MPU...");
    mpu.initialize();

    if(mpu.testConnection())
        Serial.println("MPU zainicjalizowane pomyslnie.");
    else
        Serial.println("BLAD: MPU nie moglo zostac zainicjalizowane.");

    Serial.println("Czekam na znak, aby rozpoczac prace...");

    while(Serial.available() && Serial.read()); // czyscimy bufor
    while(!Serial.available());                 // czekamy
    while(Serial.available() && Serial.read()); // czyscimy bufor znowu

Następnie, jeżeli wszystko działa, kalibrujemy i odpalamy DMP, moduł czujnika. W przeciwnym wypadku mówimy co poszło nie tak.

    if (dmpStatus == 0) 
    { //jest dobrze

        mpu.CalibrateAccel(6); //kalibracja, 600 prób
        mpu.CalibrateGyro(6);
        mpu.PrintActiveOffsets();

        Serial.println("Odpalam DMP...");
        mpu.setDMPEnabled(true);

        dmpOk = true;
    }
    else
    {
        Serial.print("BLAD! Status: "); // 1 - blad pamieci
        Serial.print(dmpStatus); // 2 - blad dmp
        Serial.println(".");
    }
}

W funkcji loop() z kolei, nie wykonujemy nic jeżeli DMP nie działa, a jak mamy odczytane dane z buforu, to czytamy: obrót, akceleracje, i kąty. Kolejno yaw, pitch, roll: pochylenie, przechylenie i odchylenie:

image.thumb.png.b4c69bfd2b959dcbc7367cfed768b4b9.png

Źródło: http://calypteaviation.com/wp-content/uploads/2013/12/przechylenie-odchylenie-pochylenie.jpg

Interesujące może być użycie "&". W tym wypadku nie są to operatory AND, tylko mówią kompilatorowi, żeby te zmienne nie były kopiowane. Jeżeli będę miał funkcję void która jako argument ma funkcję x, i ona zmieni wartość x, to po wykonaniu ta wartość zostanie. Tak więc tutaj funkcje te wymagają tegoż "&". Następnie wysyłay paczkę danych. 

Przechodząc do kodu Processingu. Nagłówek pliku będzie wyglądał, jak pozostałe, podobnie:

import processing.serial.*;

float x,y,z;
Serial port;
float YAW = 0;
float PITCH = 0;
float ROLL = 0;

void setup()
{
  size(600, 600, P3D);
  x = width/2;
  y = height/2;
  z = 0;
  
  port = new Serial(this, "COM5", 115200);
}

Uruchamiamy tryb 3D, oraz liczymy zmienne pomocnicze x, y, z. Użyjemy ich tutaj:

void draw()
{
  pushMatrix();
  background(0);
  fill(255);
  translate(x, y, z);
  rotateX(radians(ROLL));
  rotateY(radians(-YAW));
  rotateZ(radians(PITCH));
  rectMode(CENTER);
  box(100);

Przesuwamy i obracamy. Rysujemy też pudełko. Na końcu możemy narysować tekst:

  popMatrix();
  textSize(25);
  text("YAW: " + YAW, 20, height-100);
  text("PITCH: " + PITCH, 20, height-75);
  text("ROLL: " + ROLL, 20, height-50);
}

A funkcje odczytu przepisać z poprzednich przykładów:

void serialEvent(Serial port)
{
  String odczyt = port.readStringUntil('\n');
  if(odczyt != null) println(odczyt);
  else return;
  
  if(odczyt.charAt(0) == 'y')
  {
    String[] wartosci = odczyt.split("\t");
    YAW = parseFloat(wartosci[1]);
    PITCH = parseFloat(wartosci[2]);
    ROLL =parseFloat( wartosci[3]);
  }
}

void keyPressed()
{
   port.clear();
   port.write('f');
}

Całość będzie wyglądać tak:

import processing.serial.*;

float x,y,z;
Serial port;
float YAW = 0;
float PITCH = 0;
float ROLL = 0;

void setup()
{
  size(600, 600, P3D);
  x = width/2;
  y = height/2;
  z = 0;
  
  port = new Serial(this, "COM5", 115200);
}

void draw()
{
  pushMatrix();
  background(0);
  fill(255);
  translate(x, y, z);
  rotateX(radians(ROLL));
  rotateY(radians(-YAW));
  rotateZ(radians(PITCH));
  rectMode(CENTER);
  box(100);
  
  popMatrix();
  textSize(25);
  text("YAW: " + YAW, 20, height-100);
  text("PITCH: " + PITCH, 20, height-75);
  text("ROLL: " + ROLL, 20, height-50);
}

void serialEvent(Serial port)
{
  String odczyt = port.readStringUntil('\n');
  if(odczyt != null) println(odczyt);
  else return;
  
  if(odczyt.charAt(0) == 'y')
  {
    String[] wartosci = odczyt.split("\t");
    YAW = parseFloat(wartosci[1]);
    PITCH = parseFloat(wartosci[2]);
    ROLL =parseFloat( wartosci[3]);
  }
}

void keyPressed()
{
   port.clear();
   port.write('f');
}

I jak zwykle działanie pokaże filmik (jeszcze raz przepraszam za bałagan):

I jeżeli chodzi o tą część, to tyle. I chyba to już nawet wszystko co planowałem wykonać w ramach kursu. Ale! Jestem świadomy że nie omówiłem ogromu możliwości Processingu. Mogę opublikować jeszcze jedną część, i tutaj pomyślałem, że może opiszę to, co chcą czytelnicy - bo opisywać wszystko co wiem i co chcę wiedzieć, to by z 10 części wyszło 😛 Jeżeli czytelnicy nie będą chcieli kolejnej części, to w takim razie gratuluję ukończenia kursu Processingu, możesz teraz tworzyć niesamowite aplikacje graficzne, łączące świat rzeczywisty z wirtualnym poprzez Arduino. GRATULACJE !

 

  • Lubię! 2
Link to post
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.