Witam, W tym DIY Konstrukcje chciałbym zaprezentować urządzenie do gry w Monski Pong. Cały projekt składa się z dwóch programów, modułu Arduino Uno i płytki PCB. Do zbudowania tego urządzenia zachęcam wszystkich początkujących jako dobry wstęp do podstaw komunikacji Arduino z komputerem z pomocą portu szeregowego. https://obrazki.elektroda.pl/1057744800_1584404003_thumb.jpg Cały projekt miał być zbudowany na konkurs Forbot-a, ale niestety czas nie pozwolił mi to skończyć przed 15.03 więc postanowiłem owoc prac przedstawić forumowiczom. Płytka PCB składa się głównie z gniazd GoldPin żeńskich i męskich i paru rezystorów. Złącza AC (analogowe) i DC (cyfrowe) są ustawione tak na płytce, żeby wchodziły bezpośrednio w moduł Arduino (wraz z PWR - zasilaniem). Reszta gniazd jest ułożona szeregowo, która jest wykorzystana do podłączenia przycisku i sterowania. GoldPin P1_SER i P2_RST są przeznaczone do przycisków serw i reset gry, gdzie sterują wejściem DC. Natomiast C2_CZ i C1_N (CZ - czerwony, N - niebieski) są wejściami dla dwóch potencjometrów, którymi będziemy sterować rakietkami w naszej grze - doprowadzone do wejść AC. https://obrazki.elektroda.pl/3512817300_1584403208_thumb.jpg https://obrazki.elektroda.pl/1668387100_1584403242_thumb.jpg PCB wyszło jak wyszło, postanowiłem zrobić wszystko na laminacie, ponieważ na płytce stykowej występowały przerywania, styki nie zawsze zwierały, miałem sporo kłopotu żeby dociskać elementy i kręcić potencjometrem naraz :) . https://obrazki.elektroda.pl/2345937100_1584403910_thumb.jpg https://obrazki.elektroda.pl/5639260000_1584403910_thumb.jpg https://obrazki.elektroda.pl/6435732300_1584403910_thumb.jpg Żeby umieścić PCB w Arduino musiałem już przytwierdzić mój układ do podstaw obudowy. Wszystko jest przedstawione na poniższych zdjęciach. https://obrazki.elektroda.pl/1027038300_1584404630_thumb.jpg https://obrazki.elektroda.pl/8449334300_1584404708_thumb.jpg https://obrazki.elektroda.pl/4648554100_1584404707_thumb.jpg https://obrazki.elektroda.pl/1777425600_1584404706_thumb.jpg https://obrazki.elektroda.pl/1376130700_1584404710_thumb.jpg https://obrazki.elektroda.pl/8401205400_1584404711_thumb.jpg https://obrazki.elektroda.pl/9198393200_1584404711_thumb.jpg https://obrazki.elektroda.pl/9803585100_1584404715_thumb.jpg https://obrazki.elektroda.pl/4151243500_1584404714_thumb.jpg https://obrazki.elektroda.pl/3212134800_1584404715_thumb.jpg Często słyszę od młodszych kolegów lub mniej doświadczonych: skąd brać materiały na obudowy i akcesoria? Więc tak: podstawki ze starego routera, ścianki i podstawa (ta biała plexi pleksi) ze starych lamp rastrowych, dystanse z dekoderów starych lub z elektronicznego sklepu , śrubki z demontażu wszystkiego :) . DIY to MacGyver, polecam się rozejrzeć. Na ten moment to już wszystko z budowy urządzenia, zostają jeszcze zdjęcia z przykrywy i działania - to na końcu. Teraz trudniejsza rzecz, program. Na pewno wszyscy którzy używają Arduino na pewno używają też Arduino IDE. Program do Arduino jest przedstawiony poniżej: const int leftSensor = 0; const int rightSensor = 1; const int resetButton = 2; const int serveButton = 3; int leftValue = 0; int rightValue = 0; int reset = 0; int serve = 0; void setup() { Serial.begin(9600); pinMode(resetButton, INPUT); pinMode(serveButton, INPUT); } void loop() { leftValue = analogRead(leftSensor); rightValue = analogRead(rightSensor); reset = digitalRead(resetButton); serve = digitalRead(serveButton); Serial.print(leftValue); Serial.print(","); Serial.print(rightValue); Serial.print(","); Serial.print(reset); Serial.print(","); Serial.print(serve); Serial.print("\n"); } Nie robię opisu jako komentarz w kodzie bo zrobię to tutaj :) Program nie jest wymagający dla naszego Uno, tylko 37 linijek. Pierwsze 4 zmienne deklarują stałe dla wszystkich czujników, przypisując im indeks na fizycznych pinach. Kolejne 4 zmienne przechowują wartości czujników. W setup() deklarujemy cyfrowe piny resetButton i serveButton jako wejscia (i tez jako wejścia cyfrowe, bo nigdzie tego nie zrobiliśmy przecież) - żeby odbierać sygnały z przycisku. W pętli loop() przypisujemy zmienne wartości czujników z konkretnych wejść. analogRead() służy do odczytywania analogowych wartości z wejścia leftSensor i rightSensor, natomiast digitalRead() odczytuje cyfrowe sygnały. Kolejne linijki kodu wypisują w komunikacji szeregowej wartości czujników. Jak na razie - to wszystko, nie ma co opisywać, w tym kodzie dla wsadu Arduino Uno to wszystko. Czas na IDE Processing. Precessing to multimedialne środowisko programowania (coś jak IDE Arduino). Jest ono oparte na języku Java stąd można używać w nim klas i metod z Javy , ale zostało tak stworzone żeby ,,działać" bez zagłębiania się w szczegóły programowania. Co najważniejsze jest ono darmowe i działa na wszystkich urządzeniach. Używa składni języka C, zatem jest bardzo bardzo podobny do języka C++ z naszego IDE Arduino. Teraz pierwszy kod (nie finałowy) potrzebny do pewnych testów. Można oczywiście wszystko sprawdzić przed ale zalecam zrobić to z programem (patrzcie, co trzeba sprawdzić w programie i inne). import processing.serial.*; Serial myPort; String resultString; void setup() { size(480, 130); println(Serial.list()); //serial.list ma niby wykazać listę wirtualnych portów COM, jednak, trzeba wpisać numer swojego portu w miejsce zaraz pod spodem //Nie pomijam tej linijki z Serial.list ponieważ nie wiem jaki to bedzie miało skutek na działanie programu String portName = Serial.list(); //otwieramy ten port... myPort = new Serial(this, portName, 9600); //wczytujemy bajty do bufora, dopóki nie otrzymamy znaku wysuwu wiersza (ASCII 10): myPort.bufferUntil('\n'); } void draw() { //ustawianie koloru tła i wypełnienia okna background(#044f6f); fill(#ffffff); //wysiwtl ciąg tekstowy w oknie if (resultString != null) { text(resultString, 10, height/2); } } //metoda serialEvent jest uruchamiana automatycznie przez szkic Processing zawsze wtedy, gdy bufor napotka bajt o wartości ustawionej // w metodzie bufferUntil() w setup() void serialEvent(Serial myPort) { String inputString = myPort.readStringUntil('\n'); //wytnij znak powrotu oraz wysuwu wiersza z ciągu wyjściowego inputString = trim(inputString); //wyczyśc zmienną resultString resultString = ""; //podziel ciąg wejściowy na na podciągi, używając przecinka jako separatora i skonwertuj każdą sekcję na liczby całkowite int sensors = int(split(inputString, ',')); //dodaj wartośc do ciągu znaków for (int sensorNum = 0; sensorNum < sensors.length; sensorNum++) { resultString += "Sensors." + sensorNum + " : "; resultString += sensors + " \t"; } //wydrukuj wynuki na konsoli println(resultString); } Niby jakieś komentarze są, jednak podkreślę parę rzeczy. import processing.serial.* - biblioteka dla Processing do obsługi wirtualnych portów w komputerze. W setup() mamy linijkę z size(480, 130) - to wielkość okna jaka ma się pojawić przy uruchomieniu naszego programu. Cała procedura draw() dzieje się w tym oknie, myślę że nic nie muszę z niej tłumaczyć, text(resultString, 10, height/2) - prosta składnia ==> https://processing.org/reference/text_.html. Wracamy do setup() bo tu będą schody. W nagłówku mamy coś takiego: Serial myPort - jest to deklaracja zmiennej do której zostanie zapisany nasz port COM (ten wirtualny). Wracając do setup() :) ponownie mamy println(Serial.list()) - drukuje nam to (w tle, na dole widać porty w konsoli jakie znalazł) porty COM jakie są dostępne, trzeba to zrobić aby program załadował je do ,,siebie". Później String portName = Serial.list()X - czemu X jest pogrubiony, ano dlatego, że w jego miejsce trzeba wstawić numer portu pod jakim występuje nasze Arduino, zmienna String portName pobiera ,,adres" wirtualnego portu. Teraz wspomniane wcześniej ,,schody". U mnie w komputerze wirtualny port od Arduino wynosił COM20, ale jak widać w programie jest wpisane , dlatego, że nie wiem :) , program -po wpisaniu 20 - dawał błąd o podaniu wartości z poza tablicy portów szeregowych. Na każdym komputerze sprawdziłem i działa tylko pod 1, na jednym pod 0 (sprawdzałem na 7 kompach), nie znalazłem zależności. Jak ktoś wie od czego to zależy to chętnie go tu zacytuje. myPort = new Serial(this, portName, 9600) wszystko jest opisane tutaj, this to this (według zaleceń strony producenta), portName jako numer portu szeregowego, 9600 prędkość transmisji danych. Następnie myPort.bufferUntil('\n') - jest opisany w programie, czeka na znak kończący wiersz. Wychodzimy już z setup(), draw() jest już opisany, zatem ostatnia rzecz: serialEvent(Serial myPort) - funkcja wykorzystująca dane z myPort (myPort = new Serial(this, portName, 9600) ) otwiera operację dla portu szeregowego. Myślę, że lepiej nie opiszę tego jak strona Processing: https://processing.org/reference/trim_.html A dla zawiłego polecenia split też mam wyjaśnienie: https://processing.org/reference/split_.html Syntax split(value, delim) Parameters value - String: the String to be split delim - char: the character or String used to separate the data Tworzymy tablicę int sensors z której dane wycinamy po przecinku (możesz wrócić do programu z IDE Arduino i zobaczyć czym przez serial port rozdzielałem dane wyjściowe). split() dzieli nam dane w tablicy, dzięki czemu w pętli for() - sensors.length nie wypisze nam więcej danych niż byśmy chcieli. Zatem pętla for() powtarza się tyle razy ile danych jest na wyjściu (pomiędzy ,, , " do \n. Na końcu println() drukuje nam wyniki. Mamy za sobą 51 linijek kodu testowego, finałowy kod ma około 200 :) Jeśli z grubsza wszystko zostało omówione to możemy uruchomić nasz program w IDE Processing. Ważne jest to, ponieważ za pomocą tego programu możemy sprawdzić czy następuje komunikacja z programem oraz jakie wartości przyjmują czujniki. https://filmy.elektroda.pl/12_1584413540.mp4 Jak widać, mój układ ma maks wartości na potencjometrze od około 550 - 1023 (kwestia podłączenia), te wartości będą dla nas ważne do podawania ich w stałych. Też dobra praktyka (ten program) by sprawdzić stany wejść cyfrowych, odporność układu na lekkie trącenia, itp. Kiedy wszystko działa nam w tym programie (próba mikrofonu), to możemy przystąpić do finałowego programu (główny Monk). import processing.serial.*; Serial myPort; String resultString; float leftPaddle, rightPaddle; //zmienne dla czujnika zmiennej rezystancji int resetButton, serveButton; //zmienne dla wartości przycisku int leftPaddleX, rightPaddleX; //położenie poziome rakietki int paddleHeight = 50; //wymiary pionome rakietki int paddleWidth = 10; //wymiary poziome rakietki float leftMinimum = 1023; //minimalna wartośc lewego czujnika zmiennej rezystancji float rightMinimum = 550; // minimalna wartość prawego czujnika zmiennej rezystancji float leftMaximum = 530; //maksymalna wartość lewego czujnika zmiennej rezystancji float rightMaximum = 1023; //maksymalna wartość prawego czujnika zmiennej rezystancji int ballSize = 10; //rozmiar piłeczki int xDirection = 1; //kierunek poziomy piłeczki - w lewo -1, w prawo 1 int yDirection = 1; //kierunek pionowy piłeczki - w górę -1, w dół 1 int xPos, yPos; //poziome i pionowe położenie piłki boolean ballInMotion = false; //czy piłeczka powinna się ruszać int leftScore = 0; int rightScore = 0; int fontSize = 36; void setup() { size(640, 480); println(Serial.list()); //serial.list ma niby wykazać listę wirtualnych portów COM, jednak, trzeba wpisać numer swojego portu w miejsce zaraz pod spodem //Nie pomijam tej linijki z Serial.list ponieważ nie wiem jaki to bedzie miało skutek na działanie programu String portName = Serial.list(); //otwieramy ten port... myPort = new Serial(this, portName, 9600); //wczytujemy bajty do bufora, dopóki nie otrzymamy znaku wysuwu wiersza (ASCII 10): myPort.bufferUntil('\n'); //inicjowanie wartości czujnika leftPaddle = height/2; rightPaddle = height/2; resetButton = 0; serveButton = 0; //inicjowanie pozycji rakietki leftPaddleX = 50; rightPaddleX = width - 50; //ustaw brak bramek dla rysownaych kształtów noStroke(); //zainicjuj pozycję piłki na środku ekranu xPos = width/2; yPos = height/2; //utwórz czcionkę z trzeciej trzcionki dostępnej w systemie: PFont myFont = createFont(PFont.list(), fontSize); textFont(myFont); } void draw() { //ustawianie koloru tła i wypełnienia okna background(#044f6f); fill(#ffffff); //narysuj lewą rakietkę: rect(leftPaddleX, leftPaddle, paddleWidth, paddleHeight); //narysuj prawą rakietkę: rect(rightPaddleX, rightPaddle, paddleWidth, paddleHeight); //jeżeli wciśnięty jest przycik serve - wpraw piłęczkę w ruchu if (serveButton == 1) { ballInMotion = true; } //oblicz pozycję piłki i narysuj ją: if (ballInMotion == true) { animateBall(); } //jeśli jest wciśnęty przycisk reset to, wyzeruj wynik gry i rozpocznij ruch piłeczki: if (resetButton == 1) { leftScore = 0; rightScore = 0; ballInMotion = true; } //wyswitl wynik gry text(leftScore, fontSize, fontSize); text(rightScore, width-fontSize, fontSize); } //metoda serialEvent jest uruchamiana automatycznie przez szkic Processing zawsze wtedy, gdy bufor napotka bajt o wartości ustawionej // w metodzie bufferUntil() w setup() void serialEvent(Serial myPort) { String inputString = myPort.readStringUntil('\n'); //wytnij znak powrotu oraz wysuwu wiersza z ciągu wyjściowego inputString = trim(inputString); //wyczyśc zmienną resultString resultString = ""; //podziel ciąg wejściowy na na podciągi, używając przecinka jako separatora i skonwertuj każdą sekcję na liczby całkowite int sensors = int(split(inputString, ',')); //Jeśli odebrałes wszystkie odczyty czujnika, użyj ich: if (sensors.length == 4) { //przeskaluj odczyty czujnika zmiennej rezystancji do zakresu ruchu rakietki leftPaddle = map(sensors, leftMinimum, leftMaximum, 0, height); rightPaddle = map(sensors, rightMinimum, rightMaximum, 0, height); //przypisz wartości przełączników do zmiennych pzycisków resetButton = sensors; serveButton = sensors; //dodaj wartości do ciągu wynikowego resultString += "lewy: "+ "\tprawyt: " + rightPaddle; resultString += "\treset: "+ resetButton + "\tserv: " + serveButton; } //dodaj wartośc do ciągu znaków //wydrukuj wynuki na konsoli println(resultString); } void animateBall() { //jezeli piłka porusza się w lewo: if (xDirection < 0) { //jesli piłka jest po lewej stronie lewej rakietki: if ((xPos <= leftPaddleX)) { //jesli piłka jest między górną a dolną krawędzią lewej rakietki if ((leftPaddle - (paddleHeight/2) <= yPos) && (yPos <= leftPaddle + (paddleHeight /2))) { //odwróć kierunek poziomy xDirection =-xDirection; } } } //jeśli piłka porusza się w prawo else{ //jeśli piłka jest po prawej stronie prawej rakietki: if ((xPos >= (rightPaddleX + ballSize/2))) { //jeśli piłka jest między górną a dolną krawędzią prawej rakietki: if ((rightPaddle - (paddleHeight/2) <=yPos) && (yPos <= rightPaddle + (paddleHeight /2))) { //odwróć kierunke poziomy: xDirection =-xDirection; } } } //jeśli piłka wyszła poza ekran po lewej stronie okna: if (xPos < 0) { rightScore++; resetBall(); } //jeśli piłka wyszła poza ekran okna po prawej if (xPos > width) { leftScore++; resetBall(); } //powstrzymaj piłkę przed wyjściem poza górną lub dolną krawędź ekranu if ((yPos - ballSize/2 <= 0) || (yPos +ballSize/2 >=height)) { //odwróć kierunek piłki yDirection =-yDirection; } //aktualizacja pozycji piłki: xPos = xPos + xDirection; yPos = yPos + yDirection; //narysuj piłkę rect(xPos, yPos, ballSize, ballSize); } void resetBall() { //umieśc piłkę z powrotem na środku xPos = width/2; yPos = height/2; } Skupię się głównie na trudnościach jakie napotkałem i o jakich myślę, że wymagają wyjaśnienia. Od początku. Doszło 17 zmiennych (linijek), w zmiennych float leftMinimum, rightMinimum, leftMaximum i rightMaximum musimy podać swoje wartości odczytane z potencjometrów (lub innych elementów elektronicznych którymi będziesz sterować rakietkami) z tego właśnie testowego programu. Powiem szczerze, że to chyba wszystko, co było związane z trudnością obsługi komunikacji i ogólnego działania aplikacji. Wciąż trzeba podać zamiast X numer 1 dla String portName = Serial.list()X i to tyle, tak naprawdę doszło kodu do tego programu testowego, co jest wynikiem tego, finałowego. Zachowałem opisy podczas pisania więc można się samemu zapoznać, a dodatkowa ilość kodu nie jest czymś trudnym już, tylko rysowanie i wprawianie w ruch rakietek i piłeczki. Widzimy tylko zmiany w funkcji serialEvent() gdzie rakietki są zmapowane co do wartości podanych przez nas, a idąc z wcześniejszymi przykładami, można łatwo sprawdzić co oznacza kolejno zmienna/wartość w nawiasach funkcji map() https://processing.org/reference/map_.html Najważniejsze to podać zmienne ze swojego zakresu. A oto efekt: https://filmy.elektroda.pl/7_1584415197.mp4 Na końcu specjalnie nie odbiłem, żeby można było sprawdzić jak to działa w przypadku punktacji. Też można zauważyć (drugie odbicie) że czasem paletka odbije ale po jakimś czasie. Myślę (bo paletka lekko drży) że jest to wina drgania styków przez gniazda goldpin. Nie dodałem kodu który by to uspokoił ponieważ, gra była by za łatwa, tak to trzeba cały czas się ruszać żeby paletka była stabilna i gotowa na przyjęcie piłeczki. https://obrazki.elektroda.pl/8638882600_1584415435_thumb.jpg https://obrazki.elektroda.pl/7454550900_1584415435_thumb.jpg https://obrazki.elektroda.pl/3248524600_1584415435_thumb.jpg Pozdrawiam i miłej zabawy. PS Załącznik zawiera pliki z Eagle (schemat/płytka), jest poprawiona pozycja pinów PWR (informuje żeby nie było zdziwienia) pierwotnie miałem poślizg o jeden pin w przód.