Prototyp – gra zręcznościowa + (ponownie) podstawy

Pam Maksymilian rozbudował program o karanie za zbyt wczesne wciśnięcie przycisku – gratulacje! Wgrał owoce swojej pracy z wirtualnego Arduino do naszego prototypu i… działa! 

Mamy też nowych zainteresowanych Fi-BOTem- z Wydziału Mat-Inf – dlatego przy tej okazji wróciliśmy do podstaw (pinMode, digitalWrite). 

Za tydzień (10 kwietnia) będą ponownie podstawy – piny PWM – ale… z diodami RGB, oraz (popularnymi) listwami RBG. Wszystko to ma rozbudzić w nas twórczość nowego propotypu gry zręcznościowej. Zapraszam!

(c) KG 2018

Projekt — wirtualny, choć całkiem realny ;-)

Całe zajęcia poświęciliśmy wirtualnemu Arduino – aby każdy z nas miał swoją kopię układu nad którym pracujemy oraz… aby popracować samodzielnie w domu i zabłysnąć na kolejnych zajęciach 😉

Niestety – okazało się, że tinkercad.com nie oferuje sterownika HD44780 do obsługi popularnego LCD16x2 – musieliśmy wykorzystać komunikację UART (obiekt Serial). Troszkę szkoda…

W realu wygląda to tak:

Prawie ostateczna wersja oprogramowania (z kilkoma udziwnieniami – potencjometrami do konfigurowania trudności (szybkości) losowych zdarzeń) ale bez liczenia RUND i licznika czasu do końca gry:

byte btns[6]={2,3,4,5,6,7};
byte leds[5]={12,11,10,9,8};
byte i;
int czas_staly, czas_los;

void setup()
{
  Serial.begin(9600);
  for (i=0;i<6; i++)
    pinMode(btns[i], INPUT_PULLUP);
  for (i=0;i<5; i++)
    pinMode(leds[i], OUTPUT);
  Serial.println(analogRead(A5));
  srand(analogRead(A5));
  
  czas_staly=2*analogRead(A0);
  czas_los=2*analogRead(A1);
  Serial.print("KONFIG:");
  Serial.print("czas stały=");
  Serial.println(czas_staly);
  Serial.print("czas los=");
  Serial.println(czas_los);
  delay(1000);
}

byte los;
unsigned long int t1,t2;
void loop()
{
  delay(czas_staly+rand()%czas_los);
  los=rand()%5;
  Serial.print("los=");
  Serial.println(los);

  digitalWrite(leds[los], HIGH);
  
  t1=millis();
  while(digitalRead(btns[los])==HIGH);//nic nie rób, czekaj na klik!
  t2=millis();
  digitalWrite(leds[los], LOW);
  
  Serial.print("refleks=");
  Serial.println(t2-t1);
  delay(2000);
  czas_staly=2*analogRead(A0);
  czas_los=2*analogRead(A1);
}

Warte podkreślenia jest użycie tablic – to zdecydowanie ułatwiło losowanie LED-a i sprawdzenie, czy odpowiedni przycisk został wciśnięty czy nie. 

Do zrobienia:

  • rozbudowa softu o monitorowanie czasu reakcji – jeśli przekroczymy pozostały czas, to gra powinna się automatycznie zakończyć (i wyświetlić jakieś podsumowanie, a potem – rozpocząć się od początku)
  • rozbudowa softu o zabezpieczenie przez „strategią małpy” – klikanie wszystkich przycisków, aż któryś się trafi i będzie bardzo krótki czas reakcji. To już troszkę trudniejsze zadanie
  • niby kto powiedział, że ma to być tylko gra refleks? warto pomyśleć także o grze memory – i zaprogramować sekwencje błysków (przygotowane wcześniej lub może losowo?), które należy powtarzać i zdobywać punkty. Można w setupie() zadać pytanie o wybór gry: reflex czy memory (wybór odczytujemy klikając jeden albo drugi przycisk) a wówczas loop() będzie uruchamiał odpowiedni kod…

(c) KG 2018

P.S. Radek (pracowity gimnazjalisto) – skontaktuj się ze mną emailem, czekam! KG

Przycisk + dioda + random = REFLEKS!

Na zajęciach wykorzystaliśmy proste elementy do zbudowania prototypu maszyny badającej nasz refleks. Ale od początku.

Moduł z przyciskiem

Moduł posiada trzy piny – GND oraz Vcc to zalsilanie, S to stygnał wysyłany przez płytkę (na innych płytkach często oznaczony OUT). My używamy modułów firmy RobotDyn (o nazwie Button Switch module) i trzeba przyznać im dobrą jakość wykonania. Na dodatek układy te mają wbudowanego LED-a informującego o wciśnięciu przycisku.

Najpierw sprawdzamy, czy po wciśnięciu przycisku Arduino „zobaczy” jedynkę (stan HIGH) czy „zero” (stan LOW). Prosty programik poniżej:

#define gdzie 7
void setup(){
  pinMode(gdzie, INPUT);
  Serial.begin(9600);
}
void loop(){
   Serial.println(digitalRead(gdzie));
   delay(100);
}

Liczby losowe – rand()

Bibliotyki Arduino wyposażone są w funkcje pseudo losowe – czyli takie, które generują liczby „udające” prawdziwe liczby losowe. Mowa tu o funkcji rand() – aby to sprawdzić piszemy poniższy kod:

void setup(){
  Serial.begin(9600);
}
void loop(){
   Serial.print("czas= ");
   Serial.print(millis());
   Serial.print(" los=");
   Serial.printtln(rand());
   delay(100);
}

Widzimy więc duuuuuże (i losowe!) liczby całkowite, które wyświetlają się co 100ms. Aby z takich liczb zbudować coś konkretnego, np. typowy rzut kostki do gry – trzeba to lekko zmodyfikować przez użycie funkcji reszta z dzielenia całkowitego (tzw. modulo, symbol % w języku C/C++):

void setup(){
  Serial.begin(9600);
}
void loop(){
   Serial.print("czas= ");
   Serial.print(millis());
   Serial.print(" kostka=");
   Serial.printtln(1 + rand()%6);
   delay(100);
}

Jak to działa? Reszta z dzielenia całkowitego przez 6 zwraca liczby z przedziału 0..5, ale my dodajemy jeszcze jedynkę – otrzymujemy liczby 1…6 – czyli naszą kostkę do gry. W ten właśnie sposób możemy modyfikować wynik funkcji rand() i dopasowywać ją do naszych potrzeb.

Zapalenie LED-a co losowy czas

Podłączyliśmy niebieskiego LED-a bezpośrednio do pinu 13 Arduino i GND – bez dodatkowego, wymaganego opornika. Nie jest to poprawne połączenie (brak opornika = uszkodzenie LED-a), ALE niebieskie LEDy mają (wysokie) napięcie przewodzenia, około 3V. Arduino zasili je jednak 5V – co jest za dużo – i uszkadzamy naszego LED-a, ale go nie zniszczymy (celowo wybrałem niebieski LED, a nie inny – inne LEDy pracuą na napięciu ~2V, więc 5V by je zniszczyło). Zależy mi tutaj na prostocie budowy układu więc darowałem sobie niezbędny opornik (no i nie chiałem korzystać z wbudowanego LEDa #13 – bo jest mały i niewyraźny).

Chwilowo odłożyliśmy moduł z przyciskiem i zaprogramowaliśmy włączenei LED-a po losowym czasie od 5s, do 15s:

#define LED 13
void setup(){
  pinMode(LED, OUTPUT);
}
int i;
void loop(){
   //odczekanie 5..15 sekund
   delay(5000+ rand()%10000);
   //wlaczenie LED-a
   digitalWrite(LED, HIGH);
   delay(1000);
   digitalWrite(LED, LOW);
   //miganie - znak, ze za chwile powtarzamy zabawe
   for (i=0; i<4; i++){
       digitalWrite(LED, HIGH); 
       delay(200); 
       digitalWrite(LED, LOW);      
       delay(200);
   }//miganie
}

Warto pobawić się z tym programem, uzupełniając go o dodatkowe informacje wyświetlane przez monitor portu szeregowego – informujące, że trwa losowe czekanie, a potem, że włączono LEDa i na koniec – że zabawa od początku się zaczyna.

Program „badamy refleks”

Wracamy do przycisku – rozbudowujemy poprzedni program o odczytanie momentu wciśnięcia przycisku. Użytkownik ma to zrobić w momencie zapalenia się niebieskiego LEDa — tylko, że nie wiadomo, kiedy to dokładnie nastąpi (losowy czas z poprzedniego programu). Na koniec wyświetlimy czas jego reakcji – jego refleksu 😉

#define LED 13
#define gdzie 7
void setup(){
  pinMode(gdzie, INPUT);   
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
}
int i;
unsigned long int t1,t2;
void loop(){
   Serial.println("START!");
   //odczekanie 5..15 sekund
   delay(5000+ rand()%10000);
   t1=millis();
   //wlaczenie LED-a
   digitalWrite(LED, HIGH);
   while (digitalRead(gdzie)== HIGH);
   t2=millis();
   digitalWrite(LED, LOW);
   Serial.print("Rekacja (refleks)=");
   Serial.print(t2-t1);
   Serial.println(" ms");
   delay(500);
   //miganie - znak, ze za chwile powtarzamy zabawe
   for (i=0; i<4; i++){
       digitalWrite(LED, HIGH); 
       delay(200); 
       digitalWrite(LED, LOW);      
       delay(200);
   }//miganie
}

Kluczowa jest linia #17 – to w niej następuje zatrzymanie działania programu i oczekiwanie na rekację użytkownika. Zrealizowałem to za pomocą pętli podczytującej przycisk – w moim module naciśnięcie przycisku powoduje odczyt stanu LOW, natomiast stan HIGH oznacza brak wciśnięcia. Jak widać ta pętla NIC nie robi. Właśnie o to mi tu chodziło – pętla nic nie robi, więc ponownie wracamy do sprawdzenia warunku pętli while – bez straty czasu. I tak w kółko, aż w końcu naciśnięty zostanie przycisk. 

A jak mierzę czas? Za pomocą funkcji millis() – która zwraca czas (w milisekundach) od uruchomienia Arduino. Robię to dwukrotnie – przed odczytaniem przycisku zapisuję do zmiennej t1, a po naciśnięciu przycisku – do zmiennej t2. Różnica tych czasów jest właśnie Twoim czesem reakcji – Twoim refleksem.

Pomysły

Program należy rozbudować – o dwa przyciski, dwa LEDy. To wzbogaci zabawę, bo losowo zapali się albo jeden LED, albo drugi. Warto wybrać dwa kolory LED-ów i dwa kolory przycisków. To będzie dodatkowe utrudnienie dla użytkownika – ma on bowiem wcisnąć odpowiedni przycisk (np. LED żółty – to i przycisk żółty, a nie niebieski. Niebieski to dyskwalifikacja! I na odwrót).

Inna modyfikacja polega na wychwyceniu falstartu – zapobiegnięciu sytuacji, że użytkownik bezmyślnie „klika” przyciskiem w nadziei, że gdy LED się zaświeci – on właśnie wcisnął przycisk i otrzymał bardzo krótki czas reakcji. Trzeba tak zrobić, aby wciśnięcie przycisku PRZED zaświeceniem kończyło zabawę. Podpowiedź: zamiast funkcji delay() w linii #13 trzeba sprytnie wykorzystać pętlę while…

(c) KG 2018

Processing — wizualizacja danych z Arduino

processing.org

Świetny program do nauki programowania – gdyż w bardzo prosty sposób wyświetla grafikę. IDE jest proste, intuicyjne, bardzo podobne do Arduino IDE (w rzeczy samej – to też produkt MIT). Wiele osób, w tym także my, będziemy używać processing do graficznego przedstawienia tego, co „mówi” do nas Arduino (czyli Serial.print z Arduino trafi do processinga, który wyświetli stosowną grafikę). Warto wspomnieć o kilka zalet tego softwaru:

  • wieloplatfromowy (Linux, Mac, Windows)
  • darmowy (w tym także do celów komercyjnych)
  • wyświetla grafikę 2D, ale także 3D
  • obsługuje OpenGL
  • ogromna liczba dodatkowych bibliotek, rozszerzających możliwości
  • dużo manuali online (a także książek).

Podstawy

Język programowania to C++ (Java, JavaScript, Ruby, Python) więc nie powinniśmy się czuć się nieswojo. Przyjrzyjmy się szkieletowi typowego programu („skeczu”)

void setup(){
  size(800,600);
  background(0);
}

void draw(){
  ellipse(10,10, 60, 100);
  fill(12, 23, 34);
  rectangle(50,50, 100, 200);
}

Widzimy dwie funkcje – setup() oraz draw() – podobnie, jak programując w Arduino IDE. To podobieństwo nie jest przypadkowe, bo jak już wspomniałem projekt powstał przez ludzi z MIT. Dlatego też znaczenie funkcji setup() jest takie samo jak w Arduino – funkcja wykona się tylko jeden raz, na początku programu. Kolejna funkcja – draw() – będzie się wykonywana „w kółko”, czyli po zakończeniu wykona się ponownie – czyli tak, jak funkcja loop() w Arduino IDE (co prawda w przypadku processinga można zmienić takie zachowanie, ale nie wnikajmy w takie szczegóły).

Po zapoznaniu się z podstawowymi prymitywami graficznymi processinga (prostokąt, linia, elipsa) przeszliśmy do części głównej dzisiejszych zajęć – połączenia Arduino z grafiką.

Wysyłamy liczby z Arduino

W przyszłości  będziemy wyświetlać odczyty z podłączonych czujek (np. czujki pola magnetycznego SS49e, o czym było na poprzednich zajęciach), ale teraz upraszczamy maksymalnie i wykorzystujemy Arduino do nadawania pseudolosowych liczb całkowitych. Uruchamiamy Arduino IDE i wpisujemy poniższy skech

void setup(){
  Serial.begin(9600);
}

void draw(){
  Serial.println(random(1024));
  delay(200);
}

Odczyt danych z portu COM w processingu

import processing.serial.*;
Serial moj_port;

void setup(){
 println("lista dostepnych portow COMM: ");
 println(Serial.list());
 println("probuje port: ", Serial.list()[0]);
 moj_port = new Serial(this, Serial.list()[0], 9600);
}

int dane;
void draw(){
  if (moj_port.available() > 0){
    dane = moj_port.read();
    println("wczytalem: ", dane);
  }//if
}

Ważne w powyższym programie jest wybórk konkretnego portu szeregowego, czyli linijka 

moj_port = new Serial(this, Serial.list()[0], 9600);

gdzie ja wybrałem pierwszy element (indeks zerowy tablic, czyli [0]) z listy dostępnych portów (Serial.list()) – ale może się zdarzyć, że u Was nie będzie to pierwszy, a jakiś inny element – dlatego wypisałem na ekranie całą listę i port, z którym próbuję się połączyć. Proszę przyjrzeć się tym komunikatom, jeśli coś nie idzie tak jak powinno.

Oczywiście programik nic nie wyświetla graficznego, tylko odczytuje dane (i to błędnie! o czym wspomniałem na końcu zajęć – problem ten rozwiążemy później) i wypisuje je na ekranie. Przechodzimy do grafiki (prostej).

Kreślimy wykres

Lekko modyfikujemy programik. 

import processing.serial.*;
Serial moj_port;

int xPos;

void setup(){
 println("lista dostepnych portow COMM: ", Serial.list());
 println("probuje port: ", Serial.list()[0]);
 moj_port = new Serial(this, Serial.list()[0], 9600);
}

int dane;
void draw(){
  if (moj_port.available() > 0){
    dane = moj_port.read();
    println("wczytalem: ", dane);

    rect(xPos, height/2, 10, dane);
    xPos+=10;
    if (xPos >= width) {
      xPos = 0;
      background(0);
    }
  }//available
}

Bardzo prymitywna grafika, ale niech to wystarczy na dzisiaj. 

procc0

Pozostaje kilka kwestii do rozwiązania:

  • poprawne wczytywanie liczb (z Arduino wysyłamy 0..1023, a processing „widzi” tylko jakieś małe liczby, <30?)
  • ładniejsza grafika.

Zapraszam na kolejne zajęcia!