RoboSchneider 2.0

Mads Kildegaard Kristjansen & Lucas Brynk Jensen

I denne tutorial vil vi gennemgå hvordan man bygger en robot, som ved hjælp af diverse sensorer kan køre efter en streg på gulvet indtil stregen ophører, hvorefter robotten vil køre efter lydbølger for at undgå mulig kollision. Robotten i denne tutorial er navngivet RoboSchneider (efter skuespilleren) og den er udarbejdet af Lucas Brynk og Mads K. Kristjansen. Det færdige produkt, samt forventninger til produktets opførelse kan ses på nedenstående videoer.

Video af testkørsel i Unity
Video af test i TinkerCAD

Vi starter med at åbne TinkerCAD. For at kunne forstå hvad der sker med den færdige robot i Unity, er vi nødt til at opbygge RoboSchneider i TinkerCAD først. De nødvendige elementer kan ses på nedenstående billede samt en liste med navne på elementerne.

Komponentlist:

  • Breadboard x2
  • Arduino Uno
  • 9v Batteri
  • DC Motor x2 [H16930]
  • Micro Servo [SG90]
  • Ultrasonic Distance Sensor [HC-SR04]
  • H-Bridge Motor Driver [L293D]
  • 5C Regulator [LM7805]
  • 100 ohm Resistor x2
  • 10kOhm Resistor x2
  • LED-Resistor [2542 Standard White LED] x2
  • LDR-Resistor [f.eks NORPS-12]
  • Slideswitch

Hvordan selve elementerne bliver sat i forhold til hinanden er stort set lige meget i dette projekt, da TinkerCAD modellen ikke skal bruges andre steder. For forståelsens skyld opfordres der til at opsætte elementerne som skulle man bygge en “bil”.

Første skridt er at sætte 9V batteriet til breadboardet så vi kan få strøm rundt i kredsløbet. Lad plus køre igennem en sliderswitch for at kunne slukke og tænde for RoboSchneider.

Det første breadboard bliver vores H-bridge board. Sæt L293D H-Bridge (H-bro) fast i midten af breadboarded og forbind de to motorer på hver side af H-broen. Grunden til at anvende en H-bro er for at styre strømmens polaritet gennem motorerne. Dette gør at vi kan bestemme hvilken vej hjulene skal dreje. For den ene motor vil vi lade Input 1 og 2 fra H-broen gå til pin 10 og 11 på arduinoen. Man kan bruge alle digital pins til dette, men da Arduino Uno er begrænset i antallet af Digitale pins (især PWM pins), og vores servo kommer til at optage pin 9 eller 10, så er vi nødt til at bruge hvad der er tilbage. Ligeledes for den anden motor vil køre Input 3 og 4 fra H-broen hen til pin 5 og 6 på arduinoen.

På første breadboard er der lidt plads tilbage, nede i den anden ende, til at sætte en 5V regulator [LM7805]. Fra 5V Regulatorens output trækker vi en plus og minus videre til næste breadboard. Fra andet breadboard starter vi med at trække en plus og en minus til hhv Arduinoens 5V pin og GND pin for at kunne gøre Arduino Uno uafhængig af USB-forbindelsen til en PC.

Andet breadboard bliver LDR-LED boarded. Her sætter vi 2 LDR-resistorer samt 2 modstande. Modstandene skal være på 10 kOhm grundet følgende beregninger:

Da vi kender Ohms lov kan vi stille den om og finde den ønskede modstandsværdi ved den benyttede spænding. Hertil skal det siges at vi antager fra tidligere projekt at LDR-modstandene ved maksbelysning er 20-230kOhm, samt ved totalt mørke, vil være 2MOhm.

Vout = Vin * (R2/R1+R2)

Ved at sætte den valgte modstand ind, samt indsætte de kendte værdier, kommer formlen til at se sådanne ud ved 20kOhm, 230kOhm og 2MOhm:

5V * (20000/20000+10000) = 3,33V
5V * (230000/230000+10000) = 4,79V
5V * (2000000/2000000+10000) = 4,98V

Lad LDR-resistorerne køre til hhv. pin A2 og A3. (Du kan i princippet benytte enhver A pin i dette projekt, det er dog kun indgangene markeret med ‘A’ der kan bruges her). Grunden til at vi benytter pins markeret med ‘A’ for LDR-resistorerne, er fordi vi her ikke får et digitalt signal, men et analogt signal, det vil sige at det ikke bliver bearbejdet på samme måde som Digitale signaler. Hertil skal det også siges at det signal vi får fra LDR-resistorerne bliver konverteret i Arduinoen fra Volt til en målbar enhed (altså værdien fra 0-1023).

Sæt herefter 2 LED-resistorer direkte til 5V med en modstand imellem. Størrelsen af modstanden vælges ud fra den givende LED’s data. LED-resistor Data kan findes i et Datablad. Da dette projekt er virtuelt har vi ikke ægte LED-modstande at undersøge, derfor må vi gå ud fra at vi har at gøre med en standard hvid LED 5mm (3542serialnumber). Hvis man undersøger en sådanne LED-modstand i et datablad vil man finde ud af at den maximalt kan håndtere 25mA (dog kun 20mA i TinkerCAD) før det begynder at gå ud over levetiden på LED-modstanden, og da den bliver tilført 5V (minus spændingsfaldet på 3,1V) kan vi regne os frem til, med hjælp fra Ohms lov, at en 100 Ohms modstand vil være at fortrække i TinkerCAD.

U=R*I
R=U/I 
R=(5-3,1)/0,02 = 95 Ohm 

Vi runder op således vi anvender en 100 Ohms modstand. 

For en LED udenfor TinkerCAD, som max håndterer 25mA, sætter man 0.025 ind i formlen og får dermed en beregnet modstand på 

R=(5-3,1)/0,025 = 76 ohm (rundet op til 80 ohm) 

Beregningerne er lavet ud fra data fundet på følgende hjemmeside: (https://www.kitronik.co.uk/blog/led-resistor-value-calculator/

Nu skal Micro Servo’en sættes til. Her sætter vi Power samt GND til LDR-LED Breadboardet. Micro Servo’ens Signal skal køre til pin 9. Den skal altid sættes til enten pin 9 eller 10, da det er de eneste to pins der er registreret i Arduinosprogets Servo.h bibliotek som er integreret i TinkerCAD. På en fysisk prototype undenfor TinkerCAD kan man bruge enhver PWN pin til Servoen. Måden vi fandt ud af dette på, var ved at den venstre motor stoppede med at fungere korrekt, samt højre motor kørte med fuld hastighed når andre end pin 9 eller 10 blev benyttet. Servoen kørte heller ikke korrekt. Efter lidt debugging (test af systemet slavisk) fandt vi frem til at det måtte være et problem som er opstået efter servoen blev implenteret. Hertil måtte vi ind og se i dokumentationen i Servo.h biblioteket, hvor vi fandt ud af at pin 9 og 10 er de eneste som er supporteret og integreret i Servo.h biblioteket til TinkerCAD.  

Ultrasonic Distance Sensor’s TRIG, og ECHO skal køre til henholdsvis pin 3 og pin 12 (TRIG skal altid forbindes til en PWM digital pin, og ECHO skal altid forbindes til en normal digital pin). Ultrasonic Distance Sensor’s Power og GND skal også kører hen til LDR-LED Breadboardet. Hvis systemet er opsat korrekt burde det se nogenlunde ud som på billedet nedenfor.

Et PWM-signal er et signal som ikke kun kan gå fra tændt til slukket (0-1), men har et spektrum fra 0 og op til en given værdi. Et PWM-signal fungerer lidt som en kurve. Dette kan anvendes til eksempelvis en motor, for at få en blød opstart og/eller en blød nedbremsning. Dette kan også bruges i servoen, hvor vi ikke vil have servoen til at køre hele vejen rundt, men gerne vil have en til at stille sig i et bestemt punkt hvortil den bliver stående. Dette bliver også brugt på Ultrasonic Distance Sensor hvor vi ikke kun kan nøjes med et signal der går fra 0-1, men har brug for at kende den nøjagtige værdi der bliver givet fra komponentet, altså Distance sensoren.

TinkerCAD Code 

I vores Arduino kode, som kan ses forneden, vil vi først og fremmest declare(erklære) vores variabler vi gør brug af igennem programmet. En vigtig ting at have i mente er, at når man vil gøre brug af en servo, så skal man huske at anvende komponentens bibliotek via #include <Servo.h>. Uden dette vil der opstå fejl i programmet. Foruden vores variabler anvender vi også to bools, der agerer som “triggers” for at programmet ikke springer over nogle af vores funktioner. Inde i vores setup() angiver vi vores pins og servo så vi kan bruge dem i programmet, og sætter vores motorer i en stillestående tilstand. 

#include <Servo.h>
//declaring the names for the pins
//Motor
const int RightPlus = 11;
const int RightMinus = 10;
const int LeftPlus = 6;
const int LeftMinus = 5;

//Distance Sensor
const int TRIGPin = 3;
const int ECHOPin = 12;


//Declare the needed variables
int left = A5;
int right = A4;
int sensorValue1;
int sensorValue2;
int minDiff = 50; //Maximum allowed difference between left and right sensor values

bool onLine = true;
bool firstDrive = false;

int dis = 100;
long tid;
int distance;
int servoLeft = 0;
int servoFront = 0;
int servoRight = 0; 

Servo theServo;

//Setup what needs to be stated before the robot begins its Loop
void setup()
{
  pinMode(RightPlus, OUTPUT);//right plus
  pinMode(RightMinus, OUTPUT);//right minus
  pinMode(LeftPlus, OUTPUT);//left plus
  pinMode(LeftMinus, OUTPUT);//left minus
  
  pinMode(TRIGPin, OUTPUT); // Sets the trigPin as an OUTPUT
  pinMode(ECHOPin, INPUT); // Sets the echoPin as an INPUT
  
  theServo.attach(9); // The Servo
  
  //Make sure motors are off
  digitalWrite (RightPlus, LOW);
  digitalWrite (RightMinus, LOW);
  digitalWrite (LeftPlus, LOW);
  digitalWrite (LeftMinus, LOW);
  
  Serial.begin(9600);
  
}

I vores loop(), hvor hele programmet egentlig kører, starter vi med at aflæse værdierne fra vores to LDR-modstande. Disse værdier bruger vi til at vurdere om RoboSchneider følger linjen, eller om vi skal rette kursen. Her har vi også vores første bool (onLine), som sørger for at denne del af programmet kun kører imens RoboSchneider faktisk kører på linjen.

void loop()
{
  //Assigning values to var's
  sensorValue1 = analogRead(left);
  sensorValue2 = analogRead(right);
  
  int diff = abs(sensorValue1 - sensorValue2);
  
  //"If" robot is following the ground-line; Do this:
  if (onLine == true)
  { ...

Værdierne fra LDR-modstandene angiver det lys der bliver reflekteret fra ”gulvet”. LDR-modstandene har en værdi mellem 0-1023. Denne værdi er målt som mængden af strøm der passerer igennem LDR-modstanden, i forhold til hvor stor en modstand der udvikles i selve LDR-modstanden. Det vil med andre ord sige, at det hvide gulv reflekterer meget lys, og opfanges som en høj værdi, og den sorte streg reflekterer lidt lys, der opfanges som en lav værdi.

Vi bruger værdierne fra vores LDR-modstande til at bestemme hvilke motor pins der skal tilføres strøm. Dette gør vi ved at angive den pin som er tildelt motoren og indsætter en værdi mellem 0-255, eksempel: analogWrite(pin, speed). Da motoren er tilsluttet Arduinoen via en H-bro giver det 4 pins at holde styr på. Der er en analogWrite(pin,speed); for hver “pol” af de 2 motor; RightPlus, RightMinus, LeftPlus, LeftMinus. Så hvis den ene LDR-modstand måler for lavt, altså måler ind over den sorte streg, så retter vi op på kursen ved at ændre vores motor inputs. 

//"If" robot is following the ground-line; Do this:
  if (onLine == true)
  {
    // If the robot follows the line fine:
    if (diff <= minDiff && sensorValue1 >= 100 && sensorValue2 >= 100)
    {
      	//Driving Forward
        analogWrite(LeftPlus, 100);
        analogWrite(LeftMinus, 0);
        analogWrite(RightPlus, 100);
        analogWrite(RightMinus, 0);
        delay(1000);
      	firstDrive = true;
    }
    //If the robot is turning right, it turns left slightly
    else if (sensorValue1 < sensorValue2)
    {
      	//Turn Right for 1 sec at speed (50/255)
    	analogWrite(LeftPlus, 0);
        analogWrite(LeftMinus, 0);
        analogWrite(RightPlus, 50);
        analogWrite(RightMinus, 0);
      	delay(1000);
    }
        // Vice Versa
    else if (sensorValue2 < sensorValue1)
    {
      	//Turn Left for 1 sec at speed (50/255)
        analogWrite(LeftPlus, 50);
        analogWrite(LeftMinus, 0);
        analogWrite(RightPlus, 0);
        analogWrite(RightMinus, 0);
      	delay(1000);
    }
    ...
  

Vi er lidt påpasselige med vores konstruktions hastighed, og efter noget testning fandt vi frem til nogle passende værdier for vores motor inputs, som gav en stabil performance i vores opstilling. Eksempelvis, hvis RoboSchneider har for meget fart på, uden noget nedbremsning inden svinget, så kan den risikere at køre for hurtigt til at vores sensorer kan opfange og justere i programmet, således den ender med at køre over og væk fra stregen. Vi valgte derfor en lavere og mere passende hastighed for at undgå mulige fejl i adfærden. 

Når vi rammer den første målstreg, hvilket sker når begge LDR-modstande måler under den angivet minimumsværdi, ændrer vi vores onLine bool til at være false for at komme videre til den næste del af programmet. 

...

    //When the robot hits the ending of the ground line it stops and shifts over to "Off-Line" mode 
    else if (sensorValue1 <= 100 && sensorValue2 <= 100 && firstDrive == true)
    {
      	//Motor Stop for 1 sec
        analogWrite(LeftPlus, 0);
        analogWrite(LeftMinus, 0);
        analogWrite(RightPlus, 0);
        analogWrite(RightMinus, 0);
        onLine = false;
      	Serial.print("online is now false");
      	delay(1000);
    }

Den næste del af koden foregår kun efter vi har ramt den første målstreg. Her anvender vi vores Ultrasonic Distance Sensor (afstandsmåler) til at navigere den resterende del af vores opstillede bane. Vi starter med at aktivere pin 3, altså Distance Sensorens TRIG Pin, som sender en puls ud der bliver opfanget igen af Distance Sensoren. Den opfangede værdi er tiden det tager for denne puls at vende tilbage til vores sensor, målt i mikrosekunder. Denne værdi omregner vi til en målbar enhed. Lydens hastighed er 340 m/s, eller 0,034 cm/µs. Vi tager værdien fra afstandsmåleren, ganger den med 0,034, og deler med 2 da vi skal have afstanden til væggen, og ikke fra sensor til væg til sensor igen. Denne værdi gemmer vi i en variabel ”distance” som vi kan bruge til at vurdere afstanden fra sensoren på RoboSchneider og væggen forud.  

      //THIS IS OFF-LINE MODE
        //If the end of the groundline is reached
  if (onLine == false)
  {
    //Distance Sensor Start
    digitalWrite(TRIGPin, LOW);
  	delayMicroseconds(2);
  	digitalWrite(TRIGPin, HIGH);
  	delayMicroseconds(10);
  	digitalWrite(TRIGPin, LOW);
    //Read signal from TRIGPin and store as "tid"
  	tid = pulseIn(ECHOPin, HIGH);
    //Convert "tid" to "distance"
  	distance = tid * 0.034 / 2;
...

Hvis afstanden til væggen foran RoboSchneider ikke er under den angivet minimumsværdi, så kører den bare ligeud indtil den registrerer en genstand inden for minimumsafstanden. Når afstandsmåleren registrerer at vi er under minimumsgrænsen, så stopper vi RoboSchneiders fremgang for at undgå kollision. I praksis ville vi placere vores afstandsmåler ovenpå vores servo således vi kan rotere den og måle i forskellige retninger (dette kan ses på et billede af RoboSchneider længere nede i tutoriallen). 

//if the SonarSensor signal is bigger than the chosen minimum distance it goes forward 
  	if (distance >= dis)
    {
      	//Driving Forward
    	analogWrite(LeftPlus, 100);
        analogWrite(LeftMinus, 0);
        analogWrite(RightPlus, 100);
        analogWrite(RightMinus, 0);
    }
    //if signal is smaller:
    else if (distance <= dis)
    {
      	//Motor Stop
      	analogWrite(LeftPlus, 0);
        analogWrite(LeftMinus, 0);
        analogWrite(RightPlus, 0);
        analogWrite(RightMinus, 0);
  ...

Med servo.write(), i vores eksempel har vi navngivet vores for myServo, så kan man indsætte hvilken vinkel man vil indstille servoen til, eksempelvis 0, 90, eller 180 grader. Så hvis vi vil dreje servoen med 90 grader, så skriver vi i koden: myServo.write(90). Ud fra servoens placering og orientering kan disse værdier variere. I vores eksempel antager vi at 0 grader kigger servoen til venstre, 90 grader kigger den ligeud, og 180 grader til højre. 

//The check consists of 4 steps
        //Step 1 check distance to nearest wall in front of robot
        servoFront = distance;
      	//Turning the Servo to the Right
        theServo.write(180);
        delay(2000);
        //Step 2 check distance to nearest wall left of robot
        servoLeft = distance;
      	//Turning the Servo to the Left
        theServo.write(0);
        delay(4000);
        //Step 3 check distance to nearest wall right of robot
        servoRight = distance;
      	//Turning the Servo Back to facing for forward
        theServo.write(90);
        delay(2000);
...

Vi måler de tre retninger og gemmer dem i hver deres variabel. Disse variabler kan vi bruge til at sammenligne og vurdere hvor der er en væg, og hvor der er fri bane. Afhængig af hvilken retning der er fri, så justerer vi RoboSchneiders retning, hvorefter vi indstiller den til at køre fremad indtil der bliver registreret en ny væg/forhindring. 

//Turn to left wall
        if (servoLeft >= servoFront && servoLeft >= servoRight && servoRight != 0 || servoLeft == 0)
        {
          	//Turning Left (on the spot)
        	analogWrite(LeftPlus, 0);
            analogWrite(LeftMinus, 55);
            analogWrite(RightPlus, 55);
            analogWrite(RightMinus, 0);
            delay(1000);
          	//Stopping
            analogWrite(LeftPlus, 0);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 0);
            analogWrite(RightMinus, 0);
            delay(1000);
          	//Driving Forward Again
            analogWrite(LeftPlus, 130);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 130);
            analogWrite(RightMinus, 0);
        }
        //Turn to right wall
        if (servoRight >= servoFront && servoRight >= servoLeft && servoLeft != 0 || servoRight == 0)
        {
          	//Turning Right (on the spot)
        	analogWrite(LeftPlus, 55);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 0);
            analogWrite(RightMinus, 55);
            delay(1000);
          	//Stopping
            analogWrite(LeftPlus, 0);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 0);
            analogWrite(RightMinus, 0);
            delay(1000);
          	//Driving Forward Again
            analogWrite(LeftPlus, 130);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 130);
            analogWrite(RightMinus, 0);
		}
        //Continue forward
        if (servoFront >= servoLeft && servoFront >= servoRight)
        {
          	//Driving Forward
        	analogWrite(LeftPlus, 130);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 130);
            analogWrite(RightMinus, 0);
          	delay(1000);
        }

Med denne opsætning er RoboSchneider i stand til at køre igennem en bane som starter med en streg i gulvet, som naturligvis skal følges til ende.

Når enden af stregen er nået starter RoboSchneider på at køre efter Ultrasonic Distance Sensor (afstandsmåler) for at navigere igennem resten af banen, til målstregen bliver mødt. Hvis alt er opsat korrekt vil RoboSchneider opføre sig som vist på den første video oppe i toppen af bloggen her på siden.  

Complete TinkerCAD code

#include <Servo.h>
//declaring the names for the pins
//Motor
const int RightPlus = 11;
const int RightMinus = 10;
const int LeftPlus = 6;
const int LeftMinus = 5;

//Distance Sensor
const int TRIGPin = 3;
const int ECHOPin = 12;


//Declare the needed variables
int left = A5;
int right = A4;
int sensorValue1;
int sensorValue2;
int minDiff = 50; //Maximum allowed difference between left and right sensor values

bool onLine = true;
bool firstDrive = false;

int dis = 100;
long tid;
int distance;
int servoLeft = 0;
int servoFront = 0;
int servoRight = 0; 

Servo theServo;

//Setup what needs to be stated before the robot begins its Loop
void setup()
{
  pinMode(RightPlus, OUTPUT);//right plus
  pinMode(RightMinus, OUTPUT);//right minus
  pinMode(LeftPlus, OUTPUT);//left plus
  pinMode(LeftMinus, OUTPUT);//left minus
  
  pinMode(TRIGPin, OUTPUT); // Sets the trigPin as an OUTPUT
  pinMode(ECHOPin, INPUT); // Sets the echoPin as an INPUT
  
  theServo.attach(9); // The Servo
  
  //Make sure motors are off
  digitalWrite (RightPlus, LOW);
  digitalWrite (RightMinus, LOW);
  digitalWrite (LeftPlus, LOW);
  digitalWrite (LeftMinus, LOW);
  
  Serial.begin(9600);
  
}

//The Loop Begins

void loop()
{
  //Assigning values to var's
  sensorValue1 = analogRead(left);
  sensorValue2 = analogRead(right);
  
  int diff = abs(sensorValue1 - sensorValue2);
  
  //"If" robot is following the ground-line; Do this:
  if (onLine == true)
  {
    // If the robot follows the line fine:
    if (diff <= minDiff && sensorValue1 >= 100 && sensorValue2 >= 100)
    {
      	//Driving Forward
        analogWrite(LeftPlus, 100);
        analogWrite(LeftMinus, 0);
        analogWrite(RightPlus, 100);
        analogWrite(RightMinus, 0);
        delay(1000);
      	firstDrive = true;
    }
    //If the robot is turning right, it turns left slightly
    else if (sensorValue1 < sensorValue2)
    {
      	//Turn Right for 1 sec at speed (50/255)
    	analogWrite(LeftPlus, 0);
        analogWrite(LeftMinus, 0);
        analogWrite(RightPlus, 50);
        analogWrite(RightMinus, 0);
      	delay(1000);
    }
        // Vice Versa
    else if (sensorValue2 < sensorValue1)
    {
      	//Turn Left for 1 sec at speed (50/255)
        analogWrite(LeftPlus, 50);
        analogWrite(LeftMinus, 0);
        analogWrite(RightPlus, 0);
        analogWrite(RightMinus, 0);
      	delay(1000);
    }
    
    //When the robot hits the ending of the ground line it stops and shifts over to "Off-Line" mode 
    else if (sensorValue1 <= 100 && sensorValue2 <= 100 && firstDrive == true)
    {
      	//Motor Stop for 1 sec
        analogWrite(LeftPlus, 0);
        analogWrite(LeftMinus, 0);
        analogWrite(RightPlus, 0);
        analogWrite(RightMinus, 0);
        onLine = false;
      	Serial.print("online is now false");
      	delay(1000);
    }
  }
        //THIS IS OFF-LINE MODE
        //If the end of the groundline is reached
  if (onLine == false)
  {
    //Distance Sensor Start
    digitalWrite(TRIGPin, LOW);
  	delayMicroseconds(2);
  	digitalWrite(TRIGPin, HIGH);
  	delayMicroseconds(10);
  	digitalWrite(TRIGPin, LOW);
    //Read signal from TRIGPin and store as "tid"
  	tid = pulseIn(ECHOPin, HIGH);
    //Convert "tid" to "distance"
  	distance = tid * 0.034 / 2;
  
  //if the SonarSensor signal is bigger than the chosen minimum distance it goes forward 
  	if (distance >= dis)
    {
      	//Driving Forward
    	analogWrite(LeftPlus, 100);
        analogWrite(LeftMinus, 0);
        analogWrite(RightPlus, 100);
        analogWrite(RightMinus, 0);
    }
    //if signal is smaller:
    else if (distance <= dis)
    {
      	//Motor Stop
      	analogWrite(LeftPlus, 0);
        analogWrite(LeftMinus, 0);
        analogWrite(RightPlus, 0);
        analogWrite(RightMinus, 0);
    
        //The check consists of 4 steps
        //Step 1 check distance to nearest wall in front of robot
        servoFront = distance;
      	//Turning the Servo to the Right
        theServo.write(180);
        delay(2000);
        //Step 2 check distance to nearest wall left of robot
        servoLeft = distance;
      	//Turning the Servo to the Left
        theServo.write(0);
        delay(4000);
        //Step 3 check distance to nearest wall right of robot
        servoRight = distance;
      	//Turning the Servo Back to facing for forward
        theServo.write(90);
        delay(2000);

        //And step 4
        //Compare the 3 checks and turn to the wall farest away.

        //if the wall at left and the wall at right is not existing, it means the end is reached. The robot will turn right and drive off the ground.
        if (servoLeft == 0 && servoRight == 0)
        {
          	//Turning Right (On the spot)
        	analogWrite(LeftPlus, 55);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 0);
            analogWrite(RightMinus, 55);
            delay(1000);
          	//Stopping
            analogWrite(LeftPlus, 0);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 0);
            analogWrite(RightMinus, 0);
            delay(1000);
          	//Driving Forward again
            analogWrite(LeftPlus, 255);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 255);
            analogWrite(RightMinus, 0);
            delay(1000);
        }
        //Turn to left wall
        if (servoLeft >= servoFront && servoLeft >= servoRight && servoRight != 0 || servoLeft == 0)
        {
          	//Turning Left (on the spot)
        	analogWrite(LeftPlus, 0);
            analogWrite(LeftMinus, 55);
            analogWrite(RightPlus, 55);
            analogWrite(RightMinus, 0);
            delay(1000);
          	//Stopping
            analogWrite(LeftPlus, 0);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 0);
            analogWrite(RightMinus, 0);
            delay(1000);
          	//Driving Forward Again
            analogWrite(LeftPlus, 130);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 130);
            analogWrite(RightMinus, 0);
        }
        //Turn to right wall
        if (servoRight >= servoFront && servoRight >= servoLeft && servoLeft != 0 || servoRight == 0)
        {
          	//Turning Right (on the spot)
        	analogWrite(LeftPlus, 55);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 0);
            analogWrite(RightMinus, 55);
            delay(1000);
          	//Stopping
            analogWrite(LeftPlus, 0);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 0);
            analogWrite(RightMinus, 0);
            delay(1000);
          	//Driving Forward Again
            analogWrite(LeftPlus, 130);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 130);
            analogWrite(RightMinus, 0);
		}
        //Continue forward
        if (servoFront >= servoLeft && servoFront >= servoRight)
        {
          	//Driving Forward
        	analogWrite(LeftPlus, 130);
            analogWrite(LeftMinus, 0);
            analogWrite(RightPlus, 130);
            analogWrite(RightMinus, 0);
          	delay(1000);
        }
     }
  }
}

Unity

Nu tænder vi for Unity. Det første der skal gøres når Unity er åbent, er at importere den udleverede Unity.package så vi har et udgangspunkt at starte fra. Den bane vi gerne vil have RoboSchneider til at køre igennem kan ses på billedet nedenfor.

Vi starter med at trække “Robot root” ind i scenen: “PF2-afleveringsbane”. På Robot root sætter vi Wheels, Breadboard, Servo og Linesensor, som “child” til “CarChassis”. Her kan man lege lidt rundt med hvordan ens Robot skal se ud. Vi har valgt at lade Robotten se ud som på billedet nedenfor.  

Her er det vigtigt at huske at gøre følgende:  

  •  Inde i Left Motor, nede i komponentet “Configurable Joint” skal CarChassis sættes ind i “Connected Body”. Ellers vil hjulene ikke følge med Robotten. Gør det samme for Right Motor.  
  • På Breadboard skal H-bridge LeftMotor og H-bridge RightMotor sættes som “child” af Breadboard. Herefter skal Left Motor sættes ind i Komponentet H Bridge (Script) for at den har en reference. Det samme skal gøre for RightMotor (med Right Motor i stedet for Left Motor selvfølgelig) 
  • På Breadboard skal der tilføjes 7 elementer i komponentet Breadboard(Script). (Man ændrer størrelsen af Arduino Object Pins ved at sætte “Size” til den ønskede størrelse). Indsæt H-bridge forwards pin LeftMotor til Element 0. Indsæt H-bridge backwards pin LeftMotor til Element 1. Indsæt H-bridge forwards pin RightMotor til Element 2. Indsæt H-bridge backwards pin RightMotor til Element 3. Indsæt DistanceSensor til Element 4. Indsæt LineSensorLeft til Element 5 samt LineSensorRight til Element 6. Disse 7 Elementer er referencer for Breadboardet, og virker som “pins” i virkeligheden.  

En kort bemærkning til placeringen af ens LDR-modstande samt hjul. Hvis sensorerne er placeret for langt fra hinanden, kan der opstå en uhensigtsmæssig adfærd når den forsøger at rette sin kurs i forhold til den sorte streg. Den vil altså lave store og lange sving hver gang den justerer. Ligeledes hvis de er for tæt på hinanden, så kan sensorerne have svært ved at vurdere som konstruktionen er på stregen eller uden for den, og i værste fald, forårsage at maskinen hele tiden hakker frem og tilbage eller fuldstændig forbiser når stregen svinger. Det anbefales at afprøve og justere i ens eget projekt mht. skalering og position af sensorerne samt hjul. Dertil kan det også anbefales at hjulene sættes foran på robotten, da dette giver en bedre intuitiv styring under opsætningen. I det endelige produkt vil placeringen af hjul og sensorer være afgørende for robottens navigations evne, og dermed hvor nøjagtigt robotten kan følge stregen i gulvet. 

Nu er Robotten bygget, så nu skal der skrives kode, så RoboSchneider bliver funktionel. Man kan med fordel tage koden fra TinkerCAD og bruge som skabelon i Unity. Desværre er kodesproget ikke det samme for Arduino, som det er for Unity, så vi skal skrive noget af koden om, samt tilføje mere kode. Vi vil i dette afsnit ikke gå for meget i dybden med kodeeksempler, men blot redegøre for nogle af de justeringer der skal til for at ”oversætte” koden til Unity. Den fulde kode kan ses forneden. Gå ind i ArduinoMain.cs. 

I Unity (C#) skal man ikke Declare (erklære) de forskellige pinModes, da vi har referencerne liggende ude i selve Editoren. #include <Servo.h> skal ikke indgå i C# koden, derfor heller ikke “Servo theServo; og theServo.attach(pin);”. Måden vi refererer til Servo’en i Unity er gennem Editoren hvor Servo’en bliver sat ind som en public Servo servo; i ArduinoMain.cs scriptet. Desuden skal “long” ændres til “ulong”. Serial virker heller ikke i C#, derfor bruger vi Debug.Log();. Dette er et meget nyttigt værktøj, som vi benyttede til at udradere ikke nævnelsesværdige fejl med, og det kan eksempelvis bruges til at opdele sin kode og se hvor eventuelle problemer opstår når man kører programmet. 

Den første del der skal tilføjes i C# koden, er en bool samt en “if” statement til at fortælle os hvornår Robotten kommer til den første forhindring. Dette uddyber vi længere nede. Til sidst skal alle “delay()” ændres til “yield return new WaitForSeconds(); da “delay()” funktionen ikke er en del af C# dokumentationen. 

Ideelt set ville vi gerne have RoboSchneider til selv at navigere rundt om denne forhindring ved hjælp af afstandsmåleren, da vi ikke længere havde en streg at køre efter udenom forhindringen, men dette voldte os store problemer og uregelmæssig adfærd, hvor robotten ikke kunne finde tilbage på rette kurs igen. Vi var derfor nødsaget til at navigere den manuelt ved at “hardcode” bevægelsen rundt om forhindringen før vi fortsætte resten af programmet til at navigere via stregen og derefter afstandsmåleren. Koden til den første forhindring er resultatet af en række afprøvelser i forhold til vores opbygning og valg af hastigheder for motorerne. 

else if (firstBlock == true)
                    {
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 0);
                        analogWrite(1, 55);
                        analogWrite(2, 55);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(1.1f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 100);
                        analogWrite(1, 0);
                        analogWrite(2, 100);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.7f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 55);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 55);
                        yield return new WaitForSeconds(1f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 100);
                        analogWrite(1, 0);
                        analogWrite(2, 100);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(1.5f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 55);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 55);
                        yield return new WaitForSeconds(1f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 100);
                        analogWrite(1, 0);
                        analogWrite(2, 100);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.6f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 0);
                        analogWrite(1, 55);
                        analogWrite(2, 55);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(1f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                    }

Det er ikke en implementering vi anbefaler, men vores metode kan ses her foroven og længere nede i den komplette Unity kode. Da man kun støder på én forhindring i denne bane følte vi det var en acceptabel løsning i forhold til dette projekt. Hvis der var flere forhindringer i banen, så ville man være nødsaget til at lave en funktion i stedet for at skrive hele omkørslen hver gang RoboSchneider skulle forbi en forhindring.

Til sidst når vi anvender afstandsmåleren til at navigere den resterende del af banen, så er de værdi vi anvender til vores motor inputs for at dreje RoboSchneider igen noget vi har testet os frem til. Selve målingen og beregning af hvilken retning der giver fri passage er ”korrekt”, men når robotten drejer er det en delvist manuel styring i forhold til de værdier og hastigheder vi har angivet for vores motor inputs. Med lidt måling og finjustering skulle det gerne give robotten en næsten nøjagtig rotation hvis banen blev udvidet, men efter for mange retningsskift kan der potentielt opstå nogle udfordringer med robottens retning. 

Hvis koden er skrevet korrekt skulle den gerne ligne noget i stil med den kode der står herunder. 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
public class ArduinoMain : MonoBehaviour
{
    public Breadboard breadboard;
    public Servo servo;
    
    //Setup what needs to be stated before the robot begins its Loop
    IEnumerator setup()
    {
        //Set Servo Motor on 90 degrees (look forward)
        servo.write(90);
        Debug.Log("GO");
        #region PremadeSetup
        yield return StartCoroutine(loop()); ;
        #endregion PremadeSetup
    }
    //Declare the needed variables
    int left = 0;
    int right = 0;
    int minDiff = 50;
    
    bool onLine = true;
    bool firstBlock = false;
    ulong dis = 900;
    ulong servoLeft = 0;
    ulong servoFront = 0;
    ulong servoRight = 0;
    //The Loop Begins
    IEnumerator loop()
    {
        //Read the LDR-Sensores
        left = analogRead(5);
        right = analogRead(6);
        int diff = Mathf.Abs(left - right);
        
        //Read the SonarSensor
        ulong tid = pulseIn(4);
        //Read the ServoMotor
        int servoPos = servo.read();
        
        //As long as we are on the groundline
        if (onLine == true)
        {
            //If the robot is turning right, it turns left slightly
            if (left < right)
            {
                analogWrite(0, 0);
                analogWrite(1, 0);
                analogWrite(2, 50);
                analogWrite(3, 0);
            }
            // Vice Versa
            else if (right < left)
            {
                analogWrite(0, 50);
                analogWrite(1, 0);
                analogWrite(2, 0);
                analogWrite(3, 0);
            }
            // If the robot follows the line fine:
            else if (diff <= minDiff)
            {
                //it goes forward
                if (tid >= dis || tid == 0)
                {
                    analogWrite(0, 100);
                    analogWrite(1, 0);
                    analogWrite(2, 100);
                    analogWrite(3, 0);
                }
                // else it hits an obsticle
                else if (tid <= dis)
                {
                    //There is a fail in the program that makes it delay just enough for the robot to bug on start-up. This "if" prevents it from stopping
                    if (firstBlock == false)
                    {
                        firstBlock = true;
                        Debug.Log("FirstBlockPassed");
                    }
                    //After the bug, the robot avoits absticles but driving around them as shown below.
                    else if (firstBlock == true)
                    {
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 0);
                        analogWrite(1, 55);
                        analogWrite(2, 55);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(1.1f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 100);
                        analogWrite(1, 0);
                        analogWrite(2, 100);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.7f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 55);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 55);
                        yield return new WaitForSeconds(1f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 100);
                        analogWrite(1, 0);
                        analogWrite(2, 100);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(1.5f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 55);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 55);
                        yield return new WaitForSeconds(1f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 100);
                        analogWrite(1, 0);
                        analogWrite(2, 100);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.6f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 0);
                        analogWrite(1, 55);
                        analogWrite(2, 55);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(1f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                    }
                }
            }
            //When the robot hits the ending of the ground line it stops and shifts over to "Off-Line" mode 
            if (left <= 1000 && right <= 1000)
            {
                analogWrite(0, 0);
                analogWrite(1, 0);
                analogWrite(2, 0);
                analogWrite(3, 0);
                onLine = false;
            }
        }
        //THIS IS OFF-LINE MODE
        //If the end of the groundline is reached
        if (onLine == false)
        {
            //Debug.log("Offline") to make it obvious for the spectators
            Debug.Log("OffLine");
            //if the SonarSensor signal is bigger than the chosen minimum distance it goes forward 
            if (tid >= dis)
            {
                analogWrite(0, 130);
                analogWrite(1, 0);
                analogWrite(2, 130);
                analogWrite(3, 0);
            }
            //if the SonarSensor signal is smaller than the chosen minimum distance it stops and makes a check 
            else if (tid <= dis)
            {
                analogWrite(0, 0);
                analogWrite(1, 0);
                analogWrite(2, 0);
                analogWrite(3, 0);
                yield return new WaitForSeconds(2);
                //The check consists of 4 steps
                //Step 1 check distance to nearest wall in front of robot
                servoFront = pulseIn(4);
                Debug.Log("Front Servo =" + servoFront);
                servo.write(0);
                yield return new WaitForSeconds(2);
                //Step 2 check distance to nearest wall left of robot
                servoLeft = pulseIn(4);
                Debug.Log("Left Servo =" + servoLeft);
                servo.write(180);
                yield return new WaitForSeconds(4);
                //Step 3 check distance to nearest wall right of robot
                servoRight = pulseIn(4);
                Debug.Log("Right Servo =" + servoRight);
                servo.write(90);
                yield return new WaitForSeconds(2);
                //And step 4
                //Compare the 3 checks and turn to the wall farest away.
                //if the wall at left and the wall at right is not existing, it means the end is reached. The robot will turn right and drive off the ground.
                if (servoLeft == 0 && servoRight == 0)
                {
                    Debug.Log("ENDGAME");
                    analogWrite(0, 55);
                    analogWrite(1, 0);
                    analogWrite(2, 0);
                    analogWrite(3, 55);
                    yield return new WaitForSeconds(1f);
                    analogWrite(0, 0);
                    analogWrite(1, 0);
                    analogWrite(2, 0);
                    analogWrite(3, 0);
                    yield return new WaitForSeconds(1f);
                    analogWrite(0, 255);
                    analogWrite(1, 0);
                    analogWrite(2, 255);
                    analogWrite(3, 0);
                    yield return new WaitForSeconds(10f);
                }
                //Turn to left wall
                if (servoLeft >= servoFront && servoLeft >= servoRight && servoRight != 0 || servoLeft == 0)
                {
                    analogWrite(0, 0);
                    analogWrite(1, 55);
                    analogWrite(2, 55);
                    analogWrite(3, 0);
                    yield return new WaitForSeconds(1f);
                    analogWrite(0, 0);
                    analogWrite(1, 0);
                    analogWrite(2, 0);
                    analogWrite(3, 0);
                    yield return new WaitForSeconds(1f);
                    analogWrite(0, 130);
                    analogWrite(1, 0);
                    analogWrite(2, 130);
                    analogWrite(3, 0);
                }
                //Turn to right wall
                if (servoRight >= servoFront && servoRight >= servoLeft && servoLeft != 0 || servoRight == 0)
                {
                    analogWrite(0, 55);
                    analogWrite(1, 0);
                    analogWrite(2, 0);
                    analogWrite(3, 55);
                    yield return new WaitForSeconds(1f);
                    analogWrite(0, 0);
                    analogWrite(1, 0);
                    analogWrite(2, 0);
                    analogWrite(3, 0);
                    yield return new WaitForSeconds(1f);
                    analogWrite(0, 130);
                    analogWrite(1, 0);
                    analogWrite(2, 130);
                    analogWrite(3, 0);
                }
                //Continue forward
                if (servoFront >= servoLeft && servoFront >= servoRight)
                {
                    analogWrite(0, 130);
                    analogWrite(1, 0);
                    analogWrite(2, 130);
                    analogWrite(3, 0);
                }
            }
        }
        
        #region DoNotDelete
        //Wait for one frame
        yield return new WaitForSeconds(0);
        //New loop():
        yield return loop();
        #endregion DoNotDelete 
    }
    
    #region PremadeDefinitions
    void Start()
    {
        servo = FindObjectOfType<Servo>();
        if (servo == null)
        {
            Debug.Log("No servo found in the scene. Consider assigning it to 'ArduinoMain.cs' manually.");
        }
        Time.fixedDeltaTime = 0.005f; //4x physics steps of what unity normally does - to improve sensor-performance.
        StartCoroutine(setup());
    }
    IEnumerator delay(int _durationInMilliseconds)
    {
        float durationInSeconds = ((float)_durationInMilliseconds * 0.001f);
        yield return new WaitForSeconds(durationInSeconds);
    }
    public long map(long s, long a1, long a2, long b1, long b2)
    {
        return b1 + (s - a1) * (b2 - b1) / (a2 - a1);
    }
    public ulong millis()
    {
        return (ulong)(Time.timeSinceLevelLoad * 1000f);
    }
    public ulong abs(long x)
    {
        return (ulong)Mathf.Abs(x);
    }
    public long constrain(long x, long a, long b)
    {
        return (x < a ? a : (x > b ? b : x));
    }
    #endregion PremadeDefinitions
    #region InterfacingWithBreadboard
    public int analogRead(int pin)
    {
        return breadboard.analogRead(pin);
    }
    public void analogWrite(int pin, int value)
    {
        breadboard.analogWrite(pin, value);
    }
    public bool digitalRead(int pin)
    {
        return breadboard.digitalRead(pin);
    }
    public void digitalWrite(int pin, bool isHigh)
    {
        breadboard.digitalWrite(pin, isHigh);
    }
    public ulong pulseIn(int pin)
    {
        return breadboard.pulseIn(pin);
    }
    #endregion InterfacingWithBreadboard
}

Konklusion 

RoboSchneider fuldfører den opstillede bane på omtrent 87 sekunder. Den navigerer rundt ved hjælp af en række sensorer, som var påkrævet af opgavebeskrivelsen, nemlig LDR-modstande samt ultralydafstandsmåler. Den er i stand til at nå banens ende og undgå al kollision undervejs. 

Perspektivering  

RoboSchneider er ikke i stand til at fuldføre banen fuldstændig på egen hånd. Med dette menes, at vi var nødsaget til at implementere en lille smule “hard coding” til at navigere rundt om den første forhindring, hvilket vi også belyser tidligere i indlægget. Ydermere har RoboSchneider ikke nødvendigvis den hurtigste tid, men den formår at opnå en sikker og stabil gennemførelse af banen. Med lidt mere justering og testing kan denne tid forbedres, men vi har valgt at beholde den i dens nuværende tilstand med en solid performance for opgavens og forståelsens skyld. 

Afslutningsvist nævner vi, at delay() funktionen ikke virker i C#. Da dette teknisk set er sandt, så er vi opmærksomme på, at Jamie og Christian var med til at udarbejde en IEnumerator “delay” funktion, men da vi havde problemer med at implementere den, så var vi nødsaget til at bruge WaitForSeconds() i stedet. 

More

Roomba’s Overlegen

Demonstration af Roomba’s Overlegen i Unity
Figur 1: Opbygning af Roomba’s overlegen på Tinkercad

Af Kim Dinh Le & Osman Bilal Khassouk

Introduktion

Dette blogindlæg er et forsøg på at lave en robot der kan gennemføre et specifikt forhindringsbane. Det vil siges at den ikke er beregnet til at kunne gennemføre en anden bane med tilhørende elementer. Komponenter der bliver brugt er følgende:

  • Arduino Uno R3
  • 1 H-Bridge L293D
  • 9 V batteri
  • 2 Hobby Gearmotor
  • 2 Photoresistor
  • 1 Mirco Servo
  • 1 Ultrasonic Distance Sensor
  • 1 220 Ohm resistor
  • 4 Red LED
  • 2 10k Ohm resistor

Målet med denne robot er at kunne gennemføre denne bane uden at rører væggene, og komme igennem med den hurtigeste tid.

L293D i sammenspil med Arduino og DC motor.

H-Bridge Motor Driver bruges som en central komponent i vores opbyggelse af Roomba. Komponenten benyttes som en dobbelt H-bro, som gennem sammenspil med vores Arduino Uno kontrollere de 2 DC Motor som er monteret på Roomba.

Sammenkoblingen mellem L293D og de 2 DC-motor styre Roombas, hvilken retning der skal køres. For at kunne bruge motorerne har vi koblet de positive og negative poler med vores L293Ds 4 outputs, de fire outputs i figuren for neden er 1Y, 2Y, 3Y, 4Y.

1,2 EN-stiften tænder eller slukker den ene motor der er tilkoblet h-broen, den skal sættes til HIGH for at motoren kan starte. 3,4 EN-stiften gør det samme ved den anden motor. (Se figur. 2)

Vcc1 giver strøm til L293Ds interne system. Hvor Vcc2 er den strømkilde motoren bruger. Derfor er 9V batteriet tilkoblet til Vcc 2 for at styrke de motore som bruges.

Figur 2: Datablad af vores L293D

1A, 2A, 3A, 4A er L293Ds fire inputs, de er alle koblet sammen til Arduinoens pins. De fire inputs er dem som afgør, hvilken handling Arduinoen styre de 2 DC motorers handling.

Kodeudsnit 1: Pins fra Arduinoen til L293D

LDR Modstande
Fotomodstandene som bruges til Roomba benyttes for at måle forskellen på jordoverfladen og kende forskel på hvid og sort. For at øge chance for at fotomodstandene kan opfange dette ville 4 LED’er implementeres for at belyse jorden under Roomba. Der skal dog implementeres en lille væg eller lignende mellem LED’erne og fotomodstandene så de ikke påvirker de værdier som opfanges.  De værdier som opfanges af fotomodstandene, bestemmer hvordan vores Roomba agere og hvilken handling den skal påtage sig.

Hvis man kigger på kodeudsnittet på kodeudsnit 2. Så bruger Roomba en samspil af Arduino, DC-motorer, L293D, LDR- og LED sensorer til at navigere sig selv autonomt, ved at følge en sort linje.

Kodeudsnit 2: Koden til navigation af den sorte linje.

Gennem LDR-modstandene som er sammenkoblet med vores Arduino registrer værdierne, dette gøres ved brug af analogRead(sensor1Value) og analogRead(sensor2Value). Arduino registrerer disse værdier og checker hvilken If statement er sand, dette bestemmer handling vores Roomba skal påtage sig.

sonarSensor og Servo-Motor

Vi benytter denne sensor til at måle afstand på robotten og de forhindringer som kommer foran den. Sensoren måler afstanden ved brug af en sonar. Der sendes en ultralyd impuls fra sensoren, og afstanden der skal måles bestemmes ved at måle den tid det tager for lydens ekko at komme tilbage, sonaren måler dette i meter per sekund.
Der er derfor en funktion er lavet som konvertere de mikrosekunder som Sonaren kommer med til cm, dette gøres for at give en bedre forståelse af afstanden mellem Sonaren og objektet.
Dette er i vores tilfælde ideelt for robotten i den sidste forhindring med væggene, hvor sensoren bruges for at robotten ikke skal køre ind i forhindringerne, men dreje i stedet.

Kodeudsnit 3: Målingens konvertering fra meter i sekunder til Cm

Sensorens kode fungerer på den måde at sonarSensor skifter mellem “LOW”, “HIGH” også “LOW”. Sonarsensore reagere på “HIGH” pulse, der tilføjet “LOW” i starten og sat en lille tid som afstand, og en “LOW” i slutningen. At sætte en lille pause mellem “LOW” og “HIGH” i starten kan resultere i en mere effektiv “HIGH” pulse.
Dette kan give en forbedring i registreringen af objekter. Efter denne process registrere den kun ”HIGH” værdien, hvorefter konverteringen til cm finder sted. Dette kan ses i kode afsnit 4.

Kode afsnit 4: Koden som styre vores sonarSensor

Servomotoren fungere ved at sende et servo et PWM-signal (pulsbredde-modulering). Der er en minimal puls, en maksimal puls og en gentagelsesfrekvens. En servomotor kan normalt kun dreje 90 ° i begge retninger i alt 180 ° bevægelse.

Servoen benyttes til at dreje vores Sonar sensor. Sonaren er sammenkoblet ovenpå for at kunne registrere de kommende forhindringer før tid, dette bruges i den sidste del af forhindringen hvor den servoen drejer, hvilket gør sonaren i stand til at registrere muren og følge den til mål.

Man kan i Kodeafsnittet 5 se hvordan vores servomotoren drejer, den begynder med at dreje 90 grader. Dette gøres for at sonarSensoren som er monteret i toppen, kan registrere om der befinder sig objekter ved Roombas højre side.

Efter delay på 1 sekund drejer vores Roomba endnu 90 grader til 180 grader for at kunne registrere om der befinder sig objekter bag den.

Kode afsnit 5: Servo motoren

Opbygning af Program

Figur 3: Forhindringsbanen set fra fugleperspektiv

Koden er skrevet op sådan så det er delt op i to forskellige måder. Hvis man ser på banen (figur 3). Så kan man se at den første del af banen er tegnet op med en sort streg. hvoraf den sidste del af banen er frit løb. Koden er sat op i små bidder, så det er nemmere overskueligt, og giver mulighed for at skifte stadier, efter hvorhenne Roomba’s overlegen er placeret. Den første del af banen er hvor den skal følge efter den sorte streg. Ved hjælp af fotomodstander, er det blevet muligt at kunne måle værdien af roomba’s overlegen kører på enten en hvid eller sort plan, og derved roterer sig alt efter hvad der er nødvendigt.

 void followLine()
    {
        sensorLeft = analogRead(4);
        sensorRight = analogRead(5);
        if (sensorLeft > 900 && sensorRight > 900)
        {
            driveForward();
        }
        else if (sensorLeft > 900 && sensorRight < 900)
        {
            //Debug.Log("turning Right");
            turnRight();
        }
        else if (sensorLeft < 900 && sensorRight > 900)
        {
            //Debug.Log("turning Left");
            turnLeft();
        }
        else if(sensorLeft < 900 && sensorRight < 900)
        {
            driveForward();
            linefollower = false;
        }
    }

Hvis man ser videre på figur 3, kan man se at der er en forhindring ved den første del af banen. Dette er en rød mur som robotten skal kører rundt om. Det er gjort muligt at kører rundt om ved brug af afstandssensoren, som registerer at den nærmer sig en mur. Derefter kører koden ind til næste stadie hvor den vælger at undgå den første mur.

 if(wallCounter == 1)
        {
            if(pulseIn(6) < 500 && pulseIn(6) > 1)
            {
                Debug.Log("something here");
                stopDriving();
                yield return delay(1000);
                turnBack();
                yield return delay(1000);
                turnLeft();
                yield return delay(1000);
                driveForward();
                yield return delay(2500);
                turnRight();
                yield return delay(1000);
                driveForward();
                yield return delay(2500);
                turnRight();
                yield return delay(700);
                driveForward();
                yield return delay(900);
                turnLeft();
                yield return delay(1000);
                wallCounter++;
                servoMode = true;
                Debug.Log(wallCounter.ToString());
            }

Når den så har gennemførst den første del af banen, sætter vi en af vores booleske værdier til falsk. Dette gøres ved at fotomodstandene ikke længere måler at der er en sort plan. Når den når hen til sin første mur, drejer roomba’s overlegen statisk 90 grader i den rigtige retning, og servomotoren drejer 150 grader så den peger til højre for sig. Derefter går den til wallMode, hvor den så følger langs muren. Dette gøres ved at måle afstanden mellem roomba’s overlegen og muren.

   void wallMode()
    {
        if (readDist() < 800 && readDist() > 400)
        {
            driveForward();
        }
        else if (readDist() > 800 || readDist() == 0)
        {
            analogWrite(0, 90);
            analogWrite(2, 45);
        }
        else if (readDist() < 400 && readDist() > 1)
        {
            analogWrite(0, 0);
            analogWrite(2, 120);
        }
    }

Mekanik

Roomba er opbygget på en måde hvorpå en realistisk version ville være ligetil, der er opsæt LED’er tæt på de LDR modstande vi har lagt sammen. Dette gøres for at illusterer, hvordan sammensætningen ville se ud, der vil dog være noget fysisk som blokere for LED’erne så de ikke har indflydelse på vores værdier

Elektronik

Elektronik delen blev hvor den alle komponenterne skulle sammenspille med hindanden og skabe et funktionelt kredsløb som vi kalder Roomba. Gennem elektronikken har vi forskellige signaler fra vores kompenenter i form a lyd, lys osv. som giver information i form af værdier såsom afstand fra forhindringer, og lys som kan hjælpe med at navigere os til at forblive på den sorte linje.

Der blev brugt modstande af 10k ohm resistorer til LDR-sensorerne i vores TinkerCad. Dette bliver brugt da i vores mållinger med fotomodstande har den en minimum modstand på 20K ohm og ved maksimale modstand have en modstand på 2 Mohm.

Ved brug af Ohms lov bruges det til at finde fotomodstanden ud fra lysintensiteten.

Vi starter med at udregne når modstanden er 20K ohm. Hvilket er hvor lyset er maksimalt.

Derefter regner vi for når den rammer den sorte streg og lyset er minimalt.

Herudover kan vi så se at den synlige rækkevidde er rundt regnet 1,65V (4,98V-3,33V). Det vil altså siges, da inputtet bliver læst gennem analogRead() funktionen, så vil værdierne ligge mellem 0 og 1023. Arduino kører på 5V så det betyder at 1 værdi vil svare til 0,005V. Med dette kan vi så udregne de mulige værdier ved brug af målbart lys.

Dette burde være en tilstrækkeligt rækkevidde. Der skal dog lægges mærke til at 10k ohm, ikke er nødvendigt, og der måske er mulighed for at have en højere eller lavere modstand, så længe den mulige rækkevidde af målt lys ikke er for højt eller for lavt.

Software

Software fungere som hjernen af Roomba’s overlegen, og har et tæt sammenspil med elektronikken. Da Roomba’s overlegen funktionaliteter, gøres brug af værdierene som komponenterne registerer. Foreksempel anvender softwaren, brug af fotomodstanende værdier til at kunne måle om den er i en sort eller hvidt plan, og derved kører efter det bestemte fart. Et andet eksempel ville være afstandssensoren, som måler om Roomba’s overlegen er ved at ramme en mur, og derved justere sine handlinger alt efter hvilke stadie den er nået til.

Konklusion

Roomba’s overlegen klarer banen på 100 sekunder, som også kan ses i videon. For at kunne klarer denne forhindring er der blevet brugt en kombination af mekanik, elektronik og software, som giver den de nøvendige funktionaliteter for at kunne gennemføre banen. Dog er dette en kombination af vores Unity projekt og Tinkercad, og derfor ikke svarer til en fysisk plan. Derfor vil dette være en mere reference til en fysisk model og ikke direkte.

Perspektivering

Roomba’s overlegen klarer banen, dog har den kun mulighed for at klare denne specifikke bane, da mange af de forhindringer der blev lagt op, blev løst ved hjælp af “hardcoding”. Dette er gjort, da vores kompetenceniveau ikke er god nok til at kunne lave en selvtænkede AI. Hvis noget skulle ændres ville det være at den løser forhindringer i en mere selvtænkende forstand.

Raw code i Tinkercad

#include <Servo.h>

//LDR-Resistor
int sensor1Pin = A0;
int sensor1Value = 0;

int sensor2Pin = A1;
int sensor2Value = 0;

int cm, duration;

//Servo
Servo servoMotor;

//Sonar
const int sonarSensor = 2;


void setup()
{
  Serial.begin(9600);
  
  pinMode(10, OUTPUT);
  pinMode(11, OUTPUT);
  pinMode(12, OUTPUT);
  digitalWrite(12, HIGH); //Sæt denne pin til High for at enable motor 1
  

  pinMode(5, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(7, OUTPUT);
  digitalWrite(7, HIGH);// Sæt denne pin til High for at enable motor 2
  
  pinMode(sonarSensor, OUTPUT);

  servoMotor.attach(3);
}

void loop()
{
  sensor1Value = analogRead(sensor1Pin);
  Serial.println(sensor1Value);
  
  sensor2Value = analogRead(sensor2Pin);
  Serial.println(sensor2Value);

  
  
  //Test Servo
  servoMotor.write(90);
  delay(200);
  servoMotor.write(180);
  
  
if(sensor1Value > 850 && sensor2Value > 850)
  {
    digitalWrite(10,HIGH);
    digitalWrite(11,LOW);
    digitalWrite(5,HIGH);
    digitalWrite(6,LOW);
  }
  else if(sensor1Value > 850 && sensor2Value < 100)
  {
    digitalWrite(10,HIGH);
    digitalWrite(11,LOW);
    digitalWrite(5,HIGH);
    digitalWrite(6,HIGH);
  }
  else if(sensor1Value < 100 && sensor2Value > 850)
  {
    digitalWrite(10,HIGH);
    digitalWrite(11,HIGH);
    digitalWrite(5,HIGH);
    digitalWrite(6,LOW);
   }
  else if(sensor1Value < 100 && sensor2Value < 100)
  {
    digitalWrite(10,LOW);
    digitalWrite(11,HIGH);
    digitalWrite(5,LOW);
    digitalWrite(6,HIGH);
  }

   
  //Test Sonar
  digitalWrite(sonarSensor, LOW);
  delayMicroseconds(2);
  
  digitalWrite(sonarSensor, HIGH);
  delayMicroseconds(5);
  
  digitalWrite(sonarSensor, LOW);
  
  pinMode(sonarSensor, INPUT);
  
  duration = pulseIn(sonarSensor, HIGH);
  
  cm = mikrosekunderTilCm(duration);
  
  Serial.print(cm);
  Serial.print("cm");
  Serial.println();
  
  }
  
  int mikrosekunderTilCm(int mikrosekunder){
   return mikrosekunder / 29 / 2;
  
}

Raw code i Unity

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;

public class ArduinoMain : MonoBehaviour
{
    public Breadboard breadboard;
    public Servo servo;

    int sensorLeft;
    int sensorRight;
    int wallCounter = 1;
    bool linefollower = true;
    bool servoMode;

    //On included/premade Arduino functions:
    //delay(timeInMilliseconds) : use "yield return delay(timeInMilliseconds)", to get similar functionality as delay() in arduino would give you.

    //map() : works exactly as on Arduino, maps a long from one range to another. 
    //If you want to get an int or a float from the map()-function, you can cast the output like this: (int)map(s, a1, a2, b1, b2) or (float)map(s, a1, a2, b1, b2) 

    //millis() : returns the time as a ulong since the start of the scene (and therefore also the time since setup() was run) in milliseconds.

    //If you want to do something similar to serial.println(), use Debug.Log(). 

    //analogWrite() and analogRead() works as they do in arduino - remember to give them correct input-values.
    //digitalRead() and digitalWrite() writes and returns bools. (High = true). 
    //LineSensors have both write-functions implemented, motors/hbridge have both read-functions implemented.
    //The console will display a "NotImplementedException" if you attempt to write to sensors or read from motors. 


    //Additions from 21-04-2020:

    //Distance sensor:
    //The Distance (ultrasonic) sensor is added, if you use "pulseIn()" on the pin it is assigned to, 
    //it will return the time it took sound to travel double the distance to the point of impact in microseconds (type: ulong).
    //This mimics roughly how the HC-SR04 sensor works. 
    //There is no max-range of the distance-sensor. If it doesn't hit anything, it returns a 0.

    //Servo:
    //if you add the servo-prefab to the scene, ArduinoMain will automatically find the servo object, essentially handling "servo.attach()" automatically. 
    //There can be only one servo controlled by this script.
    //servo.write() and servo.read() implemented, they function similar to a servomotor. 
    //The angles that servo.write() can handle are [0:179]. All inputs outside of this range, are clamped within the range.
    //servo.read() will return the last angle written to the servo-arm. 
    //In order to attach something to the servo, so that it rotates with the servo-arm, simply make the object you wish to rotate, a child of either: Servo-rotationCenter or Servo-arm. 
    //Make sure to take into account the position of the object relative to Servo-rotationCenter. The rotated object will rotate positively around the Y-axis (up) of the Servo-rotationCenter gameobject.




    IEnumerator setup()
    {
        //Your code goes here:
        servo.write(90);
        
        //Example of delay:
        Debug.Log("pre-delay log");
        yield return delay(2000); //2 second delay
        //Your code ends here -----

        //following region ensures delay-functionality for setup() & loop(). Do not delete, must always be last thing in setup.
        #region PremadeSetup
        yield return StartCoroutine(loop()); ;
        #endregion PremadeSetup
    }

    IEnumerator loop()
    {
        //Your code goes here:
        if (linefollower == true)
        {
            followLine();
        }
        if(wallCounter == 1)
        {
            if(pulseIn(6) < 500 && pulseIn(6) > 1)
            {
                Debug.Log("something here");
                stopDriving();
                yield return delay(1000);
                turnBack();
                yield return delay(1000);
                turnLeft();
                yield return delay(1000);
                driveForward();
                yield return delay(2500);
                turnRight();
                yield return delay(1000);
                driveForward();
                yield return delay(2500);
                turnRight();
                yield return delay(700);
                driveForward();
                yield return delay(900);
                turnLeft();
                yield return delay(1000);
                wallCounter++;
                servoMode = true;
                Debug.Log(wallCounter.ToString());
            }
        }

        else if(wallCounter > 1 && servoMode == true)
        {
            ulong read = readDist();
            if(read < 700 && read > 1)
            {
                Debug.Log("Wall Ahead, adjusting");
                turnLeft();
                yield return delay(1500);
                servo.write(150);
                yield return (1600);
                servoMode = false;
            } 
        }
        else if(servoMode != true)
        {
            wallMode();
        }

        //Debug.Log("Distance wall: " + pulseIn(6));


        //Following region is implemented as to allow "yield return delay()" to function the same way as one would expect it to on Arduino.
        //It should always be at the end of the loop()-function, and shouldn't be edited.
        #region DoNotDelete
        //Wait for one frame
        yield return new WaitForSeconds(0);
        //New loop():
        yield return loop();
        #endregion DoNotDelete 
    }

    void driveForward()
    {
        analogWrite(0, 60);
        analogWrite(2, 60);
    }
    

    void turnLeft()
    {
        analogWrite(0, 0);
        analogWrite(2, 90);
    }

    void turnRight()
    {
        analogWrite(0, 90);
        analogWrite(2, 0);
    }

    void turnBack()
    {
        analogWrite(0, 0);
        analogWrite(2, 0);
        analogWrite(1, 30);
        analogWrite(3, 30);
    }

    void stopDriving()
    {
        analogWrite(0, 0);
        analogWrite(2, 0);
        analogWrite(1, 0);
        analogWrite(3, 0);
    }

    void wallMode()
    {
        if (readDist() < 800 && readDist() > 400)
        {
            driveForward();
        }
        else if (readDist() > 800 || readDist() == 0)
        {
            analogWrite(0, 90);
            analogWrite(2, 45);
        }
        else if (readDist() < 400 && readDist() > 1)
        {
            analogWrite(0, 0);
            analogWrite(2, 120);
        }
    }

    public ulong readDist()
    {
        ulong read = breadboard.pulseIn(6);
        Debug.Log("Distance from wall: " + read);
        return read;
    }

    void followLine()
    {
        sensorLeft = analogRead(4);
        sensorRight = analogRead(5);
        if (sensorLeft > 900 && sensorRight > 900)
        {
            driveForward();
        }
        else if (sensorLeft > 900 && sensorRight < 900)
        {
            //Debug.Log("turning Right");
            turnRight();
        }
        else if (sensorLeft < 900 && sensorRight > 900)
        {
            //Debug.Log("turning Left");
            turnLeft();
        }
        else if(sensorLeft < 900 && sensorRight < 900)
        {
            driveForward();
            linefollower = false;
        }
    }




    #region PremadeDefinitions
    void Start()
    {
        servo = FindObjectOfType<Servo>();
        if (servo == null)
        {
            Debug.Log("No servo found in the scene. Consider assigning it to 'ArduinoMain.cs' manually.");
        }
        Time.fixedDeltaTime = 0.005f; //4x physics steps of what unity normally does - to improve sensor-performance.
        StartCoroutine(setup());


    }

    IEnumerator delay(int _durationInMilliseconds)
    {
        float durationInSeconds = ((float)_durationInMilliseconds * 0.001f);
        yield return new WaitForSeconds(durationInSeconds);
    }

    public long map(long s, long a1, long a2, long b1, long b2)
    {
        return b1 + (s - a1) * (b2 - b1) / (a2 - a1);
    }

    public ulong millis()
    {
        return (ulong)(Time.timeSinceLevelLoad * 1000f);
    }

    public ulong abs(long x)
    {
        return (ulong)Mathf.Abs(x);
    }

    public long constrain(long x, long a, long b)
    {
        return (x < a ? a : (x > b ? b : x));
    }


    #endregion PremadeDefinitions

    #region InterfacingWithBreadboard
    public int analogRead(int pin)
    {
        return breadboard.analogRead(pin);
    }
    public void analogWrite(int pin, int value)
    {
        breadboard.analogWrite(pin, value);
    }
    public bool digitalRead(int pin)
    {
        return breadboard.digitalRead(pin);
    }
    public void digitalWrite(int pin, bool isHigh)
    {
        breadboard.digitalWrite(pin, isHigh);
    }

    public ulong pulseIn(int pin)
    {
        return breadboard.pulseIn(pin);
    }
    #endregion InterfacingWithBreadboard
}
More

FunkyRobo

Af Borgar Bodoy, Thomas Vedel og Martin Sørensen

Dette blogindlæg vil beskrive hvordan man kan lave en selvstyrende robot, der kan køre igennem denne bane. I den første del af banen skal robotten følge den sorte streg via lyssensorer. I den anden del skal robotten finde vej via distance sensor.

Disse komponenter skal bruges for at bygge robotten:

2 * 9V batterier

1 * SM-S2309S Micro Servo-motor

1 * Arduino Uno R3

1 * 28015 Ultrasonic sensor

1 * L293D H-Bro driver

3 * 10kOhm resistor

2 * Ambient light sensor (phototransistor)

1 * Pushbutton

2 * Hvide LED pære

2 * Hobby DC-Motorer

2 * 160ohm resistor

2 * Polarized capacitors

1 * Slideswitch

1 * LM7805 5V regulator

Opbygning af hardware i Tinkercad

Video med gennemgang af tinkercad opsbygning

Beskrivelse af hardware

Spændings regulator

For at illustrere hvordan robottens opbygning ville være hvis det var en virkelig robot og ikke kun en simulation er der anvendt en spændingsregulator til at regulere 9V ned til 5V. Der anvendes en LM7805 (https://www.sparkfun.com/datasheets/Components/LM7805.pdf) som er en fixed output regulator der har et konstant output der anvendes på arduinoen. 

I databladet findes der et diagram over hvordan den skal monteres som det ovenover. Der er derfor taget udgangspunkt i det. Spændingsregulatoren fungerer således at input spændingen kan håndtere diverse resistorer og andre komponenter hvor regulatorens output stadig vil blive på 5V. 

Når man starter simuleringen på TinkerCAD vil der altid blive tilsluttet et USB kabel til arduinoen hvor den så får strøm. Derfor har vi tilføjet en kontakt hvor man kan tænde og slukke den fiktive strømforsyning, som vist på billedet nedenfor.

Tryk knap

Der er valgt en knap for at styre hvilken tilstand robotten er i, hvilket vil sige når robotten tændes vil den være i “follow line mode” og efter blot et tryk på knappen vil den skifte over til “ultrasonic mode” hvor den ikke vil bruge sensorer til at følge linjen men heller bruge servo samt ultrasonic sensoren. Til tryk knappen bruges modstande på 10k ohm, da den bruges til en pull down modstand (https://www.arduino.cc/documents/datasheets/Button.pdf), dette laves som nedenfor.

Den grønne ledning bliver forbundet på sekundærsiden af trykknappen, således den læser inputtet fra brugeren, når brugeren trykker på knappen skabes der forbindelser mellem de to sider og sender 5V gennem den pin den grønne ledning er forbundet til på arduinoen, hvilket svarer til HIGH, og derefter er omskrevet til “1” inde i koden og LOW, “0” når den så ikke er trykket på. 

Lysdioder

De to hvide lysdioder udsender et konstant lys. Når dette lys rammer en overflade bliver det reflekteret, det reflekterede lys måles ved hjælp af de to fototransistore.

Til udregning af formodstande til de to hvide lysdioder,  skal der findes en modstand så der opstår et spændingsfald der svarer til forskellen mellem strømkilden for de 5V og de 1.8V der så optages af lysdioden, hvilket vil sige:

U_formodstand = U_strømkilde – U_lysdiode

U_formodstand = 5V – 1.8V

U_formodstand = 3.2V

Ved brug af ohms lov, findes den størrelse der skal til for at beskytte lysdioden, ved at dele den gennemgående strøm med spændingen (http://www1.futureelectronics.com/doc/EVERLIGHT%C2%A0/334-15__T1C1-4WYA.pdf).

U = I * R

R = U_formodstand / I_lysdiode

R = 3.2V / 0.02A = 160 ohm.

På breadboardet bliver det forbundet som vist på billedet nedenfor. 

Fototransistor

Til at følge linjen i første halvdel af banen bruges der to fototransistorer, som sender lys ud og måler det reflekterede lys. På denne måde er det muligt for robotten at vide om fototransistoren er over stregen, da den så vil måle en lavere mængde reflekteret lys i forhold til over den lyse overflade. Disse værdier bruges i koden, for at bestemme robottens opførsel når den opfanger stregen på banen.

På den samme måde som tryk knappen, bruger vi en pull down resistor på 10 kOhm der sørge for at der fremkommer en spændingsdeling der vi forbinder i A0 på arduinoen, som vist på figuren ovenfor. Det er det samme gældende for den anden fototransistor der er forbundet i A1.

Fototransistoren fungerer på den samme måde som en almindelig transistor bare hvor den side som skal have spænding for at slutte transistoren er udsat for lys, det vil sige når den får lys, kan vi måle det via analogRead() metoden inde i koden.

L293D H – bro driver

En H-bro bliver brugt til at skifte mellem positiv og negativ ladning. Derfor er den et centralt komponent til denne robot, da den skal bruges til at skifte retningen som DC-motorerne drejer. L293D er en quadruple half H-driver, hvilket vil sige at den kan bruges til at styre 2 DC-motorer. 

L293D er lavet til at give tovejs strøm af op til 600mA af spændinger fra 4.5 V til 36 V. Det er dog anbefalet at bruge en forsyningsstrøm på 4,5V – 7V. (https://cdn-shop.adafruit.com/datasheets/l293d.pdf

H-broen er opdelt på den måde at Enable 1,2 og Enable 3,4 bliver brugt til at bestemme hvilken fart hver DC-motor skal rotere med, dette bestemmes via PWM (Pulse Width Modulation) som fås fra to arduino pins. PWM virker ved at der bliver sendt et digitalt signal fra arduinoen, som bliver fortolket som et analog signal af h-broen. 

Dette gøres ved hjælp af analogWrite som kaldes på de to pins der er forbundet til Enable 1,2 og Enable 3,4. analogWrite metoden tager en værdi mellem 0 og 255, hvor 0 = 0% duty cycle og 255 = 100% duty cycle. Denne robots hastighed er bestemt ved at arduinoen sender værdien 50 til begge pins, hvilket svarer til en duty cycle på 19,6% (50/255 * 100 = 19,6%). H-broens Vcc 2 er forbundet til et batteri på 9V hvilket vil sige at h-broens output spænding er 9V ved 100% duty cycle. Dette passer dog ikke helt da noget af spændingen afgår i processen, men det tages der ikke højde for i dette projekt. Ved en duty cycle på 19,6% får man en gennemsnitlig output spænding til motorerne på 1.764 V (9V*0.196 = 1.764V).

Input 1 og 2 og input 3 og 4, bruges til at åbne og lukke for gates i h-broen, hvilket resulterer i at man kan skifte mellem positive og negative spænding, hvilket bestemmer retning som DC-motorerne kører. Disse gates åbnes og lukkes ved brug af digitalWrite som sender en høj eller lav værdi til en input pin.

I turnLeft metoden fra arduino koden ses hvordan en lav værdi bliver sendt til h-broens input pin 1 og 2 og 4, og en høj værdi til input pin 3. Dette resulterer i at venstre motors positive og negative gates begge er lukket, så den ikke bevæger sig. Mens den højre motors positive gate er høj og positive gate er lav. Derved kører højre motor fremad, mens venstre motor står stille, så robotten drejer.

void turnLeft() {
  digitalWrite(leftForward, LOW);
  digitalWrite(leftBackward, LOW);
  digitalWrite(rightForward, HIGH);
  digitalWrite(rightBackward, LOW);
}

H-broen har 4 output pins. Disse pins bruger inputtet fra de tilhørende input pins til at sende værdierne videre til DC-motorne. Output spændingen til DC-motorne bestemmes ud fra Vcc 2 strømkilden, mens strømmen som h-broen benytter internt kommer fra Vcc 1.

Sammenkobling med arduino

Vcc 1:

Benyttes til strømkilden som bruges til at styre H-broen. Da det er anbefalet at bruge 4.5 V – 7 V, bliver denne koblet til arduinoens 5V udgang.

Enable 1,2:

Enable12 = pinmode(5, OUTPUT);

Arduinoens pin 5 er en digital PWM pin, hvilket er vigtigt da den skal sende PWM signaler.

Input 1:

Input 1 = pinmode(9, OUTPUT);

Forbindes til arduino port 9.

Dette er input til negativ retning til den venstre DC-motor

Input 2:

Input 2 = pinmode(8, OUTPUT);

Forbindes til arduino port 8.

Dette er input til positiv retning til den venstre DC-motor

Enable 3,4:

Enable34 = pinmode(6, OUTPUT);

Skal også være en digital PWM pin

Input 3:

Input 3 = pinmode(11, OUTPUT);

Forbindes til arduino port 11.

Dette er input til negativ retning til den højre DC-motor

Input 4:

Input 4 = pinmode(10, OUTPUT);

Forbindes til arduino port 10.

Dette er input til positiv retning til den højre DC-motor

Vcc 2:

Her vælges strømkilden til DC-motorerne. De kan bruge en spændingskilde på 5V – 9V (http://www.farnell.com/datasheets/1863913.pdf). Men da der kun bliver brugt omkring 20% duty cycle forbindes Vcc 2 til et 9V batteri.

Output 1:

Forbindes til venstre DC-motors negative pin

Output 2:

Forbindes til vestre DC-motors positive pin

Output 3:

Forbindes til højre DC-motors negative pin

Output 4:

Forbindes til højre DC-motors positive pin

GND:

De 4 ground pins forbindes til ground

Disse pins hører til Enable 1,2 og Enable 3,4 og Vcc 1 og Vcc 2.

Ultrasonisk Distance Sensor & Servo-motor

For at give robotten den fleksibilitet og mulighed for at “se omkring” sig benyttes der en servo hvor en ultrasonic sensor er koblet på. Sensoren sender en puls ud og beregner hvor lang tid dette tager for at få reflekteret tilbage, hvilket anvendes både for at komme forbi den første forhindring samt anden del af banen. Afstanden bliver målt i mikrosekunder og for at simulere hvordan det ville være fysisk er tiden omregnet til cm, dette bliver gjort ved at tage højde for at lyd bevæger sig rundt regnet 340 meter i sekundet, dette svarer til omkring 29.412 mikrosekunder per centimeter, så for at udregne hvor langt lyden har bevæget sig frem og tilbage kan der siges afstand = ((tid / lydensHastighed)) / 2), der bliver divideret med 2 fordi det er afstanden det tager for lyden at rejse frem og tilbage. Derfor kan afstanden målt i cm regnes ud ved at sige cm = ((mikrosekund / 2) / 29. Som det så er blevet gjort i arduinoen.

long msToCm(long ms) {
  return (ms / 29 / 2);
}

Som nævnt før, er sensoren monteret på en servo-motor, denne servo anvendes til at give robotten et overblik over hvad afstanden fra venstre til højre er, som vist på billedet nedenfor. 

Servoen bliver forbundet med en plus (rød) som er forbundet til arduinoens 5V udgang, sort (minus) som er forbundet til ground og en signal (grøn) ledning hvilket er forbundet i arduino pin 3 som er en digital PWM pin, hvilket styrer vinklen på servo armen. Dette medfører i at den kan trække 1.1 kg/cm og hvis det skulle være brug for yderligere kraft kan der forbindes op til 6V hvor den kan trække 1.3 kg/cm (http://descargas.cetronic.es/microservo.pdf). 

Servo motoren benytter et library som konvertere et PWM signal til hvor mange grader den skal dreje. Via metoden servo.attach(3) kan librariet bruges til servoen.

I koden betyder dette blot at man skal bruge servo.write(90) til at servo armen peger fremad, servo.write(180) til 180 grader og servo.write(0) til 0 grader. Derved behøver man ikke udregne hvilke pmw værdier der skal skrives, men man kan bare skrive antal grader i stedet (https://dronebotworkshop.com/servo-motors-with-arduino/).

Tinkercad Kode

#include <Servo.h>
//Constants for pins
const int photoLedPin_1 = 13;
const int photoLedPin_2 = 4;

const int photoPin_1 = A0;
const int photoPin_2 = A1;

const int pingPin = 7;

const int enable12 = 5;
const int enable34 = 6;
const int leftForward = 8; 
const int leftBackward = 9; 
const int rightForward = 10; 
const int rightBackward = 11;

int buttonPin = 12;
int buttonState = 0;
int lastButtonState = 0;
int speed = 50;
int count = 0;

//initializing object for servo library
Servo servo;

long duration;
long distance;

void setup() {

//Variable for pushbutton/switch case
  count = 0;  
  
  Serial.begin(9600);
  servo.attach(3);  //sets pin 3 as servo pin
 
//Setting input and output pins
  pinMode(photoPin_1, INPUT);
  pinMode(photoPin_2, INPUT);
  Serial.println(count);

  pinMode(photoLedPin_1, OUTPUT);
  pinMode(photoLedPin_2, OUTPUT);
  
  pinMode(enable12, OUTPUT);
  pinMode(enable34, OUTPUT);
  pinMode(leftForward , OUTPUT);
  pinMode(leftBackward , OUTPUT);
  pinMode(rightForward , OUTPUT);
  pinMode(rightBackward , OUTPUT);
  
  pinMode(buttonPin, INPUT);
  
  servo.write(90);  //sets servo motor to 90 degrees
  delay(500);
}

void loop() {
  //Sets the rotation speed for the DC-motors
  analogWrite(enable12, speed);
  analogWrite(enable34, speed);
  readUltrasonicDistance(pingPin);
  
//Resets the count variable for chosing between following the black line and navigating with distancesensor 
  if(digitalRead(buttonPin) == 1) {
    count += 1;
    if(count % 2 == 0){
      count = 0;
    }
    delay(100);
  }

//Reads the value from the phototransistors
  int leftSensor = analogRead(photoPin_1);
  int rightSensor = analogRead(photoPin_2);
  
  switch (count) {
// case 0 is following the black line
    case 0:
 
    if (leftSensor > rightSensor) {
      digitalWrite(photoLedPin_1, HIGH);
      digitalWrite(photoLedPin_2, LOW);

      turnRight();
      Serial.println(leftSensor + "Turning right");

    } else if (leftSensor < rightSensor) {
      digitalWrite(photoLedPin_1, LOW);
      digitalWrite(photoLedPin_2, HIGH);

      turnLeft();
      Serial.println(rightSensor + "Turning left");
    } else {
      digitalWrite(photoLedPin_1, HIGH);
      digitalWrite(photoLedPin_2, HIGH);
      moveForward();
    }
    break;

    //Case 1 is for navigating with distance sensor
    case 1:

    digitalWrite(photoLedPin_1, LOW);
    digitalWrite(photoLedPin_2, LOW);
    
    if (distance < 250 && distance > 0) {
      stopMoving();
      delay(100);
      moveBackward();
      delay(400);
      stopMoving();
      delay(100);
      
      //look right
      servo.write(180);
      delay(1000);
      long distanceR = readUltrasonicDistance(pingPin);
      Serial.println(distanceR);
      delay(1000);
      servo.write(90);
      delay(1000);
      
      //look left
      servo.write(0);
      delay(1000);  
      long distanceL = readUltrasonicDistance(pingPin);
      Serial.println(distanceL);
      delay(1000);
      servo.write(90);
      delay(1000);
      
      if (distanceR >= distanceL) {
        turnRight();
        delay(1300);
        stopMoving();
        delay(500);
      
      } else {
        
        turnLeft();
        delay(1300);
        stopMoving();
        delay(600);
      }
      
    } else {
      
      moveForward();
      break;
    }
  }
}
 
//Method to read distance sensor
long readUltrasonicDistance(int pingPin) {
  pinMode(pingPin, OUTPUT);  // Clear the trigger
  digitalWrite(pingPin, LOW);
  delayMicroseconds(2);
  // Sets the trigger pin to HIGH state for 10 microseconds
  digitalWrite(pingPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(pingPin, LOW);
  pinMode(pingPin, INPUT);
  // Reads the echo pin, and returns the sound wave travel time in microseconds
  duration = pulseIn(pingPin, HIGH);
  distance = msToCm(duration);
  //Serial.println(distance);
  return distance;
}

void moveForward() {
  digitalWrite(leftForward, HIGH);
  digitalWrite(leftBackward, LOW);
  digitalWrite(rightForward, HIGH);
  digitalWrite(rightBackward, LOW);
}

void stopMoving() {
  digitalWrite(leftForward, LOW);
  digitalWrite(leftBackward, LOW);
  digitalWrite(rightForward, LOW);
  digitalWrite(rightBackward, LOW);
}

void moveBackward() {
  digitalWrite(leftForward, LOW);
  digitalWrite(leftBackward, HIGH);
  digitalWrite(rightForward, LOW);
  digitalWrite(rightBackward, HIGH);
}

void turnLeft() {
  digitalWrite(leftForward, LOW);
  digitalWrite(leftBackward, LOW);
  digitalWrite(rightForward, HIGH);
  digitalWrite(rightBackward, LOW);
}

void turnRight() {
  digitalWrite(leftForward, HIGH);
  digitalWrite(leftBackward, LOW);
  digitalWrite(rightForward, LOW);
  digitalWrite(rightBackward, LOW);
}

long msToCm(long ms) {
  return (ms / 29 / 2);
}

Sammenspil mellem mekanik, elektronik og software

Udover selve robottens krop, hvilket er den blå plade der ses på nedenstående billede, kan robotten deles op i 3 dele: Lyssensorer, hjul og servomotor med distrance sensor

Lyssensorer

Det er ikke helt ligegyldigt hvordan man placerer sensorerne. Hvis de sidder for langt fremme på robotten har den svært ved at komme vinkelret ind på slutningen af den første del af banen. Hvis de til gengæld er placeret for langt tilbage, drejer robotten alt for meget til højre og venstre når den næsten direkte fremad. Afstanden mellem sensorerne betyder også meget. Hvis afstanden er for stor, har robotten svært ved at registrere slutning på den første del af banen. Hvis afstanden er for lille, vil den ofte registrere slutningen på banen, selvom robotten egentligt bare er i gang med et sving.

Lyssensorerne virker ved at de sender et lys ud, hvilket bliver reflekteret tilbage af banens overflade og målt af sensorerne igen. På denne måde måler sensorerne en høj værdi hvis de reflekterer på noget hvidt, og en lavere værdi hvis de reflekterer på noget sort, i dette tilfælde stregen de skal følge.

Sensorerne er tilsluttet til arduinoer via en analog input pin hver. I arduino loopet bliver analogRead brugt til at få værdierne fra sensorerne hver gang loopet kører. Disse værdier bliver bl.a. brugt til at bestemme hvilke vej robotten skal dreje, som når leftsensor måler en større værdi end rightsensor skal den dreje til højre.

Hjul

Som beskrevet ovenover, har sensorernes placering stor betydning for hvordan robotten kører gennem banen, hvilket især skyldes at det er sensorerne som bestemmer hvornår robotten får besked om at dreje. Og dette betyder at placeringen af robottens hjul, ligeledes har stor betydning for robottens vej gennem banen.

I begyndelsen af projektet var hjulene placeret foran sensorerne – Dette medførte dog et par problemer. Først og fremmest betød det, at hjulene nåede at dreje robotten forholdsvis meget før sensorerne gav signal om at robotten skulle dreje den modsatte vej, dette medførte at robotten ville kører i en masse små meget skarpe sving frem ad banen, hvilket gav en langsommere tid, samt større usikkerhed omkring den vinkel hvorpå robotten kom ind i de forskellige sving. Usikkerheden omkring svingende betød, at der var meget stor forskel på hvordan robotten kom gennem banen. Ofte oplevede vi også, at svingende blev så skarpe at robottens sensorer ”mistede grebet” på stregen, da robotten drejede så skarpt at sensorerne begge registrerede den sorte streg samtidig, hvilket resulterede i at robotten kørte af banen.

Disse problemer blev løst ved at flytte hjulene, så de er placeret bag sensorerne. Dette betyder at robotten ikke når at dreje lige så meget før sensorerne registrere stregen igen – hvilket gør at robotten ikke ”vrikker” lige så meget frem ad banen, og derfor kommer ind i svingende med en vinkel som variere meget mindre, end da hjulene var placeret forrest. På den måde sørges der for, at der ikke er for stor variation i hvordan robotten klarer banen, samt fjerner risikoen for at robotten kører af banen.

Hjulene bliver styret af en dc-motor hver, der får strøm gennem en h-bro. H-broen bliver brugt til at bestemme hvilke retning motoren skal dreje i.

I arduino koden er der lavet 5 metoder, som bruges til at bestemme hvilke output-pins der skal sendes signaler fra. De 5 metoder har til ansvar at gøre det muligt for robotten henholdsvis at kører fremad, kører baglæns, dreje til venstre, dreje til højre og stoppe med at kører.

Som det kan ses i videoen af vores TinkerCad, kører vores motorer ned 3200 rpm. For at kunne beregne farten på vores robot skal vi bruge radius på vores hjul, da vi ikke har haft mulighed for at kunne teste hvilke størrelse der ville være passende, har vi valgt at bruge en radius på 2,5 cm, for at kunne give en idé om farten som vores robot ville have.

Formlen for at kunne udregne farten er således: fart = radius * rpm * 0,10472 (http://www.endmemo.com/physics/rpmlinear.php)

For vores robot får vi derfor følgende fart:
0,025m * 3200rpm * 0,10472 = 8,37m/s * 3,6 = 30,1km/t

Servo-motor og Distance sensor

Distance sensoren som er den blå kasse med de to grå arme på, er placeret ovenpå servo-armen som er den gule kapsel. På denne måde kan distance sensoren drejes ved at styre servoen. De farvede firkanter på den orange plade indikere servo-armens grader, henholdvis 0 (lyseblå), 90 (mørkeblå) og 180 (lilla).

Distance sensoren er forbundet til arduinoen i pin 4. Og dens værdi kan dermed læses ved hjælp af pulseIn(4).

Distance sensoren fungerer på den måde at den sender lyd ud i en fremadgående retning, den måler så den tid det tager for lyden at komme tilbage, når den reflekteres på en overflade. På den måde kan man registrere robottens afstand til forhindringerne på banen.

Når servoen skal styres gørs det ved hjælp af metoden servoWrite() med den grad som man vil have den til at pege i, f.eks. Sættes den til at pege mod 90 grader når programmet started ved at kalde metoden servoWrite(90).

Et af de steder distance sensoren anvendes er i banens anden del, hvor den skal finde vej uden at skulle følge en sort streg. Dette gøres ved at robotten kører fremad med servoen pegende mod 90 grader, på samme måde som beskrevet ovenover. Når så distance sensoren registrere en værdi som er større mindre end 250 og større end 0, ved vi at det er fordi den er tæt på en væg. Grunden til at værdien skal være større end 0 er, at distance sensoren returnere 0, når lyden ikke reflekteres, altså når den ikke rammer noget. Når robotten nærmer sig væggen, benyttes servoen til at styre distance sensoren, så den scanner fra 180 grader til 90, og fra 0 til 90 grader i to for loops, som er vist afsnittet om programmets opbygning. Robotten drejer så i den mod venstre, hvis den største værdi distance sensoren registreret var mellem 0 og 90 grader, og til højre hvis den største værdi blev registreret mellem 90 og 180 grader.

Opbygning af arduino koden i Unity

Nedestående er pseudocode af main loopet på arduinoen. Dette er for at give et bedre overblik over koden, da der gøres meget brug af nested if statements. Dette afsnit vil beskrive hvordan man løser de forskellige dele af koden på en overskuelig måde. I slutningen af dette afsnit er den komplette kode.

	//Stop at black lines
 if (leftSensor and rightSensor < maxValue and firstStageFinished = false) 
 	stopMoving 
	firstStageFinished = true		
 else if (leftSensor and rightSensor < maxValue and firstStageFinished = true) 
	stopMoving
	print "Track completed!"

	//Follow black line
 if (leftSensor > rightSensor)
	turnRight
 else if (leftSensor < rightSensor)
	turnLeft
 else	

	//Dodging obstacle
	if (0 < distance < 250 and firstStateFinished = false)
		turnAroundObstacle
		lookIfDodged
		turnBackOnTrack
	//Entering second stage of the track
	else if (250 < distance 350 and firstStateFinished = true)
		lookRightWithDistanceSensor
		lookLeftWithDistanceSensor				 	
		
		//Turn away from walls
	        if (rightDistance > leftDistance)
			turnRight
		else	
			turnLeft
	//end loop with moving forward
	else
		moveForward	

Til at styre motorerne på hjulene skal man bestemme hvilke output pins fra audinoen der skal sende signal til h-broen. Til dette skal man lave de 5 nedenstående metoder.

void moveForward () {
        for (int i = 0; i < maxSpeed; i++) {
            analogWrite (0, i);
            analogWrite (1, i);
            analogWrite (2, 0);
            analogWrite (3, 0);
        }
    }

    void turnLeft () {
        for (int i = 0; i < maxSpeed; i++) {
            analogWrite (0, 0);
            analogWrite (1, i);
            analogWrite (2, i);
        }
    }

    void turnRight () {
        for (int i = 0; i < maxSpeed; i++) {
            analogWrite (0, i);
            analogWrite (1, 0);
            analogWrite (3, i);
        }
    }

    void moveBackward () {
        for (int i = 0; i < maxSpeed; i++) {
            analogWrite (0, 0);
            analogWrite (1, 0);
            analogWrite (2, i);
            analogWrite (3, i);
        }
    }

    void stopMoving () {
        for (int i = maxSpeed; i > 0; i--) {
            analogWrite (0, i);
            analogWrite (1, i);
            analogWrite (2, i);
            analogWrite (3, i);
        }
    }

Pin 0 er fremad på venstre motor, pin 1 er fremad på højre motor, pin 2 er bagud på vestre motor og pin 3 er bagud på højre motor. Til hver metode bliver der brugt et for loop, hvilket bruges til at accelerere op til maxSpeed, som er sat til 50 i arduinoens setUp metode. Dette gøres for at få en blødere opstart. For at gøre det mere blødt kan man indsætte delays i metoderne.

Den første del robotten skal løse er at følge den sorte streg. Til dette benyttes lyssensor værdierne og koden er følgende

 if (leftSensor > rightSensor) {

            turnRight ();
            yield return delay (100);
            stopMoving ();
            yield return delay (50);
        } else if (leftSensor < rightSensor) {

            turnLeft ();
            yield return delay (100);
            stopMoving ();
            yield return delay (50);

        }

Hvis den venstre sensor måler højere end den højre skal robotten dreje til højre for at rette sig selv op. Det modsatte gælder den anden vej rundt. Grunden til der bruges stopMoving er, at hvis robotten kommer for hurtigt ind i svingene er der stor chance for den kører videre i stedet for at dreje.

Til at passere den første udfordring, bruges det nedenstående kode.

if (distance <= 250 && distance > 0 && firstStateFinished == false) {

                moveBackward ();
                yield return delay (1000);
                stopMoving ();
                turnLeft ();
                yield return delay (800);
                moveForward ();
                yield return delay (1000);
                stopMoving ();
                turnRight ();
                yield return delay (700);
                moveForward ();
                yield return delay (250);
                stopMoving ();

                // Look right
                servo.write (180);
                yield return delay (1000);
                distanceR_temp = pulseIn (4);
                servo.write (90);
                yield return delay (1000);
                Debug.Log ("Distance tempR : " + distanceR_temp);
                while (distanceR_temp < 500) {
                    moveForward ();
                    yield return delay (1200);
                    distanceR_temp = pulseIn (4);
                }
                turnRight ();
                yield return delay (750);
                stopMoving ();
                leftSensor = analogRead (5);
                while (leftSensor >= 1023) {
                    yield return delay (100);
                    moveForward ();
                    leftSensor = analogRead (5);
                    yield return delay (100);
                }

                stopMoving ();
                yield return delay (1000);
                turnLeft ();
                yield return delay (1200);
            } 

Måden hvorpå det fungerer er, at når robotten kommer tæt på udfordringen, bakker den en smule og kører mod venstre og derefter en smule mod højre, hvorefter den kigger til højre ved hjælp af sonar-censoren for at se om udfordringen er passeret. Hvis robotten stadig kan se udfordringen kører den fremad, drejer til højre og fortsætter derefter i den retning indtil den venstre censor opfanger den sorte streg igen. Når stregen igen opfanges drejes robotten mod venstre, så den står nogenlunde vinkelret på linjen og kører så videre på banen ved at følge stregen igen ved hjælp af den tidligere forklarede implementering.

Den næste del er delt bestemmer hvad robotten skal gøre når den kommer til slutning af den første del af banen, samt når den rammer målstregen i slutningen af banen.

 if (leftSensor < 1023 && rightSensor < 1023 && firstStateFinished == false) {
            stopMoving ();
            yield return delay (1000);
            firstStateFinished = true;
            moveForward ();
            yield return delay (1000);

            stopMoving ();

        } else if (leftSensor < 1023 && rightSensor < 1023 && firstStateFinished == true) {
            stopMoving ();
            Debug.Log ("Track completed!!");
            yield return delay (5000);
        }

Når robotten lyssensorer begge måler en værdi under 1023 samtidig, vil det sige at robotten har ramt enden af første del af banen og enden af anden del af banen.  firstStateFisnished bliver brugt til at finde ud af hvilken del af banen den er på. Når firstStateFisnished er false stopper robotten og den ved nu at den er igennem den første del af banen, og sætter firstStateFisnished til true. Grunden til moveForward bliver brugt i det if statementet er så den forsætter forbi linjen. I else if statementet ved robotten at den er kommet til slutningen af hele banen og stopper derfor helt med at bevæge sig.

Anden del af banen bliver gennemført med det nedenstående kode som sørger for, at når robotten nærmer sig en af væggene bakker robotten lidt tilbage. Når robotten er bakket tilbage køres der to for-loops som bevæger sonar-censoren så den scanner 180 grader foran robotten. Den højeste værdi som robotten ser til hver side gemmes som henholdsvis newTempL og newTempR.

 else if (distance <= 350 && distance >= 250 && firstStateFinished == true) {
                stopMoving ();
                yield return delay (100);
                moveBackward ();
                yield return delay (400);
                stopMoving ();
                yield return delay (200);

                // Look right
                servo.write (180);
                yield return delay (1200);
                distanceR = pulseIn (4);

                for (int i = 180; i >= 90; i--) {
                    distanceR_temp = pulseIn (4);
                    yield return delay (10);

                    if (distanceR_temp > newTempR) {
                        newTempR = distanceR_temp;
                    }
                    servo.write (i);
                }

                servo.write (90);
                yield return delay (1000);

                // Look left
                servo.write (0);
                yield return delay (1200);
                distanceL = pulseIn (4);

                for (int i = 0; i <= 90; i++) {
                    distanceL_temp = pulseIn (4);
                    yield return delay (10);

                    if (distanceL_temp > newTempL) {
                        newTempL = distanceL_temp;
                    }
                    servo.write (i);
                }

                servo.write (90);
                yield return delay (1000);

De to værdier newTempL og newTempR bruges til at styre hvilken vej robotten skal køre. Da banen er opbygget således at vejen mod mål altid er at følge den længste afstand. Tjekkes der i et if-statement om de scannede værdier viser at der er længst til højre eller venstre, som vist herunder.

 if (newTempR >= newTempL) {
                    turnRight ();
                    yield return delay (1250);
                    stopMoving ();
                    yield return delay (500);
                } else {
                    turnLeft ();
                    yield return delay (1250);
                    stopMoving ();
                    yield return delay (600);
                }

Komplet unity kode

using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;

public class ArduinoMain : MonoBehaviour {
    public Breadboard breadboard;
    public Servo servo;
    public root root;
    int maxSpeed;
    ulong distance;
    bool firstStateFinished = false;

    IEnumerator setup () {
        //Your code goes here:
        maxSpeed = 50;
        servo.write (90);

        //Example of delay:
        Debug.Log ("pre-delay log");
        yield return delay (2000); //2 second delay

        //following region ensures delay-functionality for setup() & loop(). Do not delete, must always be last thing in setup.
        #region PremadeSetup
        yield return StartCoroutine (loop ());;
        #endregion PremadeSetup
    }

    IEnumerator loop () {

        ulong distanceR = 0;
        ulong distanceL = 0;
        ulong distanceR_temp = 0;
        ulong distanceL_temp = 0;
        ulong newTempR = 0;
        ulong newTempL = 0;
        yield return delay (40);

        int rightSensor = analogRead (6);
        int leftSensor = analogRead (5);

        yield return delay (40);

        if (leftSensor < 1023 && rightSensor < 1023 && firstStateFinished == false) {
            stopMoving ();
            yield return delay (1000);
            firstStateFinished = true;
            moveForward ();
            yield return delay (1000);

            stopMoving ();

        } else if (leftSensor < 1023 && rightSensor < 1023 && firstStateFinished == true) {
            stopMoving ();
            Debug.Log ("Track completed!!");
            yield return delay (5000);
        }

        if (leftSensor > rightSensor) {

            turnRight ();
            yield return delay (100);
            stopMoving ();
            yield return delay (50);
        } else if (leftSensor < rightSensor) {

            turnLeft ();
            yield return delay (100);
            stopMoving ();
            yield return delay (50);

        } else {

            if (distance <= 250 && distance > 0 && firstStateFinished == false) {

                moveBackward ();
                yield return delay (1000);
                stopMoving ();
                turnLeft ();
                yield return delay (800);
                moveForward ();
                yield return delay (1000);
                stopMoving ();
                turnRight ();
                yield return delay (700);
                moveForward ();
                yield return delay (250);
                stopMoving ();

                // Look right
                servo.write (180);
                yield return delay (1000);
                distanceR_temp = pulseIn (4);
                servo.write (90);
                yield return delay (1000);
                Debug.Log ("Distance tempR : " + distanceR_temp);
                while (distanceR_temp < 500) {
                    moveForward ();
                    yield return delay (1200);
                    distanceR_temp = pulseIn (4);
                }
                turnRight ();
                yield return delay (750);
                stopMoving ();
                leftSensor = analogRead (5);
                while (leftSensor >= 1023) {
                    yield return delay (100);
                    moveForward ();
                    leftSensor = analogRead (5);
                    yield return delay (100);
                }

                stopMoving ();
                yield return delay (1000);
                turnLeft ();
                yield return delay (1200);
            } 
            else if (distance <= 350 && distance >= 250 && firstStateFinished == true) {
                stopMoving ();
                yield return delay (100);
                moveBackward ();
                yield return delay (400);
                stopMoving ();
                yield return delay (200);

                // Look right
                servo.write (180);
                yield return delay (1200);
                distanceR = pulseIn (4);

                for (int i = 180; i >= 90; i--) {
                    distanceR_temp = pulseIn (4);
                    yield return delay (10);

                    if (distanceR_temp > newTempR) {
                        newTempR = distanceR_temp;
                    }
                    servo.write (i);
                }

                servo.write (90);
                yield return delay (1000);

                // Look left
                servo.write (0);
                yield return delay (1200);
                distanceL = pulseIn (4);

                for (int i = 0; i <= 90; i++) {
                    distanceL_temp = pulseIn (4);
                    yield return delay (10);

                    if (distanceL_temp > newTempL) {
                        newTempL = distanceL_temp;
                    }
                    servo.write (i);
                }

                servo.write (90);
                yield return delay (1000);

                if (newTempR >= newTempL) {
                    turnRight ();
                    yield return delay (1250);
                    stopMoving ();
                    yield return delay (500);
                } else {
                    turnLeft ();
                    yield return delay (1250);
                    stopMoving ();
                    yield return delay (600);
                }
            } else {
                moveForward ();
            }
        }
        distance = pulseIn (4);

        //Following region is implemented as to allow "yield return delay()" to function the same way as one would expect it to on Arduino.
        //It should always be at the end of the loop()-function, and shouldn't be edited.
        #region DoNotDelete
        //Wait for one frame
        yield return new WaitForSeconds (0);
        //New loop():
        yield return loop ();
        #endregion DoNotDelete 
    }

    void moveForward () {
        for (int i = 0; i < maxSpeed; i++) {
            analogWrite (0, i);
            analogWrite (1, i);
            analogWrite (2, 0);
            analogWrite (3, 0);
        }
    }

    void turnLeft () {
        for (int i = 0; i < maxSpeed; i++) {
            analogWrite (0, 0);
            analogWrite (1, i);
            analogWrite (2, i);
        }
    }

    void turnRight () {
        for (int i = 0; i < maxSpeed; i++) {
            analogWrite (0, i);
            analogWrite (1, 0);
            analogWrite (3, i);
        }
    }

    void moveBackward () {
        for (int i = 0; i < maxSpeed; i++) {
            analogWrite (0, 0);
            analogWrite (1, 0);
            analogWrite (2, i);
            analogWrite (3, i);
        }
    }

    void stopMoving () {
        for (int i = maxSpeed; i > 0; i--) {
            analogWrite (0, i);
            analogWrite (1, i);
            analogWrite (2, i);
            analogWrite (3, i);
        }
    }

    #region PremadeDefinitions
    void Start () {
        servo = FindObjectOfType<Servo> ();
        if (servo == null) {
            Debug.Log ("No servo found in the scene. Consider assigning it to 'ArduinoMain.cs' manually.");
        }
        Time.fixedDeltaTime = 0.005f; //4x physics steps of what unity normally does - to improve sensor-performance.
        StartCoroutine (setup ());

    }

    IEnumerator delay (int duration) {
        float durationInSeconds = ((float) duration * 0.001f);
        yield return new WaitForSeconds (durationInSeconds);
    }

    public long map (long s, long a1, long a2, long b1, long b2) {
        return b1 + (s - a1) * (b2 - b1) / (a2 - a1);
    }

    public ulong millis () {
        return (ulong) (Time.timeSinceLevelLoad * 1000f);
    }

    public ulong abs (long x) {
        return (ulong) Mathf.Abs (x);
    }

    public long constrain (long x, long a, long b) {
        return (x < a ? a : (x > b ? b : x));
    }

    #endregion PremadeDefinitions

    #region InterfacingWithBreadboard
    public int analogRead (int pin) {
        return breadboard.analogRead (pin);
    }
    public void analogWrite (int pin, int value) {
        breadboard.analogWrite (pin, value);
    }
    public bool digitalRead (int pin) {
        return breadboard.digitalRead (pin);
    }
    public void digitalWrite (int pin, bool isHigh) {
        breadboard.digitalWrite (pin, isHigh);
    }

    public ulong pulseIn (int pin) {
        return breadboard.pulseIn (pin);
    }
    #endregion InterfacingWithBreadboard
}

Video af kørsel

Konklusion – hvor godt løser robotten opgaven

Det er lykkes at udvikle robotten således at den benytter de forskellige komponenter til at kunne gennemføre banen. Det har kun været nødvendigt at hardcode robottens opførsel for at komme uden om en enkelt udfordring, derudover, kender robotten ikke til banens forløb på forhånd.

Selvom anden del af banen gennemføres uden at være hardcoded i forhold til hvilken vej robotten skal køre, er det nødvendigt at den “rigtige” vej er den med længst afstand fra robotten hver gang den scanner efter en ny vej. Robottens opførsel er blevet implementere på den måde, da vi fandt det gav den højest mulige succesrate på anden halvdel af banen.

Da robotten ikke behøves at kunne gennemføre andre end den udleverede bane, konkluderer vi at robotten klarer opgaven tilfredsstillende og med en tid, som aldrig er i fare for at komme over grænsen på de 3 minutter.

Perspektivering – Hvad kan man gøre for at gøre den bedre?

I forhold til mulige optimeringer af robotten, kiggede vi først og fremmest på robottens hastighed. Vi kørte mange tests med forskellige hastigheder, men da flere af banens udfordringer, især svingende blev kompliceret at løse med en højere hastighed, blev vi enige om, ved at med værdien 50 som den maksimale value for analogWrite metoden opnåede vi den fart, hvorpå robotten kørte bedst igennem banen.
I forhold til farten skal det dog siges, at vores udregninger viser at robotten ville have en en fart på 30,1 km/t, hvilket vi vurderer ville være en alt for høj hastighed, hvis det havde været i den virkelige verden. Dog giver et output på 50 til vores DC-Motorer en fart som vi finder gennemfører den virtuelle bane tilfredsstillende, og vi har derfor valgt at beholde den hastighed.

Udover robottens fart, undersøgte vi også mulighederne for at optimere på måden hvorpå robotten drejer da vi kan se at det primært er i svingende at robotten taber tid. Her forsøgte vi at undgå at stoppe robottens ene hjul og i stedet blot sænke farten på det ene hjul i svingende, denne implementering gav dog problemer med, at robotten af og til kørte af stregen og ud af banen. Implementering kunne lade dog lade sig gøre, hvis vi også sænkede den generelle fart på robotten, men i så fald ville der ikke vindes så meget tid som ønsket og derfor blev denne implementering ikke valgt.

More

GoBot

af Louise Præstiin Jepsen og Victor Størner-Ryberg

Link til video af testkørsel af GoBot i Unity

Robotten formår at gennemføre banen med en gennemsnitstid på omkring 73.66 sekunder. Ud af 3 forsøg har GoBot følgende tider:

  • Tid 1: 73,7 sek.
  • Tid 2: 73.9 sek.
  • Tid 3: 73.4 sek.

Oversigt over banen kan ses nedenfor:

Bane som er blevet brugt til GoBot testkørsel

Opbygning af GoBot (Hardware)

Grundet COVID-19-krisen har vi været nødt til at lave GoBot som en simulation af kredsløbet i Tinkercad (Tinkercad.com). Dvs at robotten ikke har været bygget som et fysisk produkt, og derfor kan der være få mangler hvis der skal bygges en fysisk kopi. Derudover har vi også lavet en simulation af robottens opførelse i Unity Game Engine. 

Tinkercad

GoBot’s opsætning i Tinkercad kan ses via (link: https://www.youtube.com/watch?v=8fgix4iphu4) eller kopieres og afprøves via (link: https://www.tinkercad.com/things/9A5ltFKjVB2)

Til vores opstilling i Tinkercad, brugte vi følgende komponenter: 

Komponenter brugt til GoBot

Komponentliste for GoBot:

  • 1 stk. Arduino-board til input/output
  • 2 stk. DC Motorer til hjul
  • 1 stk. L293D H-bro (forenklet og fylder mindre når der skal bygges)
  • 1 stk. 9V batteri, bruges som strømforsyning til motorer
  • 1 stk. Servomotor, bruges til at holde afstandssensor og evt. rotere sensoren
  • 1 stk. Ultrasonisk afstandssensor (Parallax PING e.l.), bruges til at måle afstand til  omkringliggende forhindringer
  • Ledninger til forbindelse mellem komponenter
  • 1 stk. Breadboard
  • 2 stk. Røde lysdioder (Valgt på baggrund af at udregninger til modstande vises ukorrekte i Tinkercad til visse andre typer lysdioder. f.eks. hvide da programmet ikke kan skelne mellem forskellige typer af lysdioder. Andre lysdioder kan bruges i den fysiske version)
  • 2 stk. Foto modstande, skal bruges til at måle lys fra to lysdioder fastspændt på undersiden af robotten. Hovedformålet er at holde robotten på den sorte linje
  • 2 stk. 10k Ω modstande til foto modstande. (Udregninger for modstande og forklaring findes i Tinkercad afsnittet)
  • 2 stk. 150 Ω modstande til Lysdioder, bruges til at begrænse strømmen til dioderne og dermed forhindre at de brændes af

Komponenter som ikke er inkluderet i Tinkercad, men som burde overvejes i et fysisk produkt: 

  • 4 hjul
  • Lego eller lignende stabile komponenter til at lave et vognstel, som kan bære alle elektroniske og mekaniske dele.
  • 2 stk. KY-033 (Tracking sensor module) som skal erstatte både foto modstande og lysdioder, samt deres tildelte modstande. Denne to i en løsning bruger infrarød lys til at holde robotten på linjen og dermed forbedres robotten ydeevne i miljøer med meget naturligt lys.

Nedenstående billede viser den fuldendte opsætning i Tinkercad. Derefter vil der være  dybere forklaring og udregninger til de forskellige komponenter.

Færdiggjort opsætning i Tinkercad

Arduino mikrokontrolleren fungerer som samlepunkt for alle ledninger, samt alt logik vedrørende sensorer (dvs input og output fra de forskellige komponenter). Derudover bruges 5V pin porten også til at levere strøm til størstedelen af komponenterne. 

Arduino Mikrokontroller

De to DC hobby gear motorer fungerer som drivkraften i robotten. Disse skal selvfølgelig have en hjul fastspændt på enden i det fysiske produkt.  Hastigheden af begge motorer bliver indstillet i koden, samt brug af PWM pins på Arduino mikrokontrolleren.

DC hobby gear motorer

Rotationsretning af hjul, samt levering af strøm til motorerne bliver delvist styret af et mindre integreret kredsløb (L293D). Dette kredsløb fungerer som to mindre H-Broer (en til hver motor), hvilket gør det muligt at drive en belastning i både positiv og negativ retning. 

Hastigheds variabler og andet motor relateret opbevares i Arduinoen.

L293D H-BRO

9V batteriet fungerer som ekstra strømforsyning til begge gearmotorer.  (Strømmen bliver  leveret gennem H-Broen). En ekstra strømforsyning er blevet valg på bagrund af at begge motorer tilsammen dræner en god mængde strøm, og Arduino mikrokontrolleren kan kun levere 5V uden yderligere ændringer. Derfor er en ekstra strømforsyning en enkel løsning til dette problem. 

L293D pins og deres funktionalitet

Et hurtigt overblik over hvad funktionaliteten over de forskellige pins til L293D kan ses på ovenstående billede. Som udgangspunkt er opsætningen meget simpel. Pin 4, 5, 12 og 13 er vores jordforbindelse. Det vil sige de sorte ledninger fra 9V batteriet og Arduino mikrokontrollerens 5V port. Pin 16 modtager 5V fra Arduino mikrokontrolleren, og bruges kun til at give strøm til selve L293D og dens interne dele. Pin 8 derimod modtager strøm fra 9V batteriet og bruges kun til de komponenter (motorer) som L293D er forbundet til. Pin 2, 7 og 10, 15 er input fra Arduinoen til L293D, hvilket skal bruges til at give output til motorerne senere hen. Pin 3, 6, 11, 14 er output fra L293D til motorerne. Hver input pin er parret med den nærmeste output pin og hver gear motor skal have to inputs. De to sidste pins, 1.2EN og 3.4EN bruges til at tænde for enten venstre eller højre side af kredsløbet.  Hovedfunktionen bag disse to pins er at styre hastigheden af motorer via brug af PWM (husk at bruge PWM pins på Arduino mikrokontrolleren),

Hvis man vil have yderligere information om L293D så opfordrer vi at kigge i databladet til komponenten. F.eks. L293-L293D fra Texas Instruments (link: http://www.ti.com/lit/ds/symlink/l293.pdf)

9V batteri som ekstra strømforsyning

Servomotoreren og den ultrasoniske afstandssensor (i dette tilfælde Parallax PING) fungerer tilsammen som robottens øjne. Afstandssensoren udsender højfrekvens lydbølger og venter på at bølgerne bliver reflekteret tilbage. Ved brug af denne interaktion kan man så opfange omkringliggende forhindringer eller objekter i form af varighed mellem udsendelse og modtagelse af lydbølger. Denne varighed kan derefter blive konverteret til meter e.lig. ved brug af lydens hastighed (se eventuelt mere i kode afsnittet for Tinkercad). 

Servomotoren holder afstandssensoren og tillader at skifte hvilken retning sensoren peger, uafhængigt af hvilken vej robotten kører i. 

Ultrasonisk afstandssensor og servomotor

Foto modstandene og lysdioderne gør det som nævnt tidligere muligt for robotten at følge  den sorte linje. Begge disse komponenter skal være spændt fast på undersiden af robotten. Der skal være en lysdiode og en fotomodstand på hver side af robotten (dvs at den sorte streg bliver i centeret af robottens vognstel). Dioderne skal derudover ikke lyse direkte på foto modstandene, men derimod lyse på overfladen som robotten kører på. Det reflekterede lys vil derefter blive opfanget af foto modstandene. Afhængigt af overfladens farve vil der blive reflekteret mere eller mindre lys og dermed vil foto modstandenes værdier skifte. Derefter skal der bare opsættes grænseværdier for hvornår robotten fraviger for meget fra den sorte streg. Som alternativ kan der også bruges en to i en løsning såsom KY-033 (Tracking sensor module).

Foto modstande og røde LED dioder

Til de to røde lysdioder bliver der tildelt en 150 Ω modstand til hver som bruges til at begrænse strømmen til selve dioden og dermed forhindre at den brændes af. Udregninger for valg af disse modstande er baseret på data sheet til 5 mm røde LED lysdioder fra RoHS (link: http://descargas.cetronic.es/WW05A3SRP4-N%20.pdf).

For at udregne modstand værdierne til så bruger vi to værdier fra datasheetet. Disse værdier er “Forward current” og “Forward Voltage”, hvilket henholdsvis er den mængde elektrisk strøm (i milliampere) og spænding (i Volt) som skal bruges for at dioden kan fungere.

Værdier fra datasheet:

Forward Current20mA
Forward Voltage2.0V
Data sheet værdier for røde lysdioder

Nu hvor vi ved spændingsfaldet over dioden så kan vi udregne spændingsfaldet over modstandene ved brug af denne værdi samt outputtet fra vores strømforsyning (ikke 9V batteriet men 5V fra Arduino mikrokontrolleren). Dette gøres ved en simpel beregning:

  • Vres er spændingsfaldet over modstand
  • Vs er spænding fra strømforsyningen
  • Vled er spændingsfaldet over LED dioden

Nu ved vi altså at spændingsfaldet over modstanene skal være 3V. Derudover ved vi at den elektriske strøm der flyder gennem dioderne er den samme i modstandene som dioderne da de sidder i serieforbindelse. Ud fra denne information kan vi så udregne modstanenes værdi ved brug af Ohms lov.

  • R er modstanden (målt i ohm) 
  • U er spændingen (målt i volt)
  • I er strømstyrken (målt i ampere)

Ud fra ovenstående udregning kan vi så bekræfte at begge lysdioder skal bruge en modstand på omkring 150 Ω for at fungere.

Til fotomodstandene bruges der to 10k Ω modstande. Disse modstande skal hver bruges til at lave det man kalder en spændingsdeler. Grunden til at vi bruger en ekstra modstand til vores fotomodstande er at fotomodstande er variable modstande. Dvs. at deres modstandsværdi skifter ud fra forskellige omstændigheder (i dette tilfælde forskellige mængder lys). Arduino mikrokontrolleren har svært ved at måle disse ændringer i modstand, men har lettere ved at måle ændringer i spænding ved brug af det indbyggede system kaldet Analog to Digital Converter. Analog to Digital Converter også kaldet ADC kan konvertere spændingsændringer modtaget fra Analog IN portene på kontrolleren om til binære tal som så kan gemmes i variabler i selve koden. 

Hvis vi prøver at måle spændingsændringer kun med fotomodstanden, så opstår der dog problemer da der ikke er et referencepunkt udover strømforsyningen (5V) og jordforbindelsen til at udregne resultatet. Dog hvis vi indsætter vores valgte modstand på 10k Ω mellem foto modstanden og jordforbindelsen så har vi et fastlagt referencepunkt til at måle spændingsændringer. Dette gør i princippet bare at når lysmængden stiger, så falder modstanden i foto modstanden, hvilket gør at spændingen over vores anden modstand stiger.

Grunden til at vi har valgt præcis 10k Ω modstande er baseret på følgende:

Vi har nu et mindre system som har et spændingsinput, output og to modstande i serieforbindelse. Skulle meget gerne ligne nedenstående billede.

System til måling af spændings output

Med denne information kan vi nu udregne spændings outputtet (Vout) ved brug af formlen:

  • Vin er vores strømforsyning, dvs 5V fra Arduino mikrokontrolleren 
  • R1 er foto modstandens værdi målt i Ω, denne værdi varierer ud fra forskellige mængder lys.
  • R2 er vores 10k Ω modstand
  • Vout er spændings outputtet som sendes tilbage til Arduino mikrokontrolleren

Den varierende maximum og minimumsværdi fra foto modstanden kan findes ved brug af data sheetet som er tilknyttet til modellen. Da Tinkercad ikke har et modelnummer e.lig. så må vi tage udgangspunkt i en selvvalgt model. I dette tilfælde har vi valgt LDR NORP12 fra RS (link: https://components101.com/sites/default/files/component_datasheet/LDR%20Datasheet.pdf).

Denne model har en minimumsværdi på omkring 400 Ω ved fuld lysstyrke og en maksimumsværdi på 9K Ω i omgivelser uden for meget lys. (Absolut maks er selvfølgelig 1 MΩ når der er intet lys til stede, men vi er interesseret i rækkevidden hvor sensoren kan se lys) Med formlen fra tidligere kan vi nu udregne outputtet ved forskellig mængder lys.

Dette giver os en synlig rækkevidde på omkring (4.81 – 2.63) = 2.18 V. Da inputtet bliver læst gennem analogRead() funktionen så ved vi at der maksimalt kan gives værdier mellem 0 og 1023. 1023 vil selvfølgelig være de 5 V total, så det betyder at der går omkring 1 værdi pr 0.005 V. Ud fra dette ved vi at der er følgende antal forskellige mulige værdier ved brug af målbart lys: 

Hvilket burde være en acceptabel rækkevidde. Dog er modellen i selve Tinkercad anderledes og derfor kan dens værdier godt afvige fra udregningerne. Det fastholdes dog stadig at den faste modstand på 10k Ω ikke må ændres til noget der er for højt eller lavt, fordi så risikerer du at få en mulig rækkevidde af målt lys som er for vag. Det betyder altså også at 10k Ω ikke er direkte nødvendig, men mere et udgangspunkt for noget som vi ved virker.  

Koden for alle Tinkercad komponenter kan findes nederst på blogindlægget. Selve koden er dog mest af alt en teoretisk opbygning, og kan derfor ikke bruges direkte til en fysisk robot. På nuværende tidspunkt virker alle sensorer og som hovedformål er de sat op til det som de skal kunne (f.eks. at se en væg eller følge linjen). Specifikke elementer såsom fart, lysmængder og korrekte drejninger er ikke sat op i Tinkercad, men kan findes i Unity afsnittet hvor koden er væsentligt udbygget.  Dette skyldes at der er mange limitationer i Tinkercad som gør det umuligt at replikere et fysisk miljø. (f.eks. at se hvornår robotten er kommet forbi to vægge mm.)

Unity simulation

Før robottens opførelse kunne programmeres, skulle den først bygges virtuelt. Til dette fik vi et Unity projekt som indeholdte følgende Unity objekter: 

  • Linje Sensorer
  • Afstandssensor 
  • Hjul 
  • H-bro_Left
  • H-bro_Right
  • Vognstel 
  • Servomotor 
  • Breadboard

Derudover var der opgivet en bane som robotten skulle kunne gennemføre på 3 minutter. For at gøre vores sammensætning proces mere overskuelig har vi opdelt den i 5 steps. 

Step 1: Servomotor og afstandssensor

For at sammensætte robotten skulle de forskellige komponenter sættes på vognstellet som i Unity var navngivet “CarChassis”. 

Ovenstående billede viser de forskellige Unity objekter som der er i scenen. 

Det første vi satte på robotten var servomotoren, og afstandssensoren. Det er vigtigt at man påsætter afstandssensorer, som et barn af Servomotorens “arm”, da det er armen der bliver roteret. 

Step 2: Hjul

Dernæst påsatte vi vores hjul. Hjulene har et joint komponent som sørger for at de er påsat robotten, og kan roterer, uden at roterer vognstellet.  

Her kan Configurable Joint komponentet ses, samt de forskellige parameter der kan ændres. 

Ved hvert hjul skulle vognstellet angives som “connected body”, under Configurable Joint komponented.

Step 3: H-broer

Hver H-bro blev påsat vognstellet hvorefter, man skulle angive hvilken motor den hørte til.  

Dette billede viser hvor motoren skal refereres til i Unity editoren. 

Step 4: Linjesensor 

Linjesensorerne skulle også sættes på robottens vognstel, og de placeres under robotten. 

Step 5: Breadboard

Breadboardet bruges til at angive hvilke pins de forskellige dele er sat til. Det skal også bare sættes på robotten, og derefter skal hvert pin angives.

Dette er et eksempel på hvordan man kan referer til de robottens andre dele i breadboardet komponentet. 

Derudover indeholder breadboarded også vores ArduinoMain script, hvilket vi bruger til at kode robottens opførelse med. 

Placering af komponenterne 

Det er vigtigt at nævne at placeringen af de forskellige sensorer har stor betydning for robottens datainput. Hvis servormotoren og afstandssensoren placeres bag på bilen, ændre det selvfølgelig afstanden den har fra en mur. Det kan dog være en fordel, da den kan opfange hvornår robotten er passeret forbi en mur, når den drejer. Derfor skal der enten tages højde for bilens længde når en afstands måles, eller bilens længde når den forbipassere en mur. 

Ovenstående billede viser bud på forskellige placeringer af servomotoren og distancesensoren. 

Linjesensorernes placering i forhold til hinanden har også stor betydning når robotten skal følge en streg. Hvis sensorerne er tættere på hinanden får den et mere præcis input, og bilen ville dreje skarpere da den opfanger en ændring meget hurtigere. Dette kan dog skabe problemer hvis bilen kører for stærkt, da der den har et mindre areal at måle på. 

Dette viser et billede af linjesensorerne med forskellig afstand mellem hinanden.

Hvis linje sensorerne skal placeres langt fra hinanden skal de eventuel kodes, så den ikke drejer for skarpt når den rammer en kant. Dette kunne gøres ved at give den en grænseværdi, når den skal dreje eller kører ligeud. Ud over dette betyder linjesensorernes placering på robotten også meget for dens opførelse. Hvis de sidder forrest registrere den banen meget mere præcis, hvilket gør den drejer bilen med det samme der er et sving. Hvis den er bagerst registrerer den inputtet senere, hvilket gør bilen drejer meget sent. Dette kan gøre at bilen drejer for sent og kører af sporet. 

Hjulene påvirker også hvor robottens opførelse, da deres midtpunkt udgør robottens omdrejningspunkt. Hvis de derfor flyttes skal man være opmærksom på at måden robotten drejer på også ændres. 

Vores placering 

Vi valgte at placere vores afstandssensor forrest, da det giver en mere præcis idé om hvor tæt robotten er på at støde ind i noget. For at sikre sig at robotten havde forbipasseret en mur blev der bare tilføjet et delay, hvor kører lidt frem. Hvilket svarer til størrelsen på bilen. 

Ovenstående billede er den opstilling vi brugte til at løse opgaven. 

Derudover placerede vi vores linjesensorer nogenlunde i midten af bilen, med lidt mindre afstand fra hinanden end den var fra start. Dette var grundet en masse trial and error, hvor vi fandt frem til det der var mest optimal for vores metode. Vi placerede også vores hjul i midten af bilen, for at gøre omdrejningspunktet centreret, da det gør det nemmere at arbejde med. 

Unity kode – programmeringsvejledning

Robotten er programmeret til at følge den sorte streg, indtil den kommer til en mur. Hvis der er en mur, skal den tjekke hvorvidt den er på stregen. Hvis den er på stregen sættes en bool til true, som fortæller robotten at den er ved den første mur.

Udfordringen ved den første mur er bl.a. at robotten kommer opfanger muren lige efter et skarpt sving. Robottens distance sensor ville derfor opfange muren mens der er roteret. Dette kan løses ved enten at få robotten til at bakke og rette sin position før den drejer, eller få den til at dreje skarpere end hvis vinklen var ret. Derudover skal robotten også tilbage på stregen igen, efter den er kommet forbi muren. Hvorimod den sidste mur skal den bare kører lige ud til mål.  Derfor valgte vi at lave to forskellige handlinger ud fra hvilken mur den støder på. 

Flowchart over robottens opførelse 

Dette viser et flowchart over robottens opførelse, i forhold til Unity projektet. Flowchartet blev brugt som udgangspunkt for at kode selve robotten. 

Linje sensor – Unity kode

Vores linjesensor blev anvendt ud fra “Braitenberg vehicle” konceptet, som går på at ændre et køretøjs/robots adfærd ud fra dens sensor input. Dette blev gjort ved at lave et par if-statements som kontrollerede inputtet fra vores linjesensor. 

Når robotten skulle kører ligeud på stregen, blev der lavet en if-statement som tjekkede differencen mellem de to linjesensor inputs. Hvis difference var mindre en bestemt variable fik robotten lov til at kører ligeud. Dette sikrede os at robotten ikke hele tiden ville rette sig efter små differencer mellem linjesensorerne.  

Hvis den ene linjesensors input var højere end den anden, var der en if-statement der fik robotten til at dreje, så værdien ca. ville være ens igen. 

Afstandssensor – Unity kode 

Vores afstandssensor blev brugt til at måle distancen mellem robotten og en mur. Hvis robotten kom inden for en vis distance og den ikke var foran en mur allerede, skal den sætte den bool der fortæller om den er foran en mur til sand. Når den så er forbi muren bliver samme bool sat til falsk. På denne måde kan koden gentages, hvis den skulle støde på en mur igen. 

Helt konkret er robotten programmeret så når den støder på en mur får den følgende kommandoer: 

  • Sæt en bool (kaldet “wall”) til true som viser den er foran en mur 
  • drej mod venstre (i et bestemt antal sekunder som ca. svarer til 90 grader) 
  • kør lidt frem (ca. længden af bilen) 
  • Drej servomotoren (mod murens retning/90 grader) 
  • kør ligeud

Derefter har vi en if-statement som tjekker om den er forbi muren og om “wall” boolen er sand. Hvis dette er tilfældet skal den gøre følgende: 

  • kør lidt frem (ca. længden af bilen) 
  • Drej mod højre 
  • kør lidt frem igen (ca. længden af bilen) 
  • tilføj +1 til en variable (kaldet “WallCount”), som tjekker hvor langt du er om væggen

Derefter har vi to if-statements den kun kan komme ind i når den har kørt ovenstående kode igennem 2 gange, da “Wallcount” skal være lig 2. På denne måde sikre vi os at den kører højre om muren 2 gange. Når den er rundt om muren tjekker den om “Wallcount == 2” og om vi er ved første mur. Hvis vi er ved første mur skal den vende sig mod venstre, sætte servomotorens rotation til ligeud, og sætte vores bool der fortæller om der er en mur eller ej til falsk.  Hvis den er ved sidste mur skal det samme, ud over den ikke skal dreje til venstre.

Konklusion

I forhold til Unity simuleringen, formår vores robot at gennemføre den valgte bane med en acceptabel tid. Den undgår diverse forhindringer og afviger ikke fra de angivne striber. Vores Tinkercad kredsløb fungerer også som det skal, uden at have nogle direkte mangler (udover kode til drejninger). Hvis robotten skulle fremstilles fysisk skal der dog ændres i både opstillingen af vores komponenter og kode. Vores Unity projekt og Tinkercad opstilling kan derfor kun bruges som referencemateriale til en fysisk model og ikke direkte. 

Perspektivering

I forhold til Unity er GoBot på nuværende tidspunkt kodet specifikt til den fremviste bane. Den kan  med stor sandsynlighed gennemføre lignende baner. Den ville dog ikke kunne komme i mål hvis banen f.eks. krævede at den skulle dreje højre om en mur, da den altid drejer venstre om. Dette kunne eventuelt løses ved at skrive mere dynamisk kode som f.eks. får robotten til at undersøge forholdene omkring sig før den drejer om en væg. 

For at optimere denne løsning kunne man også give GoBot nogle flere sensorer og ændre koden en del så den har mere information at arbejde med. F.eks. en eller flere Ultrasonisk afstandssensorer som opfanger indkommende vægge i de blinde vinkler som robotten har på nuværende tidspunkt. Hvis robotten eksempelvis drejede om en mur, og servomotoren var drejet mod muren, ville den ikke kunne detektere hvis den kørte mod endnu en mur.  

Derudover ville det være en god idé at bygge en fysisk kopi af GoBot, da den i princippet kun virker teoretisk i Unity. Mindre fejl eller mangler i programmet kan gøre at den giver forkerte resultater, og derfor burde den også testes i et fysisk miljø. 

I forhold til Tinkercad ville det være oplagt at udbytte både foto modstande og lysdioder for et simplificeret “Tracking sensor module” såsom KY-033. Hvilket ville gøre robotten mere kompakt. 

Tinkercad kode:

//Motors and photo resistors with the number 1 is the right side of the robot
//Motors and photo resistors with the number 2 is the left side of the robot
//Most values, such as speeds, light threshholds and variables for if statements,are not finalized and need to be readjusted for physical product. 
//Define all pins for motor 1
#define motorSpeedPin1 11
#define forwardPin1 10
#define reversePin1 9

//Define all pins for motor 2
#define motorSpeedPin2 5
#define forwardPin2 4
#define reversePin2 3

//Define pin and values for ultrasonic distance sensor
#define pingPin 7
long pingDuration; //full duration for ping to return to sensor
long cmToWall; //centimeters to closests object

//Include outside libary for servo and create servo object + servo posistion variable
#include <Servo.h>
Servo myservo;
int pos = 0;

//Define pins and variables for photoresistors
#define photo1 A0
#define photo2 A1
int photo1Value = 0;
int photo2Value = 0;

void setup() //Basic setup of all pins + console
{
   //Setup for motor 1 pins as output
   pinMode(motorSpeedPin1, OUTPUT);
   pinMode(forwardPin1, OUTPUT);
   pinMode(reversePin1, OUTPUT);
  
   //Setup for motor 2 pins as output
   pinMode(motorSpeedPin2, OUTPUT);
   pinMode(forwardPin2, OUTPUT);
   pinMode(reversePin2, OUTPUT);
  
   myservo.attach(6); //Attaches servo connected to pin 6 to the servo object
     
   Serial.begin(9600);  /* just for debugging console */
}

void loop() //Main loop
{
  photo1Value = analogRead(photo1); //read value from photoresistor
  photo2Value = analogRead(photo2); 
  
  UltrasonicDistanceSensor(); //Using the function to detect and measure distance to incoming walls
  
  /*The code shown below is just a shortened showcase/concept of how the robot is supposed to work. This means that it misses crucial parts, such as 
  swinging around obstacles, detecting second wall and more. See Unity code if you want specific information about the loop.*/
  
  if (photo1Value > 900 && photo2Value > 900) //default behavior, robot follows line by driving forward
  {
      M1spinForward (); //spin right motor
  	  M2spinForward (); //spin left motor
  }
  else if (photo1Value <= 900) //If right photoresistor receives less light (meaning it is on the black line). Then readjust by spinning only the left motor.
  {
      M2spinForward (); //spin left motor
      M1Stop(); //stop right motor from spinning
  }
   else if (photo2Value <= 900) //Same procedure but just for the left photoresistor instead.
  {
      M1spinForward (); //spin right motor
      M2Stop(); //stop left motor from spinning
  }
   
  if (cmToWall < 100) //Stop the robot if there is a close wall and do the turn, turn not implemented
  {
    M1Stop(); //stops robot
    M2Stop(); //stops robot
    
    //Code to turn the robot around walls should be implemented here, check Unity code for more info.
    
    myservo.write(180); //Spin servo right to make ultra sonic distance sensor face wall while turning
  }
  else
  {
  	myservo.write(93); //Reset servo posistion to make ultra sonic distance sensor face walls in front of it.
  }

  delay(100); //small delay
}

//Spin motor 1 forward
void M1spinForward () {
  digitalWrite(forwardPin1, HIGH);
  digitalWrite(reversePin1, LOW);
  analogWrite(motorSpeedPin1, 10);
}

//Spin motor 2 forward
void M2spinForward () {
  digitalWrite(forwardPin2, HIGH);
  digitalWrite(reversePin2, LOW);
  analogWrite(motorSpeedPin2, 10);
}

//Stop motor 1 from spinning
void M1Stop() {
  digitalWrite(forwardPin1, LOW);
  digitalWrite(reversePin1, LOW);
}

//Stop motor 2 from spinning
void M2Stop() {
  digitalWrite(forwardPin2, LOW);
  digitalWrite(reversePin2, LOW);
}

//Find range using Ultrasonic Distance Sensor
void UltrasonicDistanceSensor()
{
  pinMode(pingPin, OUTPUT); //set ping pin as output for ping
  digitalWrite(pingPin, LOW); //Start with a short low pulse to ensure clean high pulse
  delayMicroseconds(2);
  
  digitalWrite(pingPin, HIGH); //High pulse for ping
  delayMicroseconds(5);
  digitalWrite(pingPin, LOW); //reset
  
  pinMode(pingPin, INPUT); //Ping is read using the same pin, pulse is caught in microseconds. 
  pingDuration = pulseIn(pingPin, HIGH); //Duration caught
  cmToWall = microsecToCm(pingDuration); //Convert the time into a distance with this function
 // Serial.println(cmToWall);  //Check distance as a print
}

//Conversion function used in measuring distance with the ultrasonic sensor
long microsecToCm(long microsec) { 
   microsec = microsec / 29; //To convert duration to distance we use the speed of sound. Which is 340 m/s (in microseconds per centimeter is about 29 μs/cm)
   microsec = microsec / 2; //ping duration is measured traveling out and back. To find the distance to the object we take half of it.
return microsec;
}

//---------------- NOT USED IN TINKERCAD BUT IN UNITY ------------------- //

//Spin motor 1 backwards(not used)
void M1spinReverse () {
  digitalWrite(forwardPin1, LOW);
  digitalWrite(reversePin1, HIGH);
  analogWrite(motorSpeedPin1, 10);
}

//Spin motor 2 backwards(not used)
void M2spinReverse () {
  digitalWrite(forwardPin2, LOW);
  digitalWrite(reversePin2, HIGH);
  analogWrite(motorSpeedPin2, 10);
}

Unity kode:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;

public class ArduinoMain : MonoBehaviour
{
    public Breadboard breadboard;
    public Servo servo;

    
    ulong distance;     //afstand som afstandssensoren måler
    bool wall;          //Om den er på vej rundt om en væg
    int WallCount;      //Hvor langt den er rundt om en væg    (  0 = bagved væg      1 = venstre om væg     2 = foran væg ) 


    bool online;        //Om robotten er på linjen 
    bool wallOne;       //Om det er første væg
    bool almostGoal;    //Om den næsten er i mål


    int speed = 80;
    int turnSpeed = 40;



    IEnumerator setup()
    {
        //Your code goes here:
        //Example of delay:
        Debug.Log("pre-delay log");

        yield return delay(2000); //2 second delay

        //following region ensures delay-functionality for setup() & loop(). Do not delete, must always be last thing in setup.
        #region PremadeSetup
        yield return StartCoroutine(loop()); ;
        #endregion PremadeSetup
    }

    IEnumerator loop()
    {
        //Your code goes here:
 
        //få værdier 
        distance = pulseIn(4) / 100;
        int Line1= analogRead(5);
        int Line2 = analogRead(6);

     
        // --- Linjesensor --- 

        //kør ligeud hvis differencen mellem linje 1 og 2 ikke er så stor 
        if (Line1 - Line2 < 400 && wall == false)
        {
            analogWrite(1, speed);
            analogWrite(0, speed);
        }

        //drej til højre hvis den ene value er for høj
        if (Line1 < Line2 && wall == false)
        {
            analogWrite(0, turnSpeed);
            analogWrite(1, 0);
        }

        //drej til venstre hvis den anden value er for høj
        if (Line1 > Line2  && wall == false)
        {
            analogWrite(0, 0);
            analogWrite(1, turnSpeed);
        }

        //tjek om den ikke er på linjen 
        if (Line1 > 0 || Line2 > 0)
            online = true;

        //tjek om den ikke er på linjen 
        if(Line1 == 1023 && Line2 == 1023)
            online = false;


        //tjek om den støder på en linje
        //dette gøres for at den kører lidt mere ligeud ved når linjen stopper 
        if (Line1 < 200 && Line2 < 200)
        {
            analogWrite(1, speed);
            analogWrite(0, speed);
            yield return delay(200);
        }


        // --- Distance Sensor --- 

        //Hvis den støder på en mur skal den begynde at dreje om muren
        if (distance >= 1 && distance <= 2  && wall == false)
        {
            wall = true;

            //tjekker om den er på linjen eller ej 
            //hvis den er på linjen betyder det den er ved første mur 
            if (online == true)
                wallOne = true;
            else
                wallOne = false;

            //hvis den er ved første mur skal den dreje mere end anden mur (da den kommer ind fra en vinkel) 
            if(wallOne == true)
            {
                StartCoroutine("Venstre", 1450); //angiver delayed den skal vente 
                yield return delay(1700);
            }

            else
            {
                StartCoroutine("Venstre", 1250);
                yield return delay(1500);
            }

            analogWrite(0, speed);
            analogWrite(1, speed);
        }


        if (distance > 15 && wall == true || distance == 0 && wall == true)
        {
            //kør lidt frem så hele robotten er forbi muren
            analogWrite(0, speed);
            analogWrite(1, speed);
            yield return delay(700);

            StartCoroutine("Højre");
            yield return delay(1500);

            analogWrite(0, speed);
            analogWrite(1, speed);
            yield return delay(850); 
            WallCount++;

            //hvis det er første væg og det er sidste sving, skal den dreje 
            if (wallOne == true && WallCount== 2)
            {
                StartCoroutine("Venstre", 1300);
                yield return delay(1500);
                servo.write(0);
                yield return delay(200);

                wall = false; //den skal ikke længere forbi en mur
            }

            //hvis det ikke er første væg og det er sidste sving, skal den køre ligeud 
            if (wallOne == false && WallCount == 2)
            {
                servo.write(0);
                yield return delay(1000);

                analogWrite(0, speed);
                analogWrite(1, speed);

                almostGoal = true; //den er næsten i mål 
                wall = false; //den skal ikke længere forbi en mur
            }
        }

        // -- Stop når du når i mål 
        if (almostGoal == true && online == true)
        {
            analogWrite(0, 0);
            analogWrite(1, 0);
        }

        #region DoNotDelete
        //Wait for one frame
        yield return new WaitForSeconds(0);
        //New loop():
        yield return loop();
        #endregion DoNotDelete 
    }
  
    IEnumerator Venstre(int timeDelay)
    {
        //drej mod venstre
        analogWrite(0, 0);
        analogWrite(1, turnSpeed);
        analogWrite(2, turnSpeed);
        analogWrite(3, 0);
        servo.write(90);
        yield return delay(timeDelay); // 1500 for meget og 1200 for fint

        //stop alt
        analogWrite(1, 0);
        analogWrite(2, 0);
        yield return delay(100);
    }

    IEnumerator Højre()
    {

        //drej mod højre
        analogWrite(0, turnSpeed);
        analogWrite(1, 0);
        analogWrite(2, 0);
        analogWrite(3, turnSpeed);
        servo.write(90);
        yield return delay(1250); //1200

        //stop alt
        analogWrite(0, 0);
        analogWrite(3, 0);
        yield return delay(100);
    }


    #region PremadeDefinitions
    void Start()
    {
        servo = FindObjectOfType<Servo>();
        if (servo == null)
        {
            Debug.Log("No servo found in the scene. Consider assigning it to 'ArduinoMain.cs' manually.");
        }
        Time.fixedDeltaTime = 0.005f; //4x physics steps of what unity normally does - to improve sensor-performance.
        StartCoroutine(setup());

    }

    IEnumerator delay(int _durationInMilliseconds)
    {
        float durationInSeconds = ((float)_durationInMilliseconds * 0.001f);
        yield return new WaitForSeconds(durationInSeconds);
    }

    public long map(long s, long a1, long a2, long b1, long b2)
    {
        return b1 + (s - a1) * (b2 - b1) / (a2 - a1);
    }

    public ulong millis()
    {
        return (ulong)(Time.timeSinceLevelLoad * 1000f);
    }

    public ulong abs(long x)
    {
        return (ulong)Mathf.Abs(x);
    }

    public long constrain(long x, long a, long b)
    {
        return (x < a ? a : (x > b ? b : x));
    }


    #endregion PremadeDefinitions

    #region InterfacingWithBreadboard
    public int analogRead(int pin)
    {
        return breadboard.analogRead(pin);
    }
    public void analogWrite(int pin, int value)
    {
        breadboard.analogWrite(pin, value);
    }
    public bool digitalRead(int pin)
    {
        return breadboard.digitalRead(pin);
    }
    public void digitalWrite(int pin, bool isHigh)
    {
        breadboard.digitalWrite(pin, isHigh);
    }

    public ulong pulseIn(int pin)
    {
        return breadboard.pulseIn(pin);
    }

    #endregion InterfacingWithBreadboard
}

More

Claptrap – selvkørende robot

Af Morten S. Jensen (mojen17) og Benjamin Parbst (bepar17)

Dette indlæg vil vise og beskrive vores udvikling af den selvkørende robot vi har lavet i forbindele med vores Portfolieopgave 2 i hardware og robot teknologi.

Opbygning af hardware:

Kredsløbet i TinkerCad er bygget op efter to forskellige tilstande, “line tracking” eller “blind tracking”. Line tracking er den tilstand robotten er i, når den følger en streg og dermed anvender lys-sensorer som primært input. Blind tracking, er når der ikke er en streg at følge, hvorved robotten må finde vej med sensor input fra ultralydssensoren.

Sensorer og servo-kredsløb:

Lyssensorer er bygget op af en hvid LED og en lys modstand. Når den hvide LED projektere lys ned på overfladen vil den reflekterede lyskilde, som opfanges af lys modstanden, variere baseret på overfladens farve. Jo mørkere, jo mindre refleksion og jo lysere, jo mere refleksion. Der er to af disse lys-sensorer og baseret på afvigelsen mellem højre og venstre lyssensor, drejer robotten i varierende grad ved at ændre hastigheden på de to hjul uafhængigt af hinanden.

Ultralydssensoren virker ved at sende en lydimpuls ud og så opfange den tilbagevendende lydrefleksion med en lyssensor. Denne sensor bruges i blind tracking tilstanden, og baseret på hvor tæt på en mur robotten er, ændres køreegenskaberne på robotten. Vi har i tinkercad sat robotten til at stoppe hvis den er en halv meter fra en mur. Derudover drejes servoen, således at ultralydssensoren kan opfange et nyt lydinput. Ultralydssensoren skal nemlig placeres på servoen. Når robotten kører ligeud, peger denne ultralydssensor også ligeud.

Servoen bruges til at dreje ultralydssensoren med for at sensoren kan få måle input i en 180 graders vinkel foran robotten. I tinkercad drejes denne servomotor rundt, hvis ultralydssensoren måler en distance, som er mindre end den accepterede distance til en forhindring.

Figur 2

Kredsløbet for den hvide LED[7] fra lyssensoren ses øverst til venstre på figur 2. Den skal bruge en spænding mellem 3 og 3.6V og har en maksimal forward current på 30mA så formodstanden til LED’erne er beregnet til 100 ohm, for at nå en spænding på 3V:

R = 3.0V/0.03A = 100 Ohm

Kredsløbet til lysmodstanden, der opfanger LED’ens refleksion ses øverst til højre på figur 2. Lysmodstanden her tager udgangspunkt i den udleverede lysmodstand i lab-timerne, som ved svag belysning ligger på 50 ohm, og ved høj belysning, 55K Ohm. Vi har placeret 150 en ohm modstand i serie med denne, så at vi kan måle spændningsforskellen over lysmodstanden med arduinoen. Arduinoen er tilkoblet kredsløbet med analog in pins mellem lysmodstanden og modstanden. Der suppleres strøm til lyssensorkredsløbet fra arduinoen.

Ultralydssensoren[4] ses nederst til venstre på figur 2 og får strøm fra arduinoen, fordi den kun skal bruge 30 mA, hvilket arduinoen godt kan levere. GND pin er tilkoblet et fælles ground og SIG pin er tilkoblet en digital pin på arduinoen, som skiftevis kan input og output signal til denne pin for at sende og modtage lyd med sensoren.

Servoen[5] ses nederst til højre på figur 2, og denne er tilkoblet en ekstern strømforsyning, fordi dens maksimum strømtræk er 140mA +/- 50mA, hvilket arduinoen ikke kan levere. Servoen skal bruge mellem 4 og 6 volt på stromøforsynings pin’en, og derfor er 3 AA 1.5v batterier i serie tilkoblet denne. Grunden til at det er 3, er fordi batterierne ved maksimal kapacitet kan outputte 1.8 v, og for ikke at overskride de 6 volt, som servoen kan håndtere, så bruges 3 så batterierne til at starte med levere 5.4v, som over tid går mod 4.5v, som også er over de 4v, som servoen minimum skal bruge. Signal pin’en på servoen er tilkoblet en digital pin på arduinoen, som styrer servoen via arduinos servo library og GND pin’en er tilkoblet et fælles ground for arduinoen og ground på batterierne.

Motorkredsløbet

De to motorer er koblet til et H-bro kredsløb med 9v strømforsyning til hver motor, som vil tilsvare den udleverede Lego motor fra PF1. Robotten drejer ved at outputte forskellige spændinger til de to motorer, baseret på et input. Disse motorer er placeret bagerst på robotten i unity således at robotten kan dreje mest muligt.

Figur 3

Motorkredsløbets kan ses på figur 3 og dets komponenter består af en L293D H-bro[2], 2 stk PF-large lego motorer[3] og 6 stk 9v batterier[1]. Vi har valgt at sætte 6stk 9v batterier i parallel, fordi at PF-large lego motoren trækker op mod 450 mA, hvilket et 9v batteri ikke levere. Det 9v batteri, som vi har valgt til kredsløbet, har en maksimal afladningsstrøm på 150 mA, hvilket betyder at vi skal have 2 stk motor * 450 mA / 150 mA pr batteri = 6 batterier for ikke at overbelaste nogle af batterierne. Disse batterier er derefter parallelt tilkoblet VCC1 og VCC2, for at fordele spændingen mellem disse to. VCC1 og VCC2 har maksimalt input på 36V, men den anbefalede spænding på VCC1 er mellem 4.5v og 7v.

DC motorene er tilkoblet output pin’s 1-4Y, og retningen motorene bestemmes af input pins 1-4A, som er tilkoblet arduinoes digitale pin’s. 1,2EN og 3,4EN bruges til at styre hastigheden af henholdsvis højre og venstre dc motor og disse er også tilkoblet arduinoens digitale pin’s. Ground pin’en er tilkoblet et fælles ground for batterierne og arduinoen.

Arduinoen skal også bruge strøm, og dette kunne suppleres med en parallel forbindelse til de batterier, som levere strøm til servo-moteren.

Komponentliste:

Figur 4

På figur 4 ses de komponenter, som er brugt i TinkerCAD. Der er ikke så meget fleksibilitet i TinkerCAD i forhold til komponenters specificering generelt, så dimensionering passer ikke 100% overens med tinkerCAD og kredsløbet, som det havde været samlet fysisk. De komponenter, som vi ville have samlet kredsløbet med fysisk består af:

2 stk DC motor (Lego Power Functions Large Motor)[3]:

  • Max spænding: 9v
  • Stall current: 450 mA

2 stk Lysmodstande:

  • Høj belysning: 50 ohm
  • Svag belysning: 55 kOhm

1 stk L293D H-bro[2]

1 stk Arduino Uno

2 stk 100 Ohm modstande

2 stk 150 Ohm modstande

1 stk Parallex Ping ultralydssensor[4]

1 stk Parallex Standard Servo[5]

6 stk 9v genopladelige batterier[1]

3 stk 1.5v genopladelige batterier[6]

Kode:

Dette er koden fra tinkerCAD, den har en implementering af de to overordnede adfærder, LINE_TRACKING OG BLIND_TRACKING, som vi anvender i Unity. Adfærden kan skiftes ved at sætte state variablen til at være enten LINE_TRACKING eller BLIND_TRACKING i setup metoden.

#include <Servo.h>

#define LEFT_LED 2
#define RIGHT_LED 3
#define LEFT_PHOTO_SENSOR 4
#define RIGHT_PHOTO_SENSOR 5

#define LEFT_FORWARD 6
#define LEFT_BACKWARD 7
#define RIGHT_FORWARD 8
#define RIGHT_BACKWARD 9

#define LEFT_MOTOR_CONTROL 10
#define RIGHT_MOTOR_CONTROL 11

#define SERVO_CONTROL 12
#define DISTANCE_SENSOR 13

#define LEFT_PHOTO_SENSOR_INPUT A0
#define RIGHT_PHOTO_SENSOR_INPUT A1

enum States {
	LINE_TRACKING, BLIND_TRACKING
};

// cycle update rate
int cycleTime = 20;

// the states that the robot is in for evaluation in real-time
States state;

// photo sensor value readings
int leftSensorReading = 0;
int rightSensorReading = 0;
int distanceSensorReading = 0;

// the maximum speed of the robot, which constraints the real-time speed
int maxSpeed = 120;
int speed = 0;
int cruisingSpeed = 200;

// the degree of turning on a scale from 0 to 1, 0.5 being straight forward
double degreeOfTurning = 0.5;

double sensorMean = 0;
double sensorDeviation = 0;

int servoAngle = 90;
Servo servo;

long cmFromObstacle = 100;

/**
* Accelerates the car to the given speed limit in the argument. There is a gradual
* acceleration or decceleration to the given speed limit and speed is slowed down
* in turns based on the sharpness of the turn.
*/
void accelerate(int speedLimit, bool forward) {
  // increases speed if max speed is not achieved yet
  speed = speed < speedLimit ? speed + 1 : speed - 1;

  // calculates the sharpness of turns on a scale from 0 to 1
  double speedTurningDecrease = (degreeOfTurning - 0.5) / 0.5;
  speedTurningDecrease = speedTurningDecrease < 0 ? speedTurningDecrease * -1 : speedTurningDecrease;

  // factor to decrease speed by up to 80% based on the sharpness of the turn
  double speedDecreaseFactor = 1 - (speedTurningDecrease * 0.8);

  if (forward) {
    // makes sure the H-bridge does not have input on the opposite direction
    digitalWrite(LEFT_BACKWARD, LOW);
    digitalWrite(RIGHT_BACKWARD, LOW);
    
    digitalWrite(LEFT_FORWARD, HIGH);
    digitalWrite(RIGHT_FORWARD, HIGH);

    // outputs speed to the dc motor actuator
    analogWrite(LEFT_MOTOR_CONTROL, 2 * (int) ((speed * (1 - degreeOfTurning)) * speedDecreaseFactor));
    analogWrite(RIGHT_MOTOR_CONTROL, 2 * (int) ((speed * degreeOfTurning) * speedDecreaseFactor));
  } else {
    // makes sure the H-bridge does not have input on the opposite direction
    digitalWrite(LEFT_FORWARD, LOW);
    digitalWrite(RIGHT_FORWARD, LOW);
    
    digitalWrite(LEFT_BACKWARD, HIGH);
    digitalWrite(RIGHT_BACKWARD, HIGH);

    // outputs speed to the dc motor actuator
    analogWrite(LEFT_MOTOR_CONTROL, 2 * (int) ((speed * (1 - degreeOfTurning)) * speedDecreaseFactor));
    analogWrite(RIGHT_MOTOR_CONTROL, 2 * (int) ((speed * degreeOfTurning) * speedDecreaseFactor));
  }

}

/**
* Sets the degree of turning based on the deviation between two sensor inputs from the
* light sensors. This sensor deviation will be translated to a range from 0 to 1
* where 0.5 is straight forward, 0 being right and 1 being left.
*/
void steer(double sensorPercentageDeviation) {
  degreeOfTurning = 0.5 + (sensorPercentageDeviation * 0.5);
}

/**
* Arduino methods starts from here
*/
void setup() {
  Serial.begin(9600);
  
  state = BLIND_TRACKING;
  
  pinMode(LEFT_LED, OUTPUT);
  pinMode(RIGHT_LED, OUTPUT);
  pinMode(LEFT_PHOTO_SENSOR, OUTPUT);
  pinMode(RIGHT_PHOTO_SENSOR, OUTPUT);
  
  pinMode(LEFT_FORWARD, OUTPUT);
  pinMode(LEFT_BACKWARD, OUTPUT);
  pinMode(RIGHT_FORWARD, OUTPUT);
  pinMode(RIGHT_BACKWARD, OUTPUT);
  
  pinMode(LEFT_MOTOR_CONTROL, OUTPUT);
  pinMode(RIGHT_MOTOR_CONTROL, OUTPUT);
  
  servo.attach(SERVO_CONTROL);
  pinMode(DISTANCE_SENSOR, INPUT);
}

void loop() {
  
  // run sequence based on the state of the robot
  switch (state) {
  	case LINE_TRACKING:
    	digitalWrite(LEFT_LED, HIGH);
        digitalWrite(RIGHT_LED, HIGH);

        digitalWrite(LEFT_PHOTO_SENSOR, HIGH);
        digitalWrite(RIGHT_PHOTO_SENSOR, HIGH);
    
    	// the light sensors input value based on the ground, being on the line means a
        // lower value, and not on line, a higher value
        leftSensorReading = analogRead(LEFT_PHOTO_SENSOR_INPUT);
        rightSensorReading = analogRead(RIGHT_PHOTO_SENSOR_INPUT);

        // calculates the mean between the two light sensor inputs and the deviation
        // between thems
        sensorMean = (leftSensorReading + rightSensorReading) / 2;
        sensorDeviation = (double) rightSensorReading - sensorMean;

        // acceleration and steering actuation of the robot
        steer(sensorDeviation / sensorMean);
        accelerate(cruisingSpeed, true);
    
    	break;
    
    case BLIND_TRACKING:
    	// send out sound
    	pinMode(DISTANCE_SENSOR, OUTPUT);
    
    	digitalWrite(DISTANCE_SENSOR, LOW);
    	delayMicroseconds(2);
    
    	digitalWrite(DISTANCE_SENSOR, HIGH);
    	delayMicroseconds(2);
    
    	digitalWrite(DISTANCE_SENSOR, LOW);
    
    	// capture the response sound and convert it to cm
    	pinMode(DISTANCE_SENSOR, INPUT);
    	distanceSensorReading = pulseIn(DISTANCE_SENSOR, HIGH);
    	cmFromObstacle = distanceSensorReading / 29 / 2;
    
    	Serial.println(cmFromObstacle);
    
        if (cmFromObstacle < 50) {
			accelerate(0, true);
          
            if (servoAngle > 0) {
				servo.write(servoAngle--);
            }
        } else {
          	accelerate(cruisingSpeed, true);
          
          	if (servoAngle < 90) {
				servo.write(servoAngle++);
            } else if (servoAngle > 90) {
             	servo.write(servoAngle--);
            }
        }
    
    	
    
    	break;
    
    default:
    	break;
  }
  
  delay(cycleTime); // Wait for 1000 millisecond(s)
}

Link til tinkercad:

https://www.tinkercad.com/things/1Sk01hog1j1-pf2/editel?sharecode=fasfiF95Cnj5vMMKwqW2A0YDxs5jav25BbUKhPjhsL0

Opbygning af adfærd:

  • Opbygning af programmet er baseret på at vores robot har forskellige states/actions baseret på hvilke udfordringer den skal løse. LINE_TRACKING, DRIVE_AROUND_OBSTACLE, ALIGN_ROBOT, BLIND_TRACKING og STOP.
  • LINE_TRACKING: Dette er den første state som robotten er i. Her baserer vores robot primært dens bevægelser på lys sensor input. Vores lyssensor er placeret help op af hinanden midt under robotten. Dette er gjort for at følge stregen så præcist så muligt. sensor inputtet vi får fra de 2 lyssensor finder vi gennemsnittet af og dette bliver vores normal værdi. Vi kan derefter måle om vores input fra enten højre eller venstre sensor har store udsving og dermed om vore robot dreje til henholdsvis højre eller venstre. Hvis robotten registrerer at den skal korrigere sætter den farten lidt ned og laver den korrosion den nu skal. Hvis forskellen i gennemsnits lys inputtet bliver højer eller laver med mere end 10% bliver der sendt et signal til vores servo, hvorpå vores distance sensor sidder, om at den skal dreje i den retning robotten drejer. Hvis det input distance sensoren returnerer er under en minimumsværdi, ved robotten at der står, noget i vejen. Når dette sker skifter robotten state.
  • DRIVE_AROUND_OBSTACLE: Når robotten er i dette state er det fordi den skal køre uden om den forhindring der nu står i vejen for den. Robotten drejer nu om egen akse, så den får en vinkel til forhindringen således at den kan køre uden om. Servoen drejer nu således at distance sensoren peger på forhindringen og måler derefter afstanden. Robotten køre nu i en bue rund om forhindringen, indtil den igen rammer den sorte streg med den lyssensor. Når robotten gør det drejer den igen rundt om sig selv, dog lidt hurtigere på det ene hjul end det andet, indtil at den modsatte lyssensor rammer den sorte streg, og robotten skifte tilbag til LINE_TRACKING statet. 
  • LINE_TRACKING: Igen køre robotten primært på lyssensor input og følger stregen rundt. Når den kommer til enden at den sorte streg fortsætter robotten indtil distance sensorens måling rammer vores minimumsværdi. Her skifter robotten igen state.
  • ALIGN_ROBOT: Servoen drejer nu, samtidigt med den måle distance indtil, at distance sensoren måler en værdi som er større end den foregående. Dette betyder at har fundet den mur som er tættest på den. (Man ville kunne have lavet således at distance sensoren drejede til begge side og målte afstanden, og ud fra dette valgte hvilken retning at dreje til, men da vi kendte banens layout kunne vi nøjes med at checke den ene retning). Robotten drejer nu igen om egen akse ca tilsvarende til den mængde servoen drejede, således at den robotten kan køre hen til muren. Når robotten har drejet begynder den at køre ligeud indtil at distance igen registrer vores minimumsværdi. Robotten skifter nu igen state.
  • BLIND_TRACKING: Robotten drejer nu om egen akse således der kommer til være ca parallel med muren. Servoen drejer distance sensoren helt om mod muren og måler afstanden. Dette gør den så længe robotten køre langs en mur. Hvis distancen mellem robotten og muren bliver enten for stor eller for lille drejer robotten enten væk fra muren eller ind mod muren. 

Når robotten kommer til enden af muren stopper den og drejer servoen således at distance sensoren peger lige ud for robotten. Robotten køre nu lige ud indtil distance sensoren måling er lig med minimumsværdien. Den drejer nu igen om egen akse om og peger distance sensoren ind mod muren, lige som den gjorde første gang. Processen gentager sig nu igen indtil at robottens lyssensor rammer den sorte tværgående steg. Og robotten skifter til det sidste state STOP.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;

public class ArduinoMain : MonoBehaviour
{
    public Breadboard breadboard;
    public Servo servo;
    
    // speed variables for both weels and offsets for each wheel
    private int speed = 0;
    private int rightWheelSpeedOffset = 0;
    private int leftWheelSpeedOffset = 0;

    // the variable determining the "normal" light intake from the ground
    private int normalLeftLightInput = 0;
    private int normalRightLightInput = 0;

    // line tracking
    private bool driveRight = false;
    private bool driveLeft = false;
    private bool firstIteration = true;
    // the angle of the servo, going from 0 to 180 degrees. 90 is straight forward
    private int servoAngle = 90;
    
    // arduino input pin constants
    private const int LEFT_FORWARD = 3;
    private const int LEFT_BACKWARD = 2;
    private const int RIGHT_FORWARD = 1;
    private const int RIGHT_BACKWARD = 0;
    private const int LEFT_SENSOR = 4;
    private const int RIGHT_SENSOR = 5;
    private const int DISTANCE_SENSOR = 6;

    // constraints on the speed
    private const int MAX_SPEED = 50;
    private const int MIN_SPEED = 20;

    private int cruisingSpeed = 50;
    private double degreeOfTurning = 0.5;

    // default: false
    private bool obstacleIsPassed = false;
    private bool wallOnLeftSide = false;

    // how close can the car be to an obstacle
    // before value = 700
    private const int DISTANCE_THRESHHOLD = 500;

    private ulong obstacleDistance = 0;
    // before value = 100
    private ulong obstacleDistanceSlack = 50;

    // states of actions the robot can do
    private enum Actions {
        LINE_TRACKING, DRIVE_AROUND_OBSTACLE, ALIGN_ROBOT, STOP, BLIND_TRACKING
    }

    // declared actions enum
    Actions actions;

    /**
    *  ***********************
    *  OUR METHODS STARTS HERE
    *  ***********************
    */
    private void accelerate(int speedLimit, bool forward) {
        // increases speed if max speed is not achieved yet
        speed = speed < speedLimit ? speed + 1 : speed - 1;

        // calculates the sharpness of turns on a scale from 0 to 1
        double speedTurningDecrease = (degreeOfTurning - 0.5) / 0.5;
        speedTurningDecrease = speedTurningDecrease < 0 ? speedTurningDecrease * -1 : speedTurningDecrease;

        // factor to decrease speed by up to 80% based on the sharpness of the turn
        double speedDecreaseFactor = 1 - (speedTurningDecrease * 0.8);

        if (forward) {
            // makes sure the H-bridge does not have input on the opposite direction
            analogWrite(LEFT_BACKWARD, 0);
            analogWrite(RIGHT_BACKWARD, 0);

            // outputs speed to the dc motor actuator
            analogWrite(LEFT_FORWARD, 2 * (int) ((speed * (1 - degreeOfTurning)) * speedDecreaseFactor));
            analogWrite(RIGHT_FORWARD, 2 * (int) ((speed * degreeOfTurning) * speedDecreaseFactor));
        } else {
            // makes sure the H-bridge does not have input on the opposite direction
            analogWrite(LEFT_FORWARD, 0);
            analogWrite(RIGHT_FORWARD, 0);

            // outputs speed to the dc motor actuator
            analogWrite(LEFT_BACKWARD, 2 * (int) ((speed * (1 - degreeOfTurning)) * speedDecreaseFactor));
            analogWrite(RIGHT_BACKWARD, 2 * (int) ((speed * degreeOfTurning) * speedDecreaseFactor));
        }

        // debugging
        Debug.Log("Accelerate (speed): " + speed);
        Debug.Log("Accelerate (speed limit): " + speedLimit);
        Debug.Log("Accelerate (degrees of turning): " + degreeOfTurning);
        
    }

     private void steer(double sensorPercentageDeviation) {
        degreeOfTurning = 0.5 + (sensorPercentageDeviation * 0.5);
        Debug.Log("Steering (sensor deviation): " + sensorPercentageDeviation);
    }

    private void rotateRobot(bool clockwise, int rotationSpeed) {
        // makes sure no output is given to the dc motors
        speed = speed < rotationSpeed ? speed + 1 : speed - 1;

        if (clockwise) {
            analogWrite(LEFT_FORWARD, speed);
            analogWrite(RIGHT_BACKWARD, speed);
        } else {
            analogWrite(LEFT_BACKWARD, speed);
            analogWrite(RIGHT_FORWARD, speed);
        }

    }
    

    // gets input from the sonar sensor on how close the car is to an obstacle
    private void checkDistanceSensor(){
        // gets input reading
        
        ulong distanceMeasure = pulseIn(DISTANCE_SENSOR);
        Debug.Log(distanceMeasure);
        // if distance measure is below an accepted threshhold of distance and is
        // not 0 (no return of sound)
        if ((distanceMeasure < DISTANCE_THRESHHOLD && distanceMeasure != 0) && !obstacleIsPassed) {
            // sets drive around obstacle state as active
            Debug.Log("Around");
            setAction(Actions.DRIVE_AROUND_OBSTACLE);
            obstacleDistance = distanceMeasure;
        } else if ((distanceMeasure < DISTANCE_THRESHHOLD + 180 && distanceMeasure != 0) && obstacleIsPassed) {
            Debug.Log("BLIND");

            setAction(Actions.ALIGN_ROBOT);
            obstacleDistance = distanceMeasure;
        }
    }

    // set the angle of the servo according to the direction    
    private void setServoAngle(){
        //determines the angle based on the direction
        

        if(degreeOfTurning >= 0.6)
        {
            servoAngle --;
        } else if( degreeOfTurning <= 0.4)
        {
            servoAngle ++;
        }
            

        //output the particular angle to the servo
        servo.write(servoAngle);
    }


    private void setAction(Actions action){
        actions = action;
        firstIteration = true;
    }

    private void stopForASecond() {
        analogWrite(LEFT_FORWARD, 0);
        analogWrite(RIGHT_FORWARD, 0);
        analogWrite(LEFT_BACKWARD, 0);
        analogWrite(RIGHT_BACKWARD, 0);
    }
    
    /**
    *  ***********************
    *   OUR METHODS ENDS HERE
    *  ***********************
    */

    IEnumerator setup()
    {
        //Your code goes here:

        // # 1
        actions = new Actions();
        actions = Actions.LINE_TRACKING;

        // # 2

        normalLeftLightInput = analogRead(LEFT_SENSOR);
        normalRightLightInput = analogRead(RIGHT_SENSOR);

        servo.write(servoAngle);

        //Example of delay:
        Debug.Log("pre-delay log");
        yield return delay(2000); //2 second delay
        //Your code ends here -----

        //following region ensures delay-functionality for setup() & loop(). Do not delete, must always be last thing in setup.
        #region PremadeSetup
        yield return StartCoroutine(loop());
        #endregion PremadeSetup
    }

    IEnumerator loop()
    {
        //Your code goes here:
        // #1
        switch (actions)
        {   //Line tracking is when the robot is driveing along a line on the road
            case Actions.LINE_TRACKING:
                // linetrack
                
                int leftSensorInput = analogRead(LEFT_SENSOR);
                int rightSensorInput = analogRead(RIGHT_SENSOR);

                Debug.Log("Left Sensor Input: " + leftSensorInput);
                Debug.Log("Right Sensor Input: " + rightSensorInput);

                // calculates the mean between the two light sensor inputs and the deviation
                // between thems
                double sensorMean = (leftSensorInput + rightSensorInput) / 2;
                double sensorDeviation = (double) rightSensorInput - sensorMean;

                // acceleration and steering actuation of the robot
                checkDistanceSensor();
                if(!obstacleIsPassed)
                {
                    setServoAngle();
                }
                steer(sensorDeviation / sensorMean);
                accelerate(cruisingSpeed, true);

                // check if the line has come to a stop
                if (sensorMean >= 1023) {
                    
                    setAction(Actions.ALIGN_ROBOT);
                    
                    // break the car
                    while (speed != 0) {
                        checkDistanceSensor();
                        steer(0);
                        accelerate(0, true);
                        yield return delay(10);
                    }
                }

                if (actions == Actions.LINE_TRACKING)
                    firstIteration = false;
                
                
                break;

            case Actions.ALIGN_ROBOT:

                servoAngle = 40;
                servo.write(servoAngle);
                yield return delay(1000);

                // sets up variables to hold sound sensor input
                ulong distanceSensorInput = pulseIn(DISTANCE_SENSOR);
                ulong previousInputReading = distanceSensorInput;

                // turn the servo until it points somewhat straight into the wall in front of it
                while (distanceSensorInput <= previousInputReading) {
                    previousInputReading = distanceSensorInput < previousInputReading ? distanceSensorInput : previousInputReading;
                    
                    // rotate the servo
                    servo.write(servoAngle--);
                    //rotateRobot(false, 10);

                    yield return delay(50);

                    //Debug.Log("Distance Sensor Input: " + distanceSensorInput);

                    distanceSensorInput = pulseIn(DISTANCE_SENSOR);
                }

                previousInputReading = distanceSensorInput;

                // rotate the robot so that it faces straight into the wall as well
                while (servoAngle != 90) {
                    distanceSensorInput = pulseIn(DISTANCE_SENSOR);
                    rotateRobot(false, 10);

                    if (distanceSensorInput > previousInputReading ) {
                        servo.write(servoAngle++);
                    }

                    yield return delay(10);
                }

                // stop rotation of the robot
                while (speed != 0) {
                    rotateRobot(false, 0);
                    yield return delay(10);
                }

                setAction(Actions.BLIND_TRACKING);

                break;
            //Drive around obstacle is the distance sensor tells the robot that something is in it's way,
            //but still needs to find a line to follow againn
            case Actions.DRIVE_AROUND_OBSTACLE:
                // stop the car 
                Debug.Log("Obstacle in the way!");
                if (firstIteration) {
                    stopForASecond();
                    yield return delay(2000);

                    analogWrite(LEFT_BACKWARD, 20);
                    analogWrite(RIGHT_FORWARD, 20);

                    yield return delay(3200);

                    stopForASecond();

                    while (pulseIn(DISTANCE_SENSOR) < obstacleDistance - obstacleDistanceSlack || pulseIn(DISTANCE_SENSOR) > obstacleDistance + obstacleDistanceSlack) {
                        int angle = servo.read();

                        if (angle < 180 && angle > 0)
                            servo.write(servoAngle++);

                        yield return delay(10);
                    }

                }

                while (
                    analogRead(LEFT_SENSOR) >= normalLeftLightInput
                ) {
                    // tjekke lydsensor
                    if (pulseIn(DISTANCE_SENSOR) < obstacleDistance - obstacleDistanceSlack || 
                        pulseIn(DISTANCE_SENSOR) > obstacleDistance + obstacleDistanceSlack
                    ) {
                        leftWheelSpeedOffset = 20;
                        rightWheelSpeedOffset = -5;
                    }

                    // kører ligeud
                    analogWrite(LEFT_FORWARD, MIN_SPEED + leftWheelSpeedOffset);
                    analogWrite(RIGHT_FORWARD, MIN_SPEED + rightWheelSpeedOffset);

                    // bestemme offsets

                    yield return delay(10);
                }

                stopForASecond();

                //analogWrite(RIGHT_FORWARD, MIN_SPEED);
                analogWrite(RIGHT_FORWARD, MIN_SPEED);

                yield return delay(2000);

                //yield return delay(2000);
                while (analogRead(RIGHT_SENSOR) >= normalLeftLightInput) {
                    analogWrite(RIGHT_FORWARD, MIN_SPEED);
                    analogWrite(LEFT_BACKWARD, 5);
                    servo.write(90);
                    yield return delay(10);
                }

                stopForASecond();
                
                obstacleIsPassed = true;

                setAction(Actions.LINE_TRACKING);

                checkDistanceSensor();

                firstIteration = false;

                break;
                //Stop is when the robot's line sensors both are ative
            case Actions.STOP:
                // stop
                analogWrite(LEFT_FORWARD, 0);
                analogWrite(RIGHT_FORWARD, 0);
                break;
                // When there isen't a line for the robot to follow and it has to figur out how to procide 
        
                //When the robot has to drive only on the distance sensor
            case Actions.BLIND_TRACKING:
                Debug.Log("BLIND TRACKING");
                // blind track that shit
                    stopForASecond();
                // sound sensor input
                ulong soundSensorInput = pulseIn(DISTANCE_SENSOR);
                bool drivingAlongWall = false;
                // kør ligeud indtil en vis afstand fra muren
                if (soundSensorInput > DISTANCE_THRESHHOLD - obstacleDistanceSlack && soundSensorInput != 0 && !drivingAlongWall)
                {
                    // sensor ligeud
                    servo.write(90);
                    
                    analogWrite(LEFT_FORWARD, MIN_SPEED);
                    analogWrite(RIGHT_FORWARD, MIN_SPEED);
                } else {
                    firstIteration = false;
                    
                    stopForASecond();

                    if (wallOnLeftSide) {
                        // drej servor og mål afstand
                        analogWrite(RIGHT_BACKWARD, MIN_SPEED);
                        analogWrite(LEFT_FORWARD, MIN_SPEED);
                        yield return delay(2900);
                    } else {
                        // drej servor og mål afstand
                        analogWrite(LEFT_BACKWARD, MIN_SPEED);
                        analogWrite(RIGHT_FORWARD, MIN_SPEED);
                        yield return delay(2300);
                    }

                    stopForASecond();

                    drivingAlongWall = true;

                    if (wallOnLeftSide) {
                        servo.write(0);
                    } else {
                        servo.write(180);
                    }

                    yield return delay(3000);
                }
                
                while (drivingAlongWall) {
                    if (wallOnLeftSide) {
                        servo.write(0);
                    } else {
                        servo.write(180);
                    }

                    soundSensorInput = pulseIn(DISTANCE_SENSOR);
                    if (soundSensorInput > DISTANCE_THRESHHOLD && soundSensorInput != 0 && soundSensorInput <= DISTANCE_THRESHHOLD + obstacleDistanceSlack)
                    {
                        // drej ind mod muren
                        if (wallOnLeftSide) {
                            rightWheelSpeedOffset = 5;
                            leftWheelSpeedOffset = 0;
                        } else {
                            leftWheelSpeedOffset = 5;
                            rightWheelSpeedOffset = 0;
                        }
                        
                    } else if (soundSensorInput > DISTANCE_THRESHHOLD + obstacleDistanceSlack || soundSensorInput == 0) {
                        Debug.Log("NOT DRIVING ALONG WALL ANYMORE!!");
                        drivingAlongWall = false;

                        // NU MÅ DEN KUN KØRE LIGEUD

                        stopForASecond();
                        servo.write(90);

                        yield return delay(2000);

                        if (pulseIn(DISTANCE_SENSOR) == 0) {
                            analogWrite(LEFT_FORWARD, MIN_SPEED);
                            analogWrite(RIGHT_FORWARD, MIN_SPEED);

                            while (
                                analogRead(LEFT_SENSOR) >= normalLeftLightInput &&
                                analogRead(RIGHT_SENSOR) >= normalRightLightInput
                            ) {
                                analogWrite(LEFT_FORWARD, MIN_SPEED);
                                analogWrite(RIGHT_FORWARD, MIN_SPEED);
                                
                                yield return delay(10);
                            }

                            setAction(Actions.STOP);
                        }

                        wallOnLeftSide = true;
                    } else if (soundSensorInput < DISTANCE_THRESHHOLD && soundSensorInput != 0)
                    {
                        // drej væk fra muren
                        if (wallOnLeftSide) {
                            leftWheelSpeedOffset = 5;
                            rightWheelSpeedOffset = 0;
                        } else {
                            rightWheelSpeedOffset = 5;
                            leftWheelSpeedOffset = 0;
                        }
                    } else {
                        leftWheelSpeedOffset = 0;
                        rightWheelSpeedOffset = 0;
                    }
                    
                    analogWrite(LEFT_FORWARD, MIN_SPEED + leftWheelSpeedOffset);
                    analogWrite(RIGHT_FORWARD, MIN_SPEED + rightWheelSpeedOffset);

                    yield return delay(10);
                }

                //Drej 90 grader omkring egen akse

                //Mål afstand til mur
                
                //setAction();
                
                break;
                
            default:
                // bla bla
                break;
        }

        yield return delay(10);

        //Following region is implemented as to allow "yield return delay()" to function the same way as one would expect it to on Arduino.
        //It should always be at the end of the loop()-function, and shouldn't be edited.
        #region DoNotDelete
        //Wait for one frame
        yield return new WaitForSeconds(0);
        //New loop():
        yield return loop();
        #endregion DoNotDelete 
    }

    #region PremadeDefinitions
    void Start()
    {
        servo = FindObjectOfType<Servo>();
        if (servo == null)
        {
            Debug.Log("No servo found in the scene. Consider assigning it to 'ArduinoMain.cs' manually.");
        }
        Time.fixedDeltaTime = 0.005f; //4x physics steps of what unity normally does - to improve sensor-performance.
        StartCoroutine(setup());


    }

    IEnumerator delay(int _durationInMilliseconds)
    {
        float durationInSeconds = ((float)_durationInMilliseconds * 0.001f);
        yield return new WaitForSeconds(durationInSeconds);
    }

    public long map(long s, long a1, long a2, long b1, long b2)
    {
        return b1 + (s - a1) * (b2 - b1) / (a2 - a1);
    }

    public ulong millis()
    {
        return (ulong)(Time.timeSinceLevelLoad * 1000f);
    }

    public ulong abs(long x)
    {
        return (ulong)Mathf.Abs(x);
    }

    public long constrain(long x, long a, long b)
    {
        return (x < a ? a : (x > b ? b : x));
    }

    #endregion PremadeDefinitions

    #region InterfacingWithBreadboard
    public int analogRead(int pin)
    {
        return breadboard.analogRead(pin);
    }
    public void analogWrite(int pin, int value)
    {
        breadboard.analogWrite(pin, value);
    }
    public bool digitalRead(int pin)
    {
        return breadboard.digitalRead(pin);
    }
    public void digitalWrite(int pin, bool isHigh)
    {
        breadboard.digitalWrite(pin, isHigh);
    }

    public ulong pulseIn(int pin)
    {
        return breadboard.pulseIn(pin);
    }
    #endregion InterfacingWithBreadboard
}

Video af robot

https://www.dropbox.com/s/fkk4d81yvdacikc/pf2%20robot.mov?dl=0&fbclid=IwAR2nw5Dst30KtDZil8WDFBQVhPzq0R461WcfcayL_REgk_fRzqKvdSyEwoc

Hvordan er samspillet mellem mekanik, elektronik og software?: 

Man kan sammenligne vores robots, mekaniske, elektroniske og software komponenters samspil, med et menneske.

Kroppen er det samme som de mekaniske komponenter, elektroniske er det samme som et menneskes sanser(Se, lytte, mærke, høre), og software der styre de mekaniske baseret på de elektroniske, ligesom hjerne gør det ved mennesker. De mekaniske komponenter består af 3 motor. En til hvert hjul og en servo motor som er placeret oven på robottens base til at styre distance sensors retning. 

De elektroniske komponenter registrer nogle inputs som de sender til softwaren. De elektriske komponenter består af, 2 LDR modstande, brugt til at måle lys input, og en ultralydssensor, brugt til at mål afstanden til en genstand målt i millisekunder. 

Baseret på disse inputs analyser softwaren dem og lave en handling baseret på disse inputs. Software komponenterne er implementeret via en arduino, som modtager og sender elektriske impulser.  Disse impulser/handlinger bliver sendt ud til de mekaniske komponenter, som udføre dem. Om det er at dreje, stoppe eller lignende har softwaren sendt til de mekaniske komponenter.  

Konklusion – hvor godt løser robotten opgaven: 

I forhold til de krav som var sat i forhold til denne opgave, løser vores robot dem til perfektion. Opførelsen for robotten fungerer som forventet, og det kommer igennem banen hver gang på ca 150-160 sek. De pågældende restriktioner som er givet i opgaven og for den oprindelige ikke covid-19 opgave ville kunne blive udført af vores virtuelle robot. Alt dette betyder at vores robot løser alle udfordringer den bliver stillet for på den pågældende bane.  

Perspektivering – Hvad kan man gøre for at gøre den bedre?

Software: Desværre er vores software løsning i løbet af udviklingsprocessen blevet lidt for skræddersyet til præcis banen i denne aflevering. Der kom hen af vejen flere og flere løsninger som specifik viden omkring denne bane.  Dette kunne vi godt have tænkte os at have lavet mere generisk og med mere tid også tænkte noget AI lignende adfærd ind i vores løsning. Vi kunne også godt have tænkt os at koden så lidt pænere ud, men igen det virker og det gør hvad det skal. 

Vi kunne godt have gjort vores kode(BLIND_TRACKING) mindre kompliceret og mere overskuelig ved at dele noget af funktionaliteten ud i mindre og mere genanvendelig metoder  således at vi ikke hele tiden skal lave checks for hvor vi er på banen og hvad robotten nu skal fortage sig.

For at kunne færdiggøre banen hurtigere ville vi kunne justerer nogle få parameter. Vi kunne sætte de delays vi har i unity programmet ned således at det var mere optimeret til hastigheder som robotten har til at udføre visse handlinger(dreje servomotoren, dreje om egen akse, osv.). Disse justeringer ville kunne skrabe nogle få sekunder af vores tid.  Vi ville også kunne sætte max hastigheden som vores robot har lidt op således at den ville køre hurtigere når der var mulighed for det. Men ellers som programmet er bygget op lige pt. vil dette ikke blive vanvittigt meget hurtigere. Vi valgte at gå en mere forsigtig og sikker vej, end en hurtig og mere klodset. 

Referencer:

  1. https://www.batteribyen.dk/media/blfa_files/ultralife-u9vl-jp-technical-data-sheet.pdf
  2. http://www.ti.com/lit/ds/symlink/l293.pdf
  3. https://www.lego.com/en-us/themes/power-functions/element-spec
  4. https://docs.rs-online.com/ca69/0900766b81382974.pdf
  5. https://docs.rs-online.com/0e85/0900766b8123f8d7.pdf
  6. https://data.energizer.com/pdfs/l91.pdf
  7. http://www1.futureelectronics.com/doc/EVERLIGHT%C2%A0/334-15__T1C1-4WYA.pdf
More

Gamle Ole

Af Mads Obel Jensen og Sebastian Christiansen

Introduktion

Denne tutorial vil guide dig igennem processen at bygge en robot som er i stand til at navigere i en labyrint, at undgå at køre ind i vægge, og følge en guide streg på jorden.

Lad os komme i gang!

Du skal bruge

1 arduino uno

1 batteri på 9v 

1 breadboard 

1 stk rød led.

2 fotomodstande 

2 dioder

3 modstande, 2x 10k ohm, 1x 250 Ohm

1 stk L293D H-bro

2 stk hobby gearmotorer

1 sonar sensor

1 stepper motor

4 kondensatorer 100 nf

Opbygning

Således er robotten konstrueret i TinkerCad, ved opstart vil motorene rotere og robotten blive bragt til livs, og begynde at observere omgivelserne.

Beskrivelse

Som set i billedet af robotten ovenover består denne robot af tre primære sektioner, hjerne, krop, og lemmer. Hjernen her er vores arduino uno, som står for at holde styr på robottens sensorer (sonar sensor, fotomodstande)  og aktuatorer (jævnstrømsmotorer). Kroppen er knudepunktet for robottens hjerne og ud til lemmerne, altså breadboardet. Lemmerne er robottens måde at interagere med omverdenen, for denne robot er det at køre, dreje og rotere sonar sensoren.

Overvejelser

Det har været vigtigt at robotten er til at forstå ved syn alene, derfor er den opstillet som anvist. Dette gør det klart helt præcist hvad den kan:

  • Den kan se med sonar sensoren som sidder forrest på robotten da dette viser at den kigger fremad.
  • Til siderne sidder moterene som viser at robotten kan køre,
  • til hver side af breadboardet sidder fotomodstande, altså kan den måle lys
  • og i midten sidder h-broen, altså kan robotten ændre kørselsretning, og dreje.

Det er vigtigt at robotten skal kunne opfatte objekter som står i vejen for den. Dette kan gøres på en af to måder, hvor begge kræver nogle antagelser om robottens omgivelser.

  1. Robotten kigger omkring sig og undgår nærliggende objekter ved at rotere i den modsatte retning ifht hvor objektet blev observeret. 
  2. Robotten kigger ligeud og antager at hvis den blot følger linjen rammer den ingen objekter, dog vil den stoppe op når et objekt fremstår foran den, da dette betyder at der er sat noget foran den, eller at den har nået målet.

Vi valgte nummer 2 da det ikke var klart ud fra opgavebeskrivelsen at robotten skal kunne gebærde sig i miljøer hvori en linje på jorden fører ind i en væg som ikke er målet. Ud fra opgavebeskrivelsen var det illustreret at robotten skal kunne følge en linje som, hvis fulgt fører til mål, uden at snyde robotten med vilje, via blinde veje, eller andet. Derfor blev løsning 2 valgt. Dog fremstår der stadig en steppermotor i robottens konstruktion og kode, da den har muligheden for at dreje sensoren, det bliver dog aldrig gjort.

Komponent teori

Motorer

I robottens konstruktion er der benyttet to jævnstrøms hobby-gearmotorer, altså to motorer som konvertere elektrisk energi til kinetisk energi (bevægelse), disse sidder på hver side af robotten.

At drive motorerne fremad kræver strøm igennem motoren fra positiv til negativ på begge motorer. Baglæns kræver strøm igennem motorerne  fra negativ til positiv. At dreje kræver blot strøm fra positiv til negativ på den ene, og strøm fra negativ til positiv på den anden, dette lader robotten rotere om sin egen akse. Hvor kun at føre strøm gennem den ene motor vil resultere i at robotten roterer omkring det stillestående hjul, lader dette robotten dreje optimalt på en smal bane.

Alt dette gøres ved styring med arduinoen og korrekte input til h-broen, med passende pulsbredde (dette beskrives nærmere i afsnittet om h-broen).

Sensorer

Fotomodstand:

Fotomodstande benyttes som sensorer da disse hæver og sænker deres interne modstand afhængig af det observerede lysindfald på sensoren. Fotomodstande følger grafen nedenfor med hensyn til modstand over lux (lysindfald).

Forholdet mellem en fotomodstand og lysindfald er således, fotomodstanden har en meget høj modstand som udgangspunkt 1M ohm, dette betyder at uden lysindfald er der få elektroner som frit kan bevæge sig og hoppe i et af hullerne i P-type metallet. Når lys rammer fotomodstanden optages en del af lysets fotoner hvilket aktivere elektronerne inde i det aktive lag. Dette lader flere elektroner bevæge sig, altså at hoppe i et af hullerne i P-type metallet. Desto mere lys som falder på fotomodstanden desto flere fotoner optages i det aktive lag, og jo lavere bliver modstanden. Konstruktionen af en fotomodstand ses nedenfor

https://www.electronics-notes.com/articles/electronic_components/resistors/light-dependent-resistor-ldr.php

Sonar sensor:

Denne sensor benytter sig af et ping, altså en ultrasonisk lyd som udsendes af den ene side af sensoren, og observeres af den anden side af sensoren, tiden imellem afsending af lyden og observationen heraf giver dig afstanden til objektet i mikrosekunder. Vi ved at lydens hastighed er 340 m/s, hvilket svarer til 29 millisekunder pr centimeter. Derfor kan vi beregne afstanden i cm ud fra den observerede millisekunder imellem udsendelse og observation af lyden, ved følgende:

cm = millisekunder / 29 /2

Vi dividere med 29 for at få afstanden i centimeter, og dividere igen med to, da lyden havde samme hastighed da den blev sendt ud, til den ramte noget, og igen blev observeret, altså er vi kun interesseret i tiden det tog lyden at blive observeret fra den ramte et objekt.

For vores robot bliver denne afstand sammenlignet med den fiktive længde af robotten plus den mindste afstand robotten skal bruge for at kunne dreje omkring sig selv.

I koden ses det at der via sonar sensoren sendes et lavt signal, hvilket sørger for at høj fremstår helt klart, hvilket sendes efter lav, efterfulgt af endnu et lavt, for igen at fremhæve høj.

Pulsein metoden benyttes på arduinoen med parametrene pin for sensorens signal port, og HIGH, dette skyldes at vi ønsker at måle intervallet fra signalet på den givne pin var lavt, til det var højt. Dette måles i millisekunder og kan benyttes til at vurdere afstanden til objektet som vis ovenover.

Andre komponenter

Polariseret Elektrolyt kondensator (Polarized capacitor):

Kendes visuelt da denne blot vil have en kant rundt i skallen.

Som det fremstår på diagrammet nedenfor har en kondensator to tilledninger, en positiv og en negativ. Når denne forbindes til en jævn-spændingskilde vil denne spændingskilde lade strøm løbe igennem kondensatoren, hvis altså den er forbundet positiv til positiv og negativ til negativ med spændingskilden. spændingskilden vil så “lade” den plade som er forbundet til negativ på spændingskilden med elektroner.

Der vil derfor være et overskud af frie elektroner i den positive plade, hvilket tillader at der løber strøm i kondensatoren.

Som det ses på figuren er den negative plade større end den positive, dette resultere i en opladningen tid, hvori kondensatoren skal lades med elektroner i den negative plade førend der løber strøm i kondensatoren. Dette har den effekt at jo større kapacitet kondensatoren har (målt i farad) desto længere opladningstid vil der være. Dette medfører naturligvis også at kondensatoren har en opladningstid hvori den vil afgive sin elektriske ladning. 

Ydermere tillader en kondensator os at stabilisere en strømkilde om nødvendigt, dette gøres da strømkilden jo selv skal oplade kondensatoren, hvorefter kondensatoren vil lade strøm løbe igennem sig. I tilfælde af en en spændingskilde vil yder varierende spænding kan en kondensator stabilisere dette, grundet opladningstiden og den sekventielle afladning af kondensatorens kapacitet.

Dobbel H-bro (L293D):

Denne chip består af 2 * 8pins, herunder:

  • 4 input
  • 4 output
  • 2 enable
    • 2 ground hertil
  • 2 power 
    • med 2 tilhørende ground pins

Strøm løber over chippen via power pins, dette benyttes til at tilkoble den ønskede spændingskilde til at drive vores motore. De fire input tilkobles arduino, som via 5v signal kan åbne og lukke for de tilhørende output, dette betyder at vi kan drive to motere både fremad og baglæns. De to enable pins benyttes til pulse width modulation (PWM) eller pulsbreddemodulation.

Pulsbreddemodulation betyder at vi sender et digitalt signal via vores arduino, som bliver fortolket som et analogt signal af modtageren, altså af h-broen. Et sådant signal er illustreret nedenfor.

Det ses her at de fire signaler er digitale signaler, altså blot høj, og lav som mulige værdier. Det skal dog gøres klart at modtager af signalet skal kende til frekvensen med hvilken signalet er genereret, da signalet ellers mister sin betydning. Ud fra frekvensen kan man opdele hele det modtagne signal i dele altså i mindre perioder, perioderne vil passe 1:1 med frekvensen, og signalet kan nu fortolkes.

Det beregnes hurtigt hvor mange høje værdier ifht lave værdier signalet indeholder, ud fra dette beregnes der en procent som ud fra hvilke modtageren sender den tilsvarende procent strøm ud til de åbne outputs. Eksempelvis, kan modtageren modtage et signal med en frekvens på 10hz, hvori den beregner at der var 4 høje værdier, derfor 6 lave, altså vil det sige at der var 40% høj også kaldet en duty cycle på 40%. Altså vil modtageren tillade 40% af den tilkoblede spændingskilde at løbe igennem sig og ud til output. for en 9V spændingskilde vil det cirka svarer til 3,6V, dog vil vi nok observere mindre, da der vil gå lidt tabt i processen.

Arduino uno afsender med en frekvens på 490hz pr pin og med pins: 3, 5, 6, 9, 10, 11 i stand til at udføre analogWrite over. Parametrene for arduinos analogwrite er pin og værdi fra 0 til 255, hvor nul er 0% duty cycle, og 255 er 100% duty cycle.

I denne robot er vi i stand til at drive motorene op til 215 RPM ved 100% duty cycle, med et par hjul på med radius på 10 cm kan vi benytte følgende for at regne robottens maksimale  bevægelseshastigheden ud:

v = r × RPM × 0.10472

v = 0,1m * 215 * 0,10472 = 2,25148 m/s

(2,25148/1000)*60*60 = 8,1 km/t

Dog vil dette ikke være realistisk at lade robotten køre, da den herunder skulle kunne vurdere lysintensiteten og dreje sig korrekt og undgå at køre ind i noget, hvilket alt sammen er begrænset af arduinoens processor hastighed hvilket er 16Mhz hvilket svarer til 16 millioner cyklusser per sekund. Det skal her nævnes at hver operation kræver et par cyklusser at eksekvere, og jo mere kompleks koden bliver, som flere cyklusser skal der bruges på hver operation. 

Da robotten skal læse sensor værdier og beregne den optimale handling, og derefter eksekvere handlingen vil det sandsynligvis være problematisk at drive den ved 8 km/t. 8,1 km/t svarer ca til 2,215 m/s, hvilket er 221,5 cm/s. Dette er en signifikant afstand at tilbagelægge på et enkelt sekund, især hvis banen er snæver.

Diode:

Vi har benyttet et par dioder i konstruktionen af robottens “krop”, en diode er yderst brugbar når man vil garantere at strømmen udelukkende løber i en retning, dette vil sige at strømmen kan løbe gennem dioden fra anode til katode. Strøm som løber i den modsatte retning vil blive stoppet.

Begrænsningen af strømmen gennem dioden fungerer således:

https://www.fysik7.dk/1080-vi-bruger-elektricitet/1085-ensretning-af-vekselspaending/1085a-sadan-virker-en-diode

Dioden er opbygget af to metaltyper, P-type og N-type, set henholdsvis til højre og venstre i illustrationen ovenover. Da elektronerne bevæger sig fra minus til plus vil elektronerne strømme fra minus og ind imod grænselaget, da de bliver tiltrukket hertil. En elektron som møder et “hul” (plus på illustrationen) vil hoppe ind i hullet, og blive skubbet ud da der bliver tilført nye huller. Dette lader strøm løbe i en retning gennem dioden. Hvis dioden vendes omvendt, således at elektroner bliver indført via plus vil de herunder blive tiltrukket til P-type metallet, og hullerne blive tiltrukket N-type metallet, altså vil hullerne og elektronerne aldrig mødes på grænselaget.

LED:

En lysdiode (LED) er konstrueret ligesom en almen diode, dog med den effekt at når strøm løber over dioden, afgives en del af effekten som lys (afgivelsen af fotoner). Dette betyder at en lysdiode ligesom en almen diode kun tillader strømmen at løbe fra katode (negativ) til anode (positiv). Lys afgives af lysdioden i det at en elektron hopper i et hul på grænselaget, farven af det lys som lysdioden afgiver afhænger af materialet. En rød lysdiode med høj lysstyrke vil består af en del Aluminium gallium indiumphosphid (AlInGap) hvilket blot er et halvleder metal, hvor halvleder metallet afgiver den røde farve når strøm løber gennem den. 

I vores robot er den benyttet en rød lysdiode til, at indikere når objekter er for tæt på robotten, dette giver et klart visuelt signal for robottens opfattelse af omgivelserne.

For ikke at beskadige lysdioden benyttes der en formodstand på 150 ohm, beregnet efter følgende formel:

R = (Vs – Vled) / Iv

Modstanden i formodstanden er lig, spændingen fra spændingskilden, minus den ønskede spænding over lysdioden, divideret med den ønskede strømstyrke målt i ampere.

R = (5 – 2) / 0,02 = 150 ohm

Dette kommer af databladet for en rød lysdiode, her beskrives det at en sådan lysdiode fungere ved en spænding på mindst 2v og med en strømstyrke 20 mA (milli ampere).

Opsætning

Nu hvor vi er bekendt med komponenterne i brug, og teorien bag, bør det være klart hvorfor vi benytter lige netop disse.

Opsætningen af robotten gøres således:

  1. Breadboardet pladseres, og h-broen (L293D) sættes i midten da denne er kernen i robottens aktuatorer.
    1. Pin enable1&2 føres, som beskrevet tidligere, til arduino uno PWM pin, dette er en pin med et ~ tegn efter tallet.
    2. det samme gøres med pin enable3&4 dog til en anden PWM pin med ~ tegnet på arduinoen. Du er nu klar til at sende et analogt signal til h-broen!
    3. Nu skal der opsættes input og output for h-broen
      • De to power pins 1 og 2 tilføres strøm fra vores 9V batteri.
      • Input 1, 2, 3, og 4 skal forbindes til hver sin pin på arduinoen og skal slutte på samme bane som input pins for h-broen.
      • Output pins fra h-broen tilføres motorene, dvs output pin 1 og 2 fører til det højre hjuls motor, hvor output pin 3 og 4 føres til højre hjulsmotoren. For hver motor skal ledningerne løbe således at output 1 og 3 løber til henholdsvis venstre og højre motors positiv, og output 2 og 4 løber til motorenes negativ.
      • De to fire ground pins fra h-broen føres ud i negativ på breadboardet.
      • Der skal nu blot sættes en kondensator på hvert output – ground sæt fra h-broen, således at kondensatorerne står før ledningerne til motorene. Din h-bro bør se således ud, på nuværende stadie.
    4. Der skal nu skrive lidt kode, som kan føre robotten frem og tilbage ved at skrive til moterene. Koden ser således ud:
void setup(){
  //Motor 1
  pinMode(lMotorP, OUTPUT);
  pinMode(lMotorN, OUTPUT);
  
  //Motor 2
  pinMode(rMotorP, OUTPUT);
  pinMode(rMotorN, OUTPUT);
}
	void goForward(float speed){
  digitalWrite(lMotorP, HIGH);
  digitalWrite(lMotorN, LOW);
  digitalWrite(rMotorP, HIGH);
  digitalWrite(rMotorN, LOW);
  
  analogWrite(pinEn12, speed);
  analogWrite(pinEn34, speed);
}

void goBackward(int speed){
  	 digitalWrite(lMotorP, LOW);
 	 digitalWrite(lMotorN, HIGH);
  	 digitalWrite(rMotorP, LOW);
 	 digitalWrite(rMotorN, HIGH);
  
  	 analogWrite(pinEn12, speed);
  	 analogWrite(pinEn34, speed);
}

Som det ses her, angiver vi hvilke pins vi skriver en høj og lav værdi til, hvilket skal gøres forinden der sendes det analoge signal som fortæller h-broen hvor kraftigt den skal drive motorene. Et højt signal på h-broens input med associeret output til positiv betyder at robotten vil bevæge sig fremad. Det modsatte gør sig også gældende. Parameteren “speed” er her en reference til den duty cycle som vi ønsker at sende som signal, altså den procentmæssige høje værdi for analog signalet. Jo større speed, jo højere spænding vil motorene drives ved.

I arduino koden kan motoren nu drives således:

void loop(){
	goForward(100);
	delay(100);
	goBackward(100);
	delay(100);
}

Med denne kode kørende ses det at robotten vil køre frem ad i 100 millisekunder, hvorefter den vil køre baglæns i 100 millisekunder.

At dreje sker på samme vis som frem og tilbage, dog at den ene motor drives fremad mens den anden drives baglæns.

void turnCounterclockwise(int speed){
  digitalWrite(lMotorP, LOW);
  digitalWrite(lMotorN, HIGH);
  digitalWrite(rMotorP, HIGH);
  digitalWrite(rMotorN, LOW);
  
  analogWrite(pinEn12, speed);
  analogWrite(pinEn34, speed);
}
  1. Nu hvor robotten kører, ønsker vi at kunne følge stregen på jorden, de stregen er en kontrast farve ifht hvid, kan vi benytte en fotomodstand til at læse en repræsentation af lysindfaldet på denne komponen. Den modstatte værdi, altså 1 – lysindfaldet må være talværdien af hvor tæt fotomodstanden er på den mørke streg. Derfor kan vi skrive den følgende kode, hvilket returnere værdien som et decimal tal fra 0 til 1.
float senseDarknessLeft(){
 	return 1 - (analogRead(A0) / 1000.0); 
}

float senseDarknessRight(){
 	return 1 - (analogRead(A1) / 1000.0); 
}

Hver foto modstand opsætte som set herunder, med en diode forinden indtaget fra 5v fra arduinoen, med stregen imod fotomodstanden, hvilket sikrer at der kun løber strøm fra positiv til negativ. Der påsættes en pull-down modstand altså en 1k ohm modstand, som leder til negativ. Der kan nu læses lysforholdet ved en ledning forbundet til arduino pins og ud til fotomodstanden før pull-down modstanden.

Pull-down modstanden sikrer at vi kan måle signalet med arduinoen uden støj. Det er vigtigt at ground her fører tilbage til arduinoens ground, og ikke til batteriets ground.

Denne procedure gentages for den modsatte side af robotten, således at der er to fotomodstande forbundet til arduinoen, en i hver side af robotten, så vi kan sigte efter at kører med stregen lige i midten under robotten.Sonar sensoren forbindes til arduinoens 5v til sensores 5v pin, og ground til arduinoens ground pin. Signal pin forbindes til en vilkårlig arduino digital pin.

For at benytte sonar sensoren til at måle afstand til et objekt foran denne sensor, skal udføre det som kaldes et “Ping” altså det at sende et signal ud og modtage det når det opfanges igen på vej tilbage. Når et sådant signal returnere betyder det, at signalet har ramt et solidt objekt, og er blevet skudt tilbage.

  1. Koden for at udføre et ping er således:
bool somethingTooClose(){
  pinMode(pingPin, OUTPUT);
  digitalWrite(pingPin, LOW);
  delayMicroseconds(2);
  digitalWrite(pingPin, HIGH);
  delayMicroseconds(5);
  digitalWrite(pingPin, LOW);

  pinMode(pingPin, INPUT);
  duration = pulseIn(pingPin, HIGH);
}

Det ses her at vi sender tre signaler, et lavt, som sikrer at sensoren er “ren” altså at der ikke er noget støj. Derefter sender vi et højt, altså sender vi pinget ud. Vi følger det op med et lavt igen, for at sikre os at der igen ikke opstår støj. “duration” er her tiden i millisekunder fra ping blev sendt til det bliver modtaget i pulseIn metoden, som her venter på det høje signal, på den rette pin.

Vi kan benytte tiden i ms til at omregne det til cm via denne funktion (se yderligere forklaring af denen i sensor afsnittet).

long microsecondsToCentimeters(long microseconds) {
  return microseconds / 29 / 2;
}

Vi kan derefter tilføje disse to linjer til somethingTooClose() metoden.

cm = microsecondsToCentimeters(duration); 

return ((int)cm <= (length + minDist));

Dette sikrer at vi altid returnere et konkret svar baseret på størrelsen af robotten og objektets afstand.Det samme gøres med stepper motoren.

Vi er nu blot interesseret i at kigge omkring, med stepper motoren, denne vil rotere sonar sensoren såled at vi kan måle afstanden til objekter til hver side af robotten.

  1. Koden for at rotere stepper motoren ser således ud:
void look(int angle){
  if(angle > 0){
   	servo.write(angle);                 
    	delay(150);   
  }else{
    	servo.write(0);
  }
}

Her beder den blot om en parameter hvilket er en vinkel hvortil robotten skal kigge, og måle afstand til eventuelle objekter.

  1. Du har nu konstrueret den komplette robot, og opstillet al den kode som er nødvendig for at drive robotten, og kontrollere den. Der mangler blot en ting, og det er at opstille det korrekte loop, for den komplette robot. Loopet skal agere som et styresystem, eller som hjernen for robotten, herunder skal den indhente data fra sine sensorer og udføre den passende handling baseret på disse input. Et sådan loop kan se således ud:
void loop()
{
  if(!somethingTooClose()){
    digitalWrite(7,LOW);
    if((senseDarknessLeft() - senseDarknessRight()) - tol > 0){
      	turnCounterclockwise(turnSpeed);
    }
    if((senseDarknessRight() - senseDarknessLeft()) - tol > 0){
      	turnClockwise(turnSpeed);
    }if(getLightDifference() < tol){
      	goForward(speed - (getLightDifference()*60)); 
    }
  }else{
    digitalWrite(7,HIGH);
    Serial.println("GOALLLL!");
    stand();
  }
}

Her ses det at robotten kører indtil den opfanger et objekt for tæt på, hvilket ifølge opgavebeskrivelsens illustration af en bane, vil være målet, den retter også sig selv til efter stregen på gulvet. 

Der er blot tilføjet en “tol” variabel, hvilket er den procentmæssige forskel der må tolereres på læsningen af de to fotomodstande.

Der er også tilføjet en getLightDifference() funktion, essensen af dette, er blot at sænke duty cyklussen for det drivende signal til motorene alt efter forskellen på målingerne på de to fotomodstande. Dette sikrer at robotten kører langsommere og skævere den kører for linjen, hvilket giver den en større chance for at rette sig til, inden den kører alt for skævt.

Problemer

Det har været problematisk at opstille motorene med den nye h-bro komponent, da den her først skulle forstås, og derefter opsættes helt korrekt med korrekt ledningsføring. 

Derudover har analogwrite givet problemer da der først skulle erfares at ikke alle pins kan benyttes til PWM.

Derudover har kondensatorerne voldt problemer da der her skulle vælges en passende kapacitans hvorved det ikke tager for lang til at aktivere, og ikke aflader for længe. Formålet med at indføre disse kondensatorer var at stabilisere output til motorene fra pulsbredde modulationen, således at motorene roterer mere stabilt over længere til og forårsager mindre variation i RPM.

Den fulde kode

Flow diagram

Kode

#include 

//Servo motor
Servo servo;

int pos = 0; //initial position of the stepper motor
long duration, cm;

int pingPin = 4; //Sonar sensor pin

//Enable pins:
int pinEn34 = 6; //Enable pin for output 3 and 4
int pinEn12 = 11; //Enable pin for output 1 and 2

//Motor pins:
int rMotorP = 12; //Positive side input of the right motor
int rMotorN = 13; //Negative side input of the right motor
int lMotorP = 9; //Positive side input of the left motor
int lMotorN = 10; //Negative side input of the left motor


int length = 50; //The length of the robot
int minDist = 50; //The distance from the robot within which we want it to react
int rotTol = 30; // The amount of time it is allowed to turn in one direction giving up
int speed = 100; //The duty cycle written to the motors, regulates speed goes from 0 to 255
int turnSpeed = 10;
float tol = 0.80;

void setup()
{
  servo.attach(2); // Assign servo to pin
  servo.write(0); //Prevents the servo from drifting
  
  pinMode(7, OUTPUT);

  //Photoresistors
  pinMode(A0, INPUT); //LDR1
  pinMode(A1, INPUT); //LDR2
  
  //Sonar sensor
  pinMode(pingPin, INPUT);
  
  //Motor 1
  pinMode(lMotorP, OUTPUT);
  pinMode(lMotorN, OUTPUT);
  
  //Motor 2
  pinMode(rMotorP, OUTPUT);
  pinMode(rMotorN, OUTPUT);
  
  Serial.begin(9600);
}

void loop()
{
  if(!somethingTooClose()){
    digitalWrite(7,LOW);
    //Serial.println(senseDarknessLeft() - senseDarknessRight());
    if((senseDarknessLeft() - senseDarknessRight()) - tol > 0){
      //Left reading was largest
      //Go counterclockwise 
      turnCounterclockwise(turnSpeed);
    }
    if((senseDarknessRight() - senseDarknessLeft()) - tol > 0){
      //Right reading was largest
      //Go clockwise 
      turnClockwise(turnSpeed);
    }if(getLightDifference()  0){
    servo.write(angle);                 
    delay(150);   
  }else{
    servo.write(0);
  }
}

//This code section is gotten from a public arduino demo project using the sonar sensors
bool somethingTooClose(){
  // establish variables for duration of the ping,
  // and the distance result in inches and centimeters:
  int duration, cm;

  // The PING))) is triggered by a HIGH pulse of 2 or more microseconds.
  // Give a short LOW pulse beforehand to ensure a clean HIGH pulse:
  pinMode(pingPin, OUTPUT);
  digitalWrite(pingPin, LOW);
  delayMicroseconds(2);
  digitalWrite(pingPin, HIGH);
  delayMicroseconds(5);
  digitalWrite(pingPin, LOW);

  // The same pin is used to read the signal from the PING))): a HIGH
  // pulse whose duration is the time (in microseconds) from the sending
  // of the ping to the reception of its echo off of an object.
  pinMode(pingPin, INPUT);
  duration = pulseIn(pingPin, HIGH);

  // convert the time into a distance
  cm = microsecondsToCentimeters(duration);
  
  return ((int)cm <= (length + minDist)); //If something is too close
}


long microsecondsToCentimeters(long microseconds) {
  // The speed of sound is 340 m/s or 29 microseconds per centimeter.
  // The ping travels out and back, so to find the distance of the
  // object we take half of the distance travelled.
  return microseconds / 29 / 2;
}

//Rotates the robot around its own axis
void turnClockwise(int speed){
  digitalWrite(lMotorP, HIGH);
  digitalWrite(lMotorN, LOW);
  digitalWrite(rMotorP, LOW);
  digitalWrite(rMotorN, HIGH);
  
  analogWrite(pinEn12, speed);
  analogWrite(pinEn34, speed);
}

//Rotates the robot around its own axis
void turnCounterclockwise(int speed){
  digitalWrite(lMotorP, LOW);
  digitalWrite(lMotorN, HIGH);
  digitalWrite(rMotorP, HIGH);
  digitalWrite(rMotorN, LOW);
  
  analogWrite(pinEn12, speed);
  analogWrite(pinEn34, speed);
}

//Tells the robot to move forwards
void goForward(float speed){
  digitalWrite(lMotorP, HIGH);
  digitalWrite(lMotorN, LOW);
  digitalWrite(rMotorP, HIGH);
  digitalWrite(rMotorN, LOW);
  
  analogWrite(pinEn12, speed);
  analogWrite(pinEn34, speed);
  
}

//Instructs the robot to move in reverse
void goBackward(int speed){
  digitalWrite(lMotorP, LOW);
  digitalWrite(lMotorN, HIGH);
  digitalWrite(rMotorP, LOW);
  digitalWrite(rMotorN, HIGH);
  
  analogWrite(pinEn12, speed);
  analogWrite(pinEn34, speed);
}

//Tells the robot to stand perfectly still
void stand(){
  digitalWrite(lMotorP, LOW);
  digitalWrite(lMotorN, LOW);
  digitalWrite(rMotorP, LOW);
  digitalWrite(rMotorN, LOW);
  analogWrite(pinEn12, 0);
  analogWrite(pinEn34, 0);
  look(0);
}

Kommentarer

Som det ses af denne linje, hvor hastigheden bliver sat når robotten skal køre fremad,

goForward(speed – (getLightDifference()*60)); 

Så er hastigheden variabel, det skal forstås således at robotten sænker hastigheden jo mere den kommer ud af kurs med linjen, dette øger sandsynligheden for at den vil ramme linjen senere, da hastigheden for robotten afgør hvor langt den kører førend den næste gang måler linjens position ifht sig selv. Ved at sænke hastigheden sørger vi for at robotten retter sig mere præcist ifht linjen over en kortere afstand. Når så robotten har rettet sig ind, og står fint for linjen igen, vil den øge hastigheden.

Ydermere er der angivet en variable tol som er den tolerance der er for differencen i målingerne fra fotomodstandene førend robotten vil tage handling og rotere efter at optimere sin position. Dvs. at robotten venter på at den er en vis procent skævt placeret ifht linjen før den retter sig ind efter målingerne. Dette sikrer at den vil komme hurtigere i mål, og jo tolerance den har for forskellen på målingerne jo længere vil den køre hurtigt, hvorefter den vil begynde at dreje.

Denne løsning kommer fra erfaringer fra første porteføljeopgave, hvori vores målinger fra fotomodstande varierede meget ifht hinanden, hvilket gjorde det svært for robotten på det tidspunkt at vælge en retning at rotere, da de begge var lige attraktive på hvert deres tidspunkt, og hurtigt skiftende. Tolerancen løste problemet der, og det løser samme problem igen.

Simulationen

Vi har benyttet unity til at simulere robotten i de forventede omgivelser, dette afsnit dokumenterer dette forsøg, og resultatet heraf.

Opbygningen

Opbygningen af robotten skete i et Unity baseret projekt. Der var i projektet allerede konstrueret nogle dele, inklusiv medfølgende scripts, som skulle gøre det muligt for os at bygge robotten forholdsvist simpelt i unity, når vi nu pga COVID-19 ikke har mulighed for at møde op fysisk og gøre dette. Robotten består hovedsageligt af af seks dele, chassiset, hjulene, servoen, linje sensoren, afstandsmåleren og arduinoen. I unity projektet er der dog et “breadboard” som binder tingene sammen for os.

Koden

Koden skulle indsættes i ArduinoMain.cs som skulle formå at emulere en arduino 1:1. ArduinoMain har nogle funktioner fra arduinoen tilføjet, så man undgår at skulle ændre for meget i koden for at få den til at virke i unity, som den ville i virkeligheden, på en arduino.

public Breadboard breadboard;
    public Servo servo;
    //On included/premade Arduino functions:
    //delay(timeInMilliseconds) : use "yield return delay(timeInMilliseconds)", to get similar functionality as delay() in arduino would give you.

    //map() : works exactly as on Arduino, maps a long from one range to another. 
    //If you want to get an int or a float from the map()-function, you can cast the output like this: (int)map(s, a1, a2, b1, b2) or (float)map(s, a1, a2, b1, b2) 

    //millis() : returns the time as a ulong since the start of the scene (and therefore also the time since setup() was run) in milliseconds.

    //If you want to do something similar to serial.println(), use Debug.Log(). 

    //analogWrite() and analogRead() works as they do in arduino - remember to give them correct input-values.
    //digitalRead() and digitalWrite() writes and returns bools. (High = true). 
    //LineSensors have both write-functions implemented, motors/hbridge have both read-functions implemented.
    //The console will display a "NotImplementedException" if you attempt to write to sensors or read from motors. 


    //Additions from 21-04-2020:

    //Distance sensor:
    //The Distance (ultrasonic) sensor is added, if you use "pulseIn()" on the pin it is assigned to, 
    //it will return the time it took sound to travel double the distance to the point of impact in microseconds (type: ulong).
    //This mimics roughly how the HC-SR04 sensor works. 
    //There is no max-range of the distance-sensor. If it doesn't hit anything, it returns a 0.

    //Servo:
    //if you add the servo-prefab to the scene, ArduinoMain will automatically find the servo object, essentially handling "servo.attach()" automatically. 
    //There can be only one servo controlled by this script.
    //servo.write() and servo.read() implemented, they function similar to a servomotor. 
    //The angles that servo.write() can handle are [0:179]. All inputs outside of this range, are clamped within the range.
    //servo.read() will return the last angle written to the servo-arm. 
    //In order to attach something to the servo, so that it rotates with the servo-arm, simply make the object you wish to rotate, a child of either: Servo-rotationCenter or Servo-arm. 
    //Make sure to take into account the position of the object relative to Servo-rotationCenter. The rotated object will rotate positively around the Y-axis (up) of the Servo-rotationCenter gameobject.


    int lMotorP = 0;
    int lMotorN = 1;
    int rMotorP = 2;
    int rMotorN = 3;
    int pingPin = 6;

    int length = 3; //The length of the robot
    int minDist = 10; //The distance from the robot within which we want it to react
    int speed = 100; //The duty cycle written to the motors, regulates speed goes from 0 to 255
    int turnSpeed = 38;
    int turn = 0;
    float tol = (float)10.0;
    bool passed = false;
    bool onTrack = true;

    IEnumerator setup()
    {
        //Your code goes here:
        //set servo to point straight
        servo.write(90);


        //Example of delay:
        Debug.Log("pre-delay log");
        yield return delay(2000); //2 second delay
        //Your code ends here -----

        //following region ensures delay-functionality for setup() & loop(). Do not delete, must always be last thing in setup.
        #region PremadeSetup
        yield return StartCoroutine(loop()); ;
        #endregion PremadeSetup
    }

    IEnumerator loop()
    {
        //Your code goes here:

        //Debug.Log("Light diff: " + getLightDifference());
        //Debug.Log("Light left: " + senseDarknessLeft());
        //Debug.Log("Light right: " + senseDarknessRight());
        if (onTrack)
        {
                if ((senseDarknessLeft() > senseDarknessRight()) && getLightDifference() > tol)
                {
                    //Left reading was largest
                    //Go clockwise 
                    rotateClockwise(turnSpeed);
                    yield return delay(100);
                }
                else if ((senseDarknessRight() > senseDarknessLeft()) && getLightDifference() > tol)
                {
                    //Right reading was largest
                    //Go counterclockwise 
                    //Debug.Log("turn cc");
                    rotateCounterclockwise(turnSpeed);
                    yield return delay(100);
                }
                else if ((getLightDifference() < tol + 1) && !somethingTooClose())
                {
                    //Debug.Log("go forwards");
                    goForward(speed);
                }

            if (somethingTooClose())
            {
                if (passed == false)
                {
                    passed = true;
                    goBackward(speed);
                    yield return delay(400);
                    rotateCounterclockwise(speed);
                    yield return delay(1600);
                    goForward(speed);
                    yield return delay(1000);
                    rotateClockwise(speed);
                    yield return delay(600);
                    goForward(speed);
                    yield return delay(800);
                    rotateClockwise(speed);
                    yield return delay(830);
                    goForward(speed);
                    yield return delay(600);
                    rotateCounterclockwise(speed);
                    yield return delay(900);
                    goBackward(speed);
                    yield return delay(500);
                    rotateCounterclockwise(speed);
                    yield return delay(600);

                    stand();
                }
            }
        }

        if (onTrack && ((senseDarknessLeft() < 200) && (senseDarknessRight() < 200)))
        {
            Debug.Log("offtrack");
            stand();
            onTrack = false;
        }

        if (!onTrack)
        {
            if (!somethingTooClose())
            {
                goForward(speed);
                yield return delay(100);
            }
            if (somethingTooClose())
            {
                stand();
                look(180);
                yield return delay(1200);
                Debug.Log(somethingTooClose());
                if (!somethingTooClose())
                {
                    rotateClockwise(speed);
                    look(90);
                    yield return delay(1450);
                }
                Debug.Log(somethingTooClose());
                if (somethingTooClose())
                {
                    Debug.Log("venstre");

                    look(0);
                    yield return delay(2000);
                    if (!somethingTooClose())
                    rotateCounterclockwise(speed);
                    look(90);
                    yield return delay(2450);
                }
            }
        }


        //Following region is implemented as to allow "yield return delay()" to function the same way as one would expect it to on Arduino.
        //It should always be at the end of the loop()-function, and shouldn't be edited.
        #region DoNotDelete
        //Wait for one frame
        yield return new WaitForSeconds(0);
        //New loop():
        yield return loop();
        #endregion DoNotDelete 
    }

    //Gets the absolute difference in light over the two light dependent resistors
    float getLightDifference()
    {
        return abs((long)((analogRead(4)) - (analogRead(5))));
    }

    //Robot must follow a dark line on a light background
    //Use this to measure the darkness using the LDR
    float senseDarknessLeft()
    {
        return (float)(analogRead(4));
    }
    //Senses the level of darkness on the right side of the robot
    float senseDarknessRight()
    {
        return (float)(analogRead(5));
    }

    //Looks to a certain degree of rotation
    void look(int angle)
    {
        if (angle > 0)
        {
            servo.write(angle);
            delay(150);
        }
        else
        {
            servo.write(0);
        }
    }

    //This code section is gotten from a public arduino demo project using the sonar sensors
    bool somethingTooClose()
    {
        // establish variables for duration of the ping,
        // and the distance result in inches and centimeters:
        int cm;
        ulong duration;
        duration = pulseIn(pingPin);

        // convert the time into a distance
        cm = (int)microsecondsToCentimeters((long)duration);

        if (cm == 0)
        {
            return false;
        }

        return (cm <= (length + minDist)); //If something is too close
    }


    long microsecondsToCentimeters(long microseconds)
    {
        // The speed of sound is 340 m/s or 29 microseconds per centimeter.
        // The ping travels out and back, so to find the distance of the
        // object we take half of the distance travelled.
        return microseconds / 29 / 2;
    }

    //Rotates the robot around its own axis
    void rotateClockwise(int speed)
    {
        analogWrite(lMotorP, speed);
        analogWrite(lMotorN, 0);
        analogWrite(rMotorP, 0);
        analogWrite(rMotorN, (int)(speed / 1.3));
    }

    //Rotates the robot around its own axis
    void rotateCounterclockwise(int speed)
    {
        analogWrite(lMotorP, 0);
        analogWrite(lMotorN, (int)(speed / 1.3));
        analogWrite(rMotorP, speed);
        analogWrite(rMotorN, 0);
    }

    //Tells the robot to move forwards
    void goForward(int speed)
    {
        analogWrite(lMotorP, speed);
        analogWrite(lMotorN, 0);
        analogWrite(rMotorP, speed);
        analogWrite(rMotorN, 0);
    }

    //Instructs the robot to move in reverse
    void goBackward(int speed)
    {
        analogWrite(lMotorP, 0);
        analogWrite(lMotorN, speed);
        analogWrite(rMotorP, 0);
        analogWrite(rMotorN, speed);
    }

    //Stops the robot
    void stand()
    {
        analogWrite(lMotorP, 0);
        analogWrite(lMotorN, 0);
        analogWrite(rMotorP, 0);
        analogWrite(rMotorN, 0);
    }

Problemer

Der var i starten problemer med at koden nogle gange af uforklarlige årsager crashede unity helt, hvilket gjorde det meget svært at fejlsøge og rette i koden, dette betød at koden blev ændret lidt og derfor afviger fra vores tinkerCad kode. Dertil kommer at unity opførte sig underligt, det virkede som om at robotten sad fast når den skulle bakke, der er et problem med det “free rolling wheel”, det er nemlig det der skaber en stor modstand, hvilket viste sig at være fordi at hjulet anvendte et fixed joint i stedet for et configurable joint. Alt i alt var det sværeste primært at få unity til at opføre sig, som ønsket. Men efter utallige crashes og lang tids fejlsøgning, lykkedes det endelig.

Videoen

Som set i videoen er vores rekordtid omkring 40 Sekunder.

Konklusion

Robotten som konstrueret i Tinkercad havde nogle uforudsete problemer da denne opbygning of program blev placeret i den simulerede verden. Det blev herunder klart, at banen indeholdte forhindringer som robotten ikke kunne omgås. Dette indebærer et stykke hvor linien på jorden fører ind i en væg, og et stykke af banen hvor der ikke er optegnet nogen linje. Dette voldte naturlige problemer da Tinkercad konstruktionen baserede sig på fundamentale antagelser draget ud fra opgavebeskrivelsen, disse værende primært at robotten blot skulle følge en linje på gulvet, og undgå vægge.

Koden for Unity simulationen ses ovenover og afviger i visse aspekter fra den angivet for Tinkercad konstruktionen. Den primære afvigelse er den, at robotten benytter sonar sensoren til at se omkring sig, for at indstille sin krop i den rette vinkel for at køre, uden at ramme ind i objekter (mere end højst nødvendigt).

Det må konkluderes at simulationen opfylder målet for opgaven, da den her kommer i mål, og benytter passende sensorer og styresystem, til at omgås omgivelserne på bedste vis.

Konstruktionen i Tinkercad, opfylder opgavebeskrivelsens mål, for sensor-aktuator forholdene, da den her vil køre efter sine indtryk af omverdenen. Dog har den huller, som det, at den ikke tager højde for eventuelle fælder på banen, som stykker uden linje på gulvet, eller linjer der fører ind i en væg. Det skal dog siges at konstruktionen i Tinkercad styres ligesom den i Unity, og derfor vil kunne håndtere ændringerne i kodebasen.

Forbedringer

Tinkercad:

Helt ideelt vil robotten orientere sig ifht alle objekter omkring den, det vil sige at den skulle rotere sin sonar sensor 180 grader frem og tilbage, for at holde maksimal afstand til alle objekter.

optimalt ville robotten også udnytte en søgealgoritme til at finde vej, uden at køre i ring, det kunne gøres ved eksempelvis A*. Da denne vil inkludere en heuristik funktion, som gør nye stier mere attraktive end stier robotten allerede har besøgt. 

Hastigheden for robottens kørsel kan altid redigeres, dette er jo blot en enkelt variable som enstemmigt styrer duty cyklussen altså den procentmæssige tid hvori robottens h-bro modtager et højt signal ifht et lavt signal. Som beskrevet i tidligere afsnit vil robotten på nuværende tidspunkt kører ca 8 km/t med hjul på 10 cm i radius, hvilket er en forholdsvis høj hastighed, men muligvis også for store hjul, det er svært at vurdere uden en testkørsel i den virkelige verden, med en fysisk robot.

Unity:

Det er klart ud fra videoen at styringen af robotten ikke er ideel, det at robotten skal stoppe op for at dreje, sænker dens rekordtid dramatisk. Robotten kunne ideelt holder sig opdateret på objekter løbende ved at rotere sonar sensoren konstant og mappe indtrykkene til et indre kort over omverdenen. Robotten kunne også benytte en passende søgealgoritme A* eller lignende til bedre at vurdere den korrekte og hurtigste handling for bedst at når målet, hvilket vil hæve vores rekordtid markant.

More

Speedy Gonzales

Af Mads Due Kristensen og Magnus Mortensen

Dette blogindlæg vil beskrive hvordan man kan lave en robot der kan følge en linje så længe den er tilsted samt følge vække når der ikke er nogen linje. For at kunne lave robotten skal der bruges følgende komponenter:

  • 2x DC gearmotor
  • 1x Servomotor
  • 1x 9V batteri
  • 1x Arduino uno
  • 1x Arduino Ultrasonic Sensor
  • 1x 5V spændingsregulator (LM7805)
  • 1x H-bro (L2930)
  • 2x LDR modstand
  • 2x Hvide LED
  • 2x 450 ohm modstand
  • 2x 10 Kilo ohm modstand
  • Ledninger

Robottens mål

Målet med denne robot er at kunne gennemføre banen herunder på hurtigst mulig tid ved at følge linjen og undgå væggene. For kunne udføre denne opgave har vi valgt at konfigurer og sammensætte de ovenstående komponenter på følgende måde.

Opbygning af hardware – fra TinkerCad

Robottens hardware består af disse subsystemer: Motor og h-bro, LDR modstande og ultrasoniske sensor, Arduino og strømforsyning. På billedet nedenfor ses vores robot setup fra TinkerCad.

Motor

Robotten har to gearmotorer som bliver styret med en h-bro.  I dette tilfælde er h-broen en færdig chip, men hvis dette ikke er tilgængeligt, kan den også blive bygget selv. Chippen kan styres, som set  i dette tilfælde, af en Arduino ved at sætte højt signal på en af input benene. Det er vigtigt at man ikke sætter et højt signal, på begge input ben, på samme side af h-broen, da den påhæftet kan gå i stykker af det. H-broen tillader strømmen at løbe gennem motoren i en retning hvis inputtet på en af dens ben er højt. Hvis inputtet er højt på begge input ben af H-broen, så kan strømmen løbe begge retninger gennem motoren, hvilket det kan ødelægge den.  

Spændingsregulator

I denne robot er det eneste der skal bruge 9V de to motorer og resten 5V. Dette betyder at vi bruger en 5V spændingsregulator som er sluttet til et 9V batterie, hvor outputtet på 5V er sat til breadboards ydre kanaler så de resterende komponenter nemt kan benytte det. Da de to motorer styres af en h-bro skal den kunne levere 9V. H-broens interne komponenter kan kun klare 5V så for at konfigurer h-broen rigtig kan man med fordel tage et kig i databladet, http://www.ti.com/lit/ds/symlink/l293.pdf. Ved at give 5V spænding til H-broens interne komponenter på pin 8, og samtidigt give h-broen 9V på pin 16, så sikre vi os at h-broen kan leverer den nødvendige spænding til motorerne, uden at den selv går i stykker. De resterende komponenter som kun kræver 5V er Arduinoen, servomotoren og den ultrasoniske sensor. Selv om Arduinoen har en indbygget spændingsregulator som gør det muligt at forbinde den direkte til 9V, så har vi valgt at bruge en 5V spændingsregulator til at levere den de ønskede 5V til arduinoen. Dette udretter derfor ikke noget, men giver en ekstra sikkerhed, hvis Arduionens spændingsdeler skulle være i stykker.

Arduinoen

Arduinoen er hjernen i denne robot. Ved hjælp af dens digitale og analog input kan information fra sensorerne på robotten behandles. Arduinoen er også grunden til at hastigheden af motorerne kan styres. For at kunne styre de to motorens hastighed bruges Arduionoens pins som understøtter PMW. Disse pins er derfor forbundet til h-broen da PWM gør det muligt at bruge værdier fra 0 til 1023. Ved at bruge disse værdier kan man fra Arduinoen bestemme hvor hurtigt motoren skal køre. 

LDR

For at robotten kan følge en linje, skal den først kunne se linjen. Til dette bruger vi to ldr modstande og to hvide LED’er. De to ldr modstande er placeret med lidt mellemrum alt efter hvor tyk stegen den skal følge er. Når robotten følger linjen perfekt så vil LDR modstandene have en lav modstand da der reflekteres meget lys fra det hvide underlag. Dette resultere i at arduinoen modtager en høj værdi. Hvis en af LDR modstandene derimod kommer ind over linjen, så vil der være en høj modstand som gør at Arduinoen modtager en lav værdi, da der ikke reflekteres lige så meget lys tilbage. De hvide LED’er er forbundet til 9V med en 450 ohm modstand for at opnå de rette strøm til led’en på 0.2 mA. Dette er udregnet ved brug af ohms lov:

De to LDR modstande er forbundet til ground med en 10 Kilo ohm modstand, denne opstilling er også kendt som en spændingsdeler. LDR modstandene ændre deres modstand som resultat af at lysnettet ændre sig. Men dette kan arduinoen ikke opfange da arduinoen opfanger ændringer i spændingen, som er konstant uden spændingsdelere, da hele spændingsfaldet altid vil være over LDR modstande. Men nu da der bruges en spændingsdeler vil spændingen ændre sig når modstanden gør og det kan udregnes. Hvis vi gør ud fra at ldr modstande har en modstand på 300 Ohm når der er lys og 10M Ohm når der ikke er lys er spændingen som følger:

Ultrasonisk sensor

For at kunne dreje på den ultrasoniske sensor bruger vi en servomotor. Den og sensoren skal bruge 5V input spænding så de er koblet på den samme 5V spændingsregulator som arduinoen er. For at kunne styre begge dele skal deres signal pin kobles til arduino digitale PWM pins.

Robottens fysiske opbygning

LDR modstandene er placeret under robotten så de kan se underlaget og de er placeret med et mellemrum der gør at de ikke ikke opfanger linjen når robotten er ret på linjen. Derudover er LDR’ne og hjulene placeret i hver sin ende af robotten. Dette gør at hjulene ikke skal dreje særlig meget for at ændre position på fronten af robotten. Derfor bliver det nemmere at korrigere når LDR’ne rammer linjen. Servoen og den ultrasonisk sensor er placeret tilbage på robotten, så sensoren ikke stikker ud over robotten front. Dette er gjort så vi få det mest retvisende billede af, hvor langt væk forhindringer er.

Tinkercad kode

Koden der er lavet i tinkercad er forholdsvis simpel og viser kun hvordan man kan styre de enkelte komponenter. Den reelle logik er implementeret i Unity.

Ethvert arduino program består af en setup function. Det er typisk her man sætter sine mods og andre ting, der skal til for at resten af programmet kan fungere. I vores setup åbnes der for seriel kommunikation på frequence 9600 og så sættes det forskellige pin til enten inputs eller outputs.

// For at kunne styre en servo i Arduino skal dette bibliotek importeres
#include <Servo.h>

// Her laves en instans af servo
Servo servo_2;
// Ping pin til ultrasonisk sensor
const int pingPin = 2;

int sonicDistCm, rightLDR, leftLDR;

// Her opsættes de forskellige pinners
void setup()
{
  // Åbner for serielle kommunikation på frekvens 9600
  Serial.begin(9600);
  // Forbinder servoen til pin 9
  servo_2.attach(9);
  
  //input
  pinMode(A4,INPUT);
  pinMode(A5,INPUT);
  
  //output
  pinMode(4,OUTPUT);
  pinMode(5,OUTPUT); 
  pinMode(6,OUTPUT);
  pinMode(10,OUTPUT); 
  pinMode(11,OUTPUT);
  
}

Loop funktionen er med i alle programmer. Denne funktion bliver gentaget hele tiden. Det første vi gør her er at aflæse værdierne fra de to ldr modstande og udregne afstanden som den ultrasoniske sensor for målt.

void loop() {
  sonicDistCm = measureDistance();
  rightLDR = analogRead(A5);
  leftLDR = analogRead(A4);
  
  followLine(rightLDR, leftLDR);

  sonicDistCm = measureDistance();
  Serial.println(sonicDistCm);
  
}

Den næste funktion er lavet for at vise, hvordan man ville kunne styre sine motorer og derved følge en linje. Selvom at pin 11 og 5 er digitale pins laves der et analogt write. Dette kan lade sig gøre da disse to pins understøtter PWM. Og det er PWM der gør det muligt at styre motorens hastighed.

void followLine(int rightLDR, int leftLDR) {
            // Styre motor hastigheden ved sende et analogt signal
	analogWrite(11,rightLDR);
  	analogWrite(5,leftLDR);
}

De næste 5 funktioner er hjælper funktioner som gør det nemmere at styre robotten, da de fjerner behovet for at huske på hvilke pins der gør hvad.

void left() {
	digitalWrite(11,HIGH);
  	digitalWrite(6,HIGH);
}

void right() {
	digitalWrite(5,HIGH);
  	digitalWrite(10,HIGH);  
}

void forward() {
  	digitalWrite(11,HIGH);
  	digitalWrite(5,HIGH);
}

void back() {
  	digitalWrite(10,HIGH);
  	digitalWrite(6,HIGH);	
}

void stop() {
	digitalWrite(11,LOW);
  	digitalWrite(5,LOW);
            digitalWrite(10,LOW);
  	digitalWrite(6,LOW);
}

I starten af programmet lavede vi en instance af servo fra et ekstern bibliotek. Denne instance har en metode, hvor der bruges værdier fra 0 til 180 til at dreje servoen.

void turnServo(int amount) {
  // Servoen kan dreje fra 0 til 180 grader.
  servo_2.write(amount);
}

For at kunne lave en måling med den ultrasoniske sensor skal den trigPin sættes lavt i 2 millisekunder. Hvorefter den skal sættes højt i 5 millisekunder for at generere en ultrasonisk bølge. Nu kan pulseIn funktionen bruges til at aflæse hvor lang tid bølge er om at komme tilbage og denne tid kan så omregnes til cm.

int measureDistance(){
  long duration, cm;
  pinMode(pingPin, OUTPUT);
  digitalWrite(pingPin, LOW);
  delayMicroseconds(2);
  digitalWrite(pingPin, HIGH);
  delayMicroseconds(5);
  digitalWrite(pingPin, LOW);
  pinMode(pingPin, INPUT);
  duration = pulseIn(pingPin, HIGH);
  cm = microsecondsToCentimeters(duration);
  return cm;
}

long microsecondsToCentimeters(long microseconds) {
   return microseconds / 29 / 2;
}

Og her er den samlede kode:

// For at kunne styre en servo i Arduino skal dette bibliotek importeres
#include <Servo.h>

// Her laves en instans af servo
Servo servo_2;
// Ping pin til ultrasonisk sensor
const int pingPin = 2;

int sonicDistCm, rightLDR, leftLDR;

// Her opsættes de forskellige pinners
void setup()
{
  // Åbner for serielle kommunikation på frekvens 9600
  Serial.begin(9600);
  // Forbinder servoen til pin 9
  servo_2.attach(9);
  
  //input
  pinMode(A4,INPUT);
  pinMode(A5,INPUT);
  
  //output
  pinMode(4,OUTPUT);
  pinMode(5,OUTPUT); 
  pinMode(6,OUTPUT);
  pinMode(10,OUTPUT); 
  pinMode(11,OUTPUT);
  
}
void loop() {
  sonicDistCm = measureDistance();
  rightLDR = analogRead(A5);
  leftLDR = analogRead(A4);
  
  followLine(rightLDR, leftLDR);

  sonicDistCm = measureDistance();
  Serial.println(sonicDistCm);
  
}

void followLine(int rightLDR, int leftLDR) {
            // Styre motor hastigheden ved sende et analogt signal
	analogWrite(11,rightLDR);
  	analogWrite(5,leftLDR);
}

void left() {
	digitalWrite(11,HIGH);
  	digitalWrite(6,HIGH);
}

void right() {
	digitalWrite(5,HIGH);
  	digitalWrite(10,HIGH);  
}

void forward() {
  	digitalWrite(11,HIGH);
  	digitalWrite(5,HIGH);
}

void back() {
  	digitalWrite(10,HIGH);
  	digitalWrite(6,HIGH);	
}

void stop() {
	digitalWrite(11,LOW);
  	digitalWrite(5,LOW);
            digitalWrite(10,LOW);
  	digitalWrite(6,LOW);
}

void turnServo(int amount) {
  // Servoen kan dreje fra 0 til 180 grader.
  servo_2.write(amount);
}

int measureDistance(){
  long duration, cm;
  pinMode(pingPin, OUTPUT);
  digitalWrite(pingPin, LOW);
  delayMicroseconds(2);
  digitalWrite(pingPin, HIGH);
  delayMicroseconds(5);
  digitalWrite(pingPin, LOW);
  pinMode(pingPin, INPUT);
  duration = pulseIn(pingPin, HIGH);
  cm = microsecondsToCentimeters(duration);
  return cm;
}

long microsecondsToCentimeters(long microseconds) {
   return microseconds / 29 / 2;
}

Opbygning af programmet

Unity programmet som kan findes nedenfor,  indeholder flere kommentere som går mere i dybten omkring funktionaliteten. Derfor dykker dette afsnit ikke helt ned i kodens enkelte elementer, men vil i stedet forsøge at give overordnet indblik i, hvad programmet gør.

    /*
     * Declartion of a bunch of boolean values, which are used to control the robots behaviour.
     * Only start is true as the rest will be altered depending on the map the robot will face.
     */
    bool start = true;
    bool distanceOk = false;
    (bool, bool, bool) goTo;
    bool allWall = false;
    bool forceFront = false;
    bool fromLdr = false;
    bool modeLeft = false;
    bool modeRight = false;
    bool modeFront = false;
    bool modeBack = false;
    bool modeDist = fal
    bool lastGoingLeft = false;
    bool lastGoingRight = false;
    bool rightAvailable = false;
    bool leftAvailable = false; 
    bool passedObstacle = false; 
    bool ostacleFound = false;
    int ostaclesPassed = 0;

Ligesom arduino programmet bruges der her en setup og loop funktion. Vi benytter dog ikke rigtigt setup metoden til noget. I stedet instantierer vi en masse varibaler som bliver brugt til at afgøre hvilket stadie robotten er i.

Den næste funktion er “hoved” loop’et, og det er her programmets logisk ligger. Vores program kan ses som en statemachine, hvilket forklarer de 3 if statements i dette loop. Disse if satements bruges til at igangsætte den rigtige adfærd, alt efter hvilken situation robotten befinder sig i.

Generelt set kan vores robot være i 3 stadier. Start, følg linjen og fri kørende. Disse 3 stadier tackler alle problemer som forekommer på den bane robotten kører på.

        if (start)
        {
            // wait for the servo to turn as it starts facing left, and tell the robot to start following the line.
            servo.write(90);
            yield return delay(1500);
            start = false;
            fromLdr = true;
        }

Start stadiet gør faktisk kun at servoen drejer på plads og sætter robotten op til at følge linjen.

        // if it is not the start just follow the line until a obstacle occurs.
        else if (fromLdr)
        {
            align();
            followLine();
        }

Direkte efter start stadiet begynder robotten at følge linjen, det gør den ved at bruge align og followline metoderne.

/*
         * A state where the robot cannot follow the line any more.
         * Main purporse is to guide the robot past obstacles and back to the line.
         */
        else if (modeDist)
        {
            // while there is no obstacle right in front of the robot, then just drive forward.
            dist = pulseIn(6);
            while (dist > 300 || dist == 0)
            {
                front();
                yield return delay(2);
                dist = pulseIn(6);
            }
            
            //obstacle found, check options
            
            stop();
            //check right 
            servo.write(180);
            yield return delay(1500);
            dist = pulseIn(6);
            if (dist > 1000 || dist == 0)
            {
                rightAvailable = true;
                Debug.Log("right is available");
            }
            
            
            //check left
            servo.write(0);
            yield return delay(3000);
            dist = pulseIn(6);
            if (dist > 800 || dist == 0)
            {
                leftAvailable = true;
                Debug.Log("Left is available");
            }
            
            /*
             * Made specifically to beat this map
             * If no ostacle have been passed yet, then drive around it on the right side.
             * functionality of driving around on the left side is not included to save space, but is basically the same.
             */
            if (rightAvailable && ostaclesPassed < 1)
            {
                //turn right and drive until the obstacle is not in the path anymore.
                right();
                yield return delay(1400);
                stop();
                dist = pulseIn(6);
                while (dist < 350 && dist != 0)
                {
                    Debug.Log("i am in the loop");
                    front();
                    yield return delay(2);
                    dist = pulseIn(6);
                }
                
                // Turn back to orginal direction.
                yield return delay(900);
                stop();
                left();
                yield return delay(1600);
                
                // keep driving forward until the obstacle is passed. 
                while (!passedObstacle)
                {
                    front();
                    yield return delay(2);
                    dist = pulseIn(6);

                    if (dist < 300 && dist != 0)
                    {
                        ostacleFound = true;
                    }

                    if (ostacleFound && (dist > 300 || dist == 0))
                    {
                        passedObstacle = true;
                    }
                    
                }
                yield return delay(900);
                stop();
                
                // Now that the obstacle is passed, then drive back to the line
                left();
                servo.write(90);
                yield return delay(1600);
                front();
                yield return delay(2);
                
                // keep driving until the line is found.
                while (ldrLeft > 900 || ldrRight > 900)
                {
                    ldrLeft = analogRead(4);
                    ldrRight = analogRead(5);
                    Debug.Log("Last loop");
                    yield return delay(2);
                }
                
                // The line is now found, correct robots position in order for it to follow the line again.
                yield return delay(200);
                stop();
                ldrLeft = analogRead(4);
                while (ldrLeft > 900)
                {
                    right();
                    yield return delay(2);
                    ldrLeft = analogRead(4);
                }
                
                // Go back to follow the line mode and count the amount of obstacles passed.
                modeDist = false;
                fromLdr = true;
                Debug.Log("I am done");
                rightAvailable = false;
                leftAvailable = false;
                ostaclesPassed++;
            }
            
            /*
             * Turn the robot to the right if it meets an obstacle.
             * Made in order to beat the last part of the course, where we know no line is present anymore
             */
            if (rightAvailable && ostaclesPassed >= 1)
            {
                right();
                servo.write(90);
                yield return delay(1500);
                rightAvailable = false;
                leftAvailable = false;

            }
            // turn the robot to the left if it meets an obstacle.
            if (leftAvailable)
            {
                Debug.Log("left!");
                left();
                servo.write(90);
                yield return delay(1800);
                leftAvailable = false;
                rightAvailable = false;

            }    
        }

Det sidste stadie er det frikørende stadie, dette stadie indenholder funktionaliten der får robotten til at kunne undgå forhindringer. I vores tilfælde er dette stadie blevet “hardcoded” til at gennemføre præcist denne bane. Det betyder at koden ikke er fleksibel nok til at robotten ville kunne gennemføre vilkårlige baner, hvilket er et problem. I det frikørende stadie tjekker robot hvilken side den kan køre til, når en forhindring dukker op. Derefter drejer den mod den ledige side for derefter at køre ligeud indtil en ny forhindringer bliver ramt.

/*
     * A method used in order to align the robot, so it will follow the line in with making to many corrections, as those takes time
     * If a correction is made, and the value of the LDR which side were corrected is still low, then assume the end of the line has been reached.
     */
    void align()
    {
        int ldrRight = analogRead(5);
        int ldrLeft = analogRead(4);
        if (lastGoingLeft)
        {
            left();
            if (ldrRight < 500)
            {
                fromLdr = false;
                modeDist = true;
            }
        }
        else if (lastGoingRight)
        {
            right();
            if (ldrRight < 500)
            {
                fromLdr = false;
                modeDist = true;
            }
        }
    }

Når robotten prøver at rette sig op, så husker den, om sidste gang var et sving mod venstre eller højre. Derfor drejer den blot den modsatte vej indtil den er tilbage på linjen igen.

/*
     * Method used to follow the line.
     * It is insprired by the "shy" Braitenberg Vehicle, which means, if one of the sensors picks up the line, then it will try to "avoid" it.
     * If the difference between the two sensors is small, then speed up to get a better finish time.
     * if input on both sensors are low, or the ultrasonic sensor has a low value, then change the mode to free driving mode. 
     */
    void followLine()
    {
        int ldrRight = analogRead(5);
        int ldrLeft = analogRead(4);
        int difLR = (ldrLeft - ldrRight);
        int difRL = (ldrRight - ldrLeft);
        ulong dist = pulseIn(6);

        int leftSpeed = (ldrLeft / 4) / 4;
        int rightSpeed = (ldrRight / 4) / 4;

        if (difLR < 200 && difRL < 200)
        {
            leftSpeed *= 3;
            rightSpeed *= 3;
        }
        else if (difRL > difLR)
        {
            lastGoingLeft = true;
            lastGoingRight = false;
        }
        else if (difRL < difLR)
        {
            lastGoingRight = true;
            lastGoingLeft = false;
        }

        analogWrite(3, leftSpeed);
        analogWrite(1, rightSpeed);
        fromLdr = true;

        if ((ldrLeft < 350 && ldrRight < 350) || (dist < 250 && dist != 0)) 
        {
            stop();
            fromLdr = false;
            modeDist = true;
            return;
        }
    }

Denne metoden bruges hvis robotten er i gang med at følge en linje. Koden der får robotten til at følge linjen er inspireret af Braitenberg Vehicles. I dette tilfælde det Braitenbreg Vehicle som vil jagte lyset i stedet for at undgå det.

Vi benytter dog ikke direkte de aflæste værdier fra LDR modstandene da robotten ville være alt for aggressiv. Værdierne nedskaleres så robotten kan dreje sikker. Hvis linjen er lige bruges de aflæste værdier dog, for at få robotten hastighed op. Som Braintenberg teori siger, så er det værdien fra den højre modstand der bliver brugt til at styre den venstre motor og det omvendt for den anden side. Braitenberg bliver brugt for at give vores robot noget der ligner avanceret adfærd uden at det reelt er avanceret.

Og her er den samlede kode:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using System;

public class ArduinoMain : MonoBehaviour
{
    public Breadboard breadboard;
    public Servo servo;
    //On included/premade Arduino functions:
    //delay(timeInMilliseconds) : use "yield return delay(timeInMilliseconds)", to get similar functionality as delay() in arduino would give you.

    //map() : works exactly as on Arduino, maps a long from one range to another. 
    //If you want to get an int or a float from the map()-function, you can cast the output like this: (int)map(s, a1, a2, b1, b2) or (float)map(s, a1, a2, b1, b2) 

    //millis() : returns the time as a ulong since the start of the scene (and therefore also the time since setup() was run) in milliseconds.

    //If you want to do something similar to serial.println(), use Debug.Log(). 

    //analogWrite() and analogRead() works as they do in arduino - remember to give them correct input-values.
    //digitalRead() and digitalWrite() writes and returns bools. (High = true). 
    //LineSensors have both write-functions implemented, motors/hbridge have both read-functions implemented.

    //The console will display a "NotImplementedException" if you attempt to write to sensors or read from motors. 

    IEnumerator setup()
    {
        //Your code goes here:

        //Example of delay:
        Debug.Log("pre-delay log");
        yield return delay(2000); //2 second delay
        Debug.Log("After delay");

        //Your code ends here -----

        //following region ensures delay-functionality for setup() & loop(). Do not delete, must always be last thing in setup.
        #region PremadeSetup
        yield return StartCoroutine(loop()); ;
        #endregion PremadeSetup
    }

    /*
     * Declartion of a bunch of boolean values, which are used to control the robots behaviour.
     * Only start is true as the rest will be altered depending on the map the robot will face.
     */
    bool start = true;
    bool distanceOk = false;
    (bool, bool, bool) goTo;
    bool allWall = false;
    bool forceFront = false;
    bool fromLdr = false;
    bool modeLeft = false;
    bool modeRight = false;
    bool modeFront = false;
    bool modeBack = false;
    bool modeDist = fal
    bool lastGoingLeft = false;
    bool lastGoingRight = false;
    bool rightAvailable = false;
    bool leftAvailable = false; 
    bool passedObstacle = false; 
    bool ostacleFound = false;
    int ostaclesPassed = 0;

    //Main loop which control the robot
    IEnumerator loop()
    {
        if (start)
        {
            // wait for the servo to turn as it starts facing left, and tell the robot to start following the line.
            servo.write(90);
            yield return delay(1500);
            start = false;
            fromLdr = true;
        }
        

        //Example of reading the different values picked up by sensors.
        int ldrLeft = analogRead(4);
        int ldrRight = analogRead(5);
        ulong dist = pulseIn(6);
        Debug.Log("Ldr left: " + ldrLeft + " right: " + ldrRight + "  dist: " + dist);

        // if it is not the start just follow the line until a obstacle occurs.
        else if (fromLdr)
        {
            align();
            followLine();
        }
        
        /*
         * A state where the robot cannot follow the line any more.
         * Main purporse is to guide the robot past obstacles and back to the line.
         */
        else if (modeDist)
        {
            // while there is no obstacle right in front of the robot, then just drive forward.
            dist = pulseIn(6);
            while (dist > 300 || dist == 0)
            {
                front();
                yield return delay(2);
                dist = pulseIn(6);
            }
            
            //obstacle found, check options
            
            stop();
            //check right 
            servo.write(180);
            yield return delay(1500);
            dist = pulseIn(6);
            if (dist > 1000 || dist == 0)
            {
                rightAvailable = true;
                Debug.Log("right is available");
            }
            
            
            //check left
            servo.write(0);
            yield return delay(3000);
            dist = pulseIn(6);
            if (dist > 800 || dist == 0)
            {
                leftAvailable = true;
                Debug.Log("Left is available");
            }
            
            /*
             * Made specifically to beat this map
             * If no ostacle have been passed yet, then drive around it on the right side.
             * functionality of driving around on the left side is not included to save space, but is basically the same.
             */
            if (rightAvailable && ostaclesPassed < 1)
            {
                //turn right and drive until the obstacle is not in the path anymore.
                right();
                yield return delay(1400);
                stop();
                dist = pulseIn(6);
                while (dist < 350 && dist != 0)
                {
                    Debug.Log("i am in the loop");
                    front();
                    yield return delay(2);
                    dist = pulseIn(6);
                }
                
                // Turn back to orginal direction.
                yield return delay(900);
                stop();
                left();
                yield return delay(1600);
                
                // keep driving forward until the obstacle is passed. 
                while (!passedObstacle)
                {
                    front();
                    yield return delay(2);
                    dist = pulseIn(6);

                    if (dist < 300 && dist != 0)
                    {
                        ostacleFound = true;
                    }

                    if (ostacleFound && (dist > 300 || dist == 0))
                    {
                        passedObstacle = true;
                    }
                    
                }
                yield return delay(900);
                stop();
                
                // Now that the obstacle is passed, then drive back to the line
                left();
                servo.write(90);
                yield return delay(1600);
                front();
                yield return delay(2);
                
                // keep driving until the line is found.
                while (ldrLeft > 900 || ldrRight > 900)
                {
                    ldrLeft = analogRead(4);
                    ldrRight = analogRead(5);
                    Debug.Log("Last loop");
                    yield return delay(2);
                }
                
                // The line is now found, correct robots position in order for it to follow the line again.
                yield return delay(200);
                stop();
                ldrLeft = analogRead(4);
                while (ldrLeft > 900)
                {
                    right();
                    yield return delay(2);
                    ldrLeft = analogRead(4);
                }
                
                // Go back to follow the line mode and count the amount of obstacles passed.
                modeDist = false;
                fromLdr = true;
                Debug.Log("I am done");
                rightAvailable = false;
                leftAvailable = false;
                ostaclesPassed++;
            }
            
            /*
             * Turn the robot to the right if it meets an obstacle.
             * Made in order to beat the last part of the course, where we know no line is present anymore
             */
            if (rightAvailable && ostaclesPassed >= 1)
            {
                right();
                servo.write(90);
                yield return delay(1500);
                rightAvailable = false;
                leftAvailable = false;

            }
            // turn the robot to the left if it meets an obstacle.
            if (leftAvailable)
            {
                Debug.Log("left!");
                left();
                servo.write(90);
                yield return delay(1800);
                leftAvailable = false;
                rightAvailable = false;

            }
            
            

        }
  

        //Your code ends here -----

        //Following region is implemented as to allow "yield return delay()" to function the same way as one would expect it to on Arduino.
        //It should always be at the end of the loop()-function, and shouldn't be edited.
        #region DoNotDelete
        //Wait for one frame
        yield return new WaitForSeconds(0);
        //New loop():
        yield return loop();
        #endregion DoNotDelete 
    }

    /*
     * A method used in order to align the robot, so it will follow the line in with making to many corrections, as those takes time
     * If a correction is made, and the value of the LDR which side were corrected is still low, then assume the end of the line has been reached.
     */
    void align()
    {
        Debug.Log("ALIGN!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
        int ldrRight = analogRead(5);
        int ldrLeft = analogRead(4);
        if (lastGoingLeft)
        {
            left();
            if (ldrRight < 500)
            {
                fromLdr = false;
                modeDist = true;
            }
        }
        else if (lastGoingRight)
        {
            right();
            if (ldrRight < 500)
            {
                fromLdr = false;
                modeDist = true;
            }
        }
    }

    /*
     * Method used to follow the line.
     * It is insprired by the "shy" Braitenberg Vehicle, which means, if one of the sensors picks up the line, then it will try to "avoid" it.
     * If the difference between the two sensors is small, then speed up to get a better finish time.
     * if input on both sensors are low, or the ultrasonic sensor has a low value, then change the mode to free driving mode. 
     */
    void followLine()
    {
        int ldrRight = analogRead(5);
        int ldrLeft = analogRead(4);
        int difLR = (ldrLeft - ldrRight);
        int difRL = (ldrRight - ldrLeft);
        ulong dist = pulseIn(6);

        int leftSpeed = (ldrLeft / 4) / 4;
        int rightSpeed = (ldrRight / 4) / 4;

        if (difLR < 200 && difRL < 200)
        {
            leftSpeed *= 3;
            rightSpeed *= 3;
        }
        else if (difRL > difLR)
        {
            lastGoingLeft = true;
            lastGoingRight = false;
        }
        else if (difRL < difLR)
        {
            lastGoingRight = true;
            lastGoingLeft = false;
        }

        analogWrite(3, leftSpeed);
        analogWrite(1, rightSpeed);
        fromLdr = true;

        if ((ldrLeft < 350 && ldrRight < 350) || (dist < 250 && dist != 0)) 
        {
            stop();
            fromLdr = false;
            modeDist = true;
            return;
        }
    }

    
    // makes the robot turn left 
    void left()
    {
        if (!modeLeft)
        {
            stop();
            modeLeft = true;
            modeRight = false;
            modeFront = false;
            modeBack = false;
        }
        analogWrite(1, 30);
        analogWrite(2, 30);
    }

    // makes the robot turn right
    void right()
    {
        if (!modeRight)
        {
            stop();
            modeLeft = false;
            modeRight = true;
            modeFront = false;
            modeBack = false;
        }
        analogWrite(0, 30);
        analogWrite(3, 30);
    }
    // makes the robot drive forward
    void front()
    {
        if (!modeFront)
        {
            stop();
            modeLeft = false;
            modeRight = false;
            modeFront = true;
            modeBack = false;
        }
        analogWrite(1, 70);
        analogWrite(3, 70);
    }
    // makes the robot stop 
    void stop()
    {
        digitalWrite(1, false);
        digitalWrite(3, false);
        digitalWrite(0, false);
        digitalWrite(2, false);
    }

    #region PremadeDefinitions
    void Start()
    {
        servo = FindObjectOfType<Servo>();
        if (servo == null)
        {
            Debug.Log("No servo found in the scene. Consider assigning it to 'ArduinoMain.cs' manually.");
        }
        Time.fixedDeltaTime = 0.005f; //4x physics steps of what unity normally does - to improve sensor-performance.
        StartCoroutine(setup());


    }

    IEnumerator delay(int _durationInMilliseconds)
    {
        float durationInSeconds = ((float)_durationInMilliseconds * 0.001f);
        yield return new WaitForSeconds(durationInSeconds);
    }

    public long map(long s, long a1, long a2, long b1, long b2)
    {
        return b1 + (s - a1) * (b2 - b1) / (a2 - a1);
    }

    public ulong millis()
    {
        return (ulong)(Time.timeSinceLevelLoad * 1000f);
    }

    public ulong abs(long x)
    {
        return (ulong)Mathf.Abs(x);
    }

    public long constrain(long x, long a, long b)
    {
        return (x < a ? a : (x > b ? b : x));
    }


    #endregion PremadeDefinitions

    #region InterfacingWithBreadboard
    public int analogRead(int pin)
    {
        return breadboard.analogRead(pin);
    }
    public void analogWrite(int pin, int value)
    {
        breadboard.analogWrite(pin, value);
    }
    public bool digitalRead(int pin)
    {
        return breadboard.digitalRead(pin);
    }
    public void digitalWrite(int pin, bool isHigh)
    {
        breadboard.digitalWrite(pin, isHigh);
    }

    public ulong pulseIn(int pin)
    {
        return breadboard.pulseIn(pin);
    }
    #endregion InterfacingWithBreadboard
}

Hvordan er samspillet mellem mekanik, elektronik og software?

Robotter er på mange måder en replikation af levende væsner. De løser på samme måde som os, problemer ved hjælp af deres mekaniske, elektroniske og software dele. Det er derfor altid essentielt at forstå hvordan disse elementer af robotten spiller sammen. 

På denne robot er det mekaniske ret simpel, da den blot har 3 bevægelige dele. Dette er dens 3 hjul og servomotoren som drejer den ultrasoniske sensor. Disse dele er kendt som, robottens aktuatorer og er det der gør robotten i stand til at interagere med verden omkring sig. I dette tilfælde skal robotten rent fysisk, kun kunne bevæge sig og undgå forhindringer. Derfor er der i dette tilfælde ikke grund til at tilføje ekstra mekaniske dele til robotten.

For at robotten skal bevæge sig langs en linje og undgå at køre ind i ting, har den i høj grad brug for at kunne føle verden omkring sig på en eller anden måde. Her kommer robottens elektroniske dele ind. I denne opgave har robotten 5 elektroniske dele som den bruger til at føle verden omkring sig. Disse er 2 LED’er, 2 LDR’er og en ultrasonisk sensor. Disse elektroniske dele giver alt den information robotten har behov for, til at kunne undgå forhindringer og måle om den stadig føler linjen på jorden. Denne information skal dog bearbejdes før robotten ret faktisk undgår forhindringerne, og det er her robotten sidste del kommer ind i billedet, nemlig softwaren.

Software gør robotten i stand til at træffe beslutninger baseret på den information den har fået via dens elektroniske sensore(LDR og Ultrasonisk). Når softwaren har fundet den bedste løsning, så bruger softwaren de mekaniske dele til at udføre den løsning den har  fundet. I denne opgave skal robotten kun kunne klare sig igennem en enkelt bane, hvilket gør at software ikke behøver at være særlig kompleks for at klare denne opgave. Dog skal det altid huskes på, at når man bygger robotter, så den sværeste del at få en simpel robot til at udføre en kompleks opgave. 

Et konkret eksempel på samspillet mellem alle elementer på denne robot, kunne være LDR modstandene og metoden fra sidste afsnit followline. Med henblik på funktionaliteten så er LDR modstandene placeret, så de er lidt længere væk fra hinanden end linjens brede. Dette er gjort i et forsøg på at gøre followline nemmere at programmere. Havde de været placeret tættere på hinanden, kunne funktionalitet hvor robotten prøver at køre væk fra linjen, blive langt sværere at få til at virke. Der kunne nemlig ske det at, begge LDR modstande endte på linjen samtidigt, hvilket ville gøre det svært at vide, hvilken side vi skulle korrigere på. Havde LDR modstande derimod været længere fra hinanden, ville det også have gjort metoden mere kompliceret. Med mere afstand mellem sig, kunne vi risikere at robotten kom mere ud af kurs i forhold til linjen, før vi fandt ud af det. Det betyder at vi i followline metoden, ville være nødt til at lave større korrigeringer, for at få robotten på rette spor igen. Derfor kan et relativt simpelt valg, som længden mellem to sensorer, skabe unødig kompleksitet. Dette viser altså hvor vigtigt det er at tænke sig om, når man bygger robotten.

Konklusion – hvor godt løser robotten opgaven

Robotten er i stand til at gennemføre banen i de fleste tilfælde på ca. 88 sekunder. Der er dog enkelte gang hvor noget går galt og den ikke opføre sig rigtigt. Robotten opfylder altså alle de givne krav da den kan følge en linje og undgå objekter der skulle komme i vejen for den. Der opstår dog problemer hvis banen skifter natur. Lige nu er noget af logikken hardcoded ind, da vi ikke var i stand til at gennemføre banen uden. F.eks når den kommer til den sidste væg. Her har vi fortalt robotten at den bare skal dreje til højre, da den ikke kan vide hvor målet er. Softwaren er derfor en smule rigid, men uden yderligere information, såsom et punkt til målet som robotten kendte, kunne det ikke lade sig gøre at lave en mere alsidig løsning.

Perspektivering – Hvad kan man gøre for at gøre den bedre?

Det er ikke altid at robotten klare sig gennem banen, så det er klart et sted, hvor robotten kunne forbedres. Det er dog en smule spøjs at der ikke sker det samme hver gang, da vi har så stor kontrol over omgivelserne, hvor man i virkeligheden f.eks. kunne opleve forskellige belysning, eller at banen ændre sig over tid. Ud over dette køre robotten ikke særligt hurtigt når den ikke følger linjen, dette kunne også godt forbedres.

Robotten kan også nemt komme ind i en situation, hvor banen blev så kompleks at det ikke længere kunne løses ved at hardcode robotten. Her kunne en løsning eventuelt involvere brugen af AI, men grundet tiden til dette projekt, blev det aldrig undersøgt. AI åbner dog mange muligheder, og i teorien kunne man have bygget en neural network der selv træner robotten til gennemføre banen. Her vil det give mening at give points når robotten følger linjen og minus points når den rammer forhindringer. Men denne metode vil robotten køre meget dårligt til at starte med men eller en del forsøg vil den begynde at køre helt optimalt.

Video

More

“The Snail” Sneglen

Nikolaj Martinus. Kristensen, Mira Berg Schmidt, Simone Dalager

Robottens navn: Sneglen

Introduktion

I denne PF2 opgave blev vi stillet en opgave om at skulle gennemfører en virtuel bane i unity, ved at bygge en “robot” som skal kunne følge en sort linje og undgå objekter. Opgaven går mere ud på at lave et program og en opbygning af robotten, der bringer den sikkert i mål uden at den kører af stregen eller kører ind i objekter.

Til at kunne gennemfører dette benyttes en arduino,  2 DC Motorer, 2 lyssensorer, 2 lys LDR, 1 Sonar Sensor, 1 servomotor som bliver opsat unity.

Dette blogindlæg vil indeholde afsnit omkring b.la hardwaren, opbygning af programmet, en konklusion og til sidst en perspektivering. 

Tutorial

Hvad skal bruges til opbygningen af Sneglen

  • Arduino Uno
  • 1 eller 2 breadboard
  • 2 hvide LED’er
  • 2 LDR
  • 2 stk.10 Kohm modstande
  • 2 stk.450 ohm modstande
  • 2 DC gearing motor
  • L293D H-Bro
  • 9V batteri
  • 5V spændingsregulator
  • Servo motor
  • HC-SR04 sonar

Princippet bag Sneglens linje følger

Sneglen kan følge en sort linje på en hvid baggrund ved hjælp af lys. Hvidt lys på en hvid overflade vil næsten reflektere helt tilbage, hvor på en sort overflade vil lyset blive absorberet. Det er dette princip Sneglen bruger i sin opførsel. Det er de 2 LED’er der skaber lyset mod jorden/overfladen og de 2 LDR’er som aflæser den reflekterende lys der kommer tilbage.

Reference for billede findes i kilder

Når begge LDR’er får reflekteret det meste af lyset tilbage, fordi de befinder sig over en hvid overflade vil Sneglen bevæge sig lige ud. Det betyder nemlig at der er en LDR på hver side af stregen

//if both sensors see white then forward
            if (leftLDRValue > 500 & rightLDRValue > 500)
            {
                Debug.Log("Move forward");
                analogWrite(motorRight1, motorspeedGo);
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, motorspeedGo);
                analogWrite(motorLeft2, motorspeedStop);

                //yield return delay(10);
            }

Reference for billede findes i kilder

Når den venstre LDR får en lav værdi skyldes dette at den bevæger sig over den sorte streg, hvor det hvide lys absorberes. Når dette sker vil motoren på Sneglens højre hjul køre hurtigere og dermed drejer Sneglen til venstre.

//left
if (leftLDRValue < 500 & rightLDRValue > 500)
            {
                Debug.Log("Turn left");
                analogWrite(motorRight1, turnSpeed); //forsøg
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, notTurnwheel);
                analogWrite(motorLeft2, 20);

                //yield return delay(20);//forsøg
            }

Reference findes i kilder

Det er det samme princip der sker når den højre LDR rammer den sorte streg, som når den venstre LDR gør. I stedet vil motoren på Sneglens venstre hjul nu køre hurtigere og det vil få sneglen til at dreje mod højre.

//right
            if (leftLDRValue > 500 & rightLDRValue < 500)
            {
                Debug.Log("Turn right");
                analogWrite(motorRight1, motorspeedStop);
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, turnSpeed);
                analogWrite(motorLeft2, motorspeedStop);

                //yield return delay(10);
            }

Reference for billede findes i kilder

Når begge LDR’er ikke får nogen værdi tilbage betyder det at de begge befinder sig over den sorte streg hvor alt lys absorberes. Denne sorte streg er en stoplinje for Sneglen og dermed stopper Sneglen med at køre når begge LDR rammer den.

if (leftLDRValue < 500 & rightLDRValue < 500)
            {
                Debug.Log("Stop");
                analogWrite(motorRight1, motorspeedStop);
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, motorspeedStop);
                analogWrite(motorLeft2, motorspeedStop);

                yield return delay(500);
                followLine = false;
                followSonar = true;
            }

Opbygning

Vi har ikke haft mulighed for at kunne bygge sneglen fysisk. VI har bygget den i Unity. I Unity består sneglen af en turkis plade som forestiller en Ardunio. Der er koblet to styrende hjul på hver sin side af Arduinoen. Foran er et fri rullende hjul som skal sikre balance og sørge for at den hverken tipper/vælter. Under pladen er to firkanter, som forestiller de to LED’er og LDR’er. I Unity har de fået lyskilder tilføjet, så vi styre ikke LED’er i koden i Unity. Afstanden mellem de to firkanter må ikke være for stor eller for lille ellers vil den ikke være præcis nok. Afstanden mellem dem skal være lidt større end bredden på den sorte streg for at de bedst kan læse. Oven på pladen er der en orange firkant med tre små forskellig farvede firkanter. Dette skal forestille en servomotor. De små farvede firkanter indikere vinklerne 0, 90 og 180 grader. Kapslen oven på firkanten peger i den retning som servo vinkel er indsat til. Ovenpå servomotoren er der en sonar. Sonaren sidder oven på servoen for at den drejer med servoen rundt, på denne måde kan man vinkle sonaren i den retning man ønsker den skal dreje.

I den fysiske verden ville der være noget til forskel. Vi vil her komme ind på nogle af de forskelle der er. Placeringen LDR’erne er ikke ligegyldig som det også er tilfældet i Unity. Dog er der en forskel på placeringen af LDR’erne i forhold til LED’erne. LDR’erne er bedste placeret lidt over LED’erne da de derved nemmere kan opfange den reflekterede lys fra overfladen. Ved en fysisk opbygning af Sneglen ville der være langt flere aspekter at tage højde for i opbygningen, men det omhandler meget hvordan den opbygges i lego og dette er svært at beskrive når man ikke selv har gjort det.

For at lave opsætningen i Unity har vi fået udleveret et Unity projekt med de forskellige “komponenter”, som vi skal bruge. Her er det vigtigt at huske opsætningen i Unity. H-broerne skal refereres til den tilhørende motor og hjul. Linjesensor skal være “child” af “robot chassis” da den ellers ikke vil følge med robotten. Hvis man har problemer med at hjulene ikke følger roboten skal man se om deres “connected body” under “Configurable Joint” er “CarChassis”. Sonaren skal være “child” af “CarChassis”

Opsætning af Hierakiet i Unity:

Video af testkørsel

Vedhæftet nedenfor ses en video af  “Sneglens” hurtigste tid på 77 sekunder, når man trækker delayed på 2 sekunder fra når den er sat i gang. Robotten er ikke hurtig, men formår at kører banen igennem fra start til slut, uden at berører de røde vægge og køre på afveje. Vi prioriterede at robotten gennemførte banen med de stillede regler, frem for hvor hurtigt robotten kunne komme fra start til slut punkter i banens opsætning. Hvilket resulterede i en relativ langsom tid, men en gennemført tur rundt i banen.

Testkørsel af Sneglen

Opbygning af hardware i TinkerCAD

For at finde hvilke modstand der skal på LED’erne kan man bruge ohms lov og udregne modstanden, da forspændingen(forward current) på en hvid LED er 20 mA fundet i datablad side 4

Ohms lov

http://www1.futureelectronics.com/doc/EVERLIGHT%C2%A0/334-15__T1C1-4WYA.pdf

LED’erne er hvide, da hvidt lys reflekteres bedre end f.eks. rødt lys.

LDR’erne er placeret lidt højere oppe end LED’erne derved bliver lyset reflekteret bedst i den virkelige verden. Dette er dog ikke vigtigt i vores virtuelle verden.

Modstanden for LDR’erne er ikke beregnet da deres modstand i LDR’erne jo ændre sig afhængigt af lysforholdet. Der er valgt modstand på 10 kohm ud fra en tommelfingerregel og da systemet er virtuelt vil det ikke give mening at måle på en fysisk LDR.

Vores valg faldt på HC-SR04 sonar, da det var den vi fandt bedst og mest information om da vi undersøgte hvordan vi bedst kunne bruge en sonar til at sikre sig at robotten ikke kører ind i vægge og andre objekter. I Den virtuelle verden i Unity var det også opførelsen på HC-SR04 sonaren der var blevet efterlignet og det naturlige valg faldt derfor på denne.

Spændingsregulatoren bruges til at give Arduino strøm, da den ikke kan klare 9V. Derudover kan servomotoren og sonaren heller ikke klare 9V, de bliver derfor også tilkoblet 5V spændingsregulatoren

Vi har valgt at benytte os har de analog pin til hjulenes motore, så vi kan regulere hvor hurtig de kører frem af, ved at tilskrive forskellige værdier som en hastighed mellem 0-255. Hvis vi have brugt de digitale pini stedet for, så kan vi kun tilskrive den 2 værdier som er 1 og 0, hvilket vil sige de kan være high eller low så inten kører hjulenes motorer med fuld hastighed eller stilstand. Men da vi gerne ville regulere hastigheden, på hurtig robotten skulle køre bruger vi de analog pin. 

Vi har valgt at sætte sonars output i en digital pin med pwm (pulse width modulation) da vi begyndte at løbe tør for analoge porte.

Opbygning af program 

For at lave vores system har vi brugt Arduinos servo  bibliotek og deres “new ping” bibliotek. Vi ønskede at teste vores arduino kode i TinkerCAD, men “new ping” biblioteket findes ikke i TinkerCAD og da vi prøvede at indsætte biblioteket manuelt var for meget kode for at TinkerCAD kunne simulere dette.

Hvordan er samspillet mellem mekanik, elektronik og software?
Der er ikke noget samspil da det hele er software da det foregår virtuelt. Vi kan ikke spekulere i hvad der kunne eller ikke kunne fungere i den virkelige verden når vi ikke har arbejdet med det fysisk.

Braitenberg vehicles kan autonomt bevæge sig på basis af dets sensor inputs. Den har sensorer, der måler en stimulus og hjul der skriver hver deres motor. Disse motorer fungere som aktuatorer. Når sensorer og aktuatorer bliver forbundet, og kan registrer et signal, vil et hjul blive sat i bevægelse. Dette kan overføres til hvordan vores robot “Sneglen” bevæger sig rundt på den opstillede bane. Som tidligere gennemgået i princippet bag hvordan sneglen følger en linje, har vi en sensor i højre og venstre side af robotten som hver især er forbundet til de respekterende hjul på hver side.  Når robottens ene sensor kører over den sorte streg og ser sort og den anden ser hvidt, så drejer robotten til den side hvor sensoren ser sort. Hermed har vi en opbygning der benytter samme princip som med Braitenberg vehicles om at nogle sensorer giver et signal til nogle aktuatorer om at de skal sættes i bevægelse.

Unity kode

int motorLeft1 = 0; //digitalport, pin 2 i Ardunio kode
    int motorLeft2 = 1; //digitalport, pin 3 i Ardunio kode
    int motorRight1 = 2; //digitalport, pin 4 i Ardunio kode
    int motorRight2 = 3; //digitalport, pin 5 i Ardunio kode

    int sonar = 6;

    int motorspeedStop = 0;
    int motorspeedGo = 50;
    int turnSpeed = 50;
    int notTurnwheel = 0;

    
    bool followLine = true;
    bool followSonar = false;
    bool firstTurn = false;
    bool secondTurn = false;
    bool thirdTurn = false;
    bool firstBox = false;

    IEnumerator setup()
    {
        //Your code goes here:

        //Example of delay:
        Debug.Log("pre-delay log");
        yield return delay(2000); //2 second delay
        Debug.Log("after delay log");
        
        //Your code ends here -----

        //following region ensures delay-functionality for setup() & loop(). Do not delete, must always be last thing in setup.
        #region PremadeSetup
        yield return StartCoroutine(loop()); ;
        #endregion PremadeSetup
    }

    IEnumerator loop()
    {
        //Your code goes here:
        // put your main code here, to run repeatedly:
        int leftLDRValue = analogRead(4); //venstre LDR værdi A1 i Arduino kode
        int rightLDRValue = analogRead(5); //højre LDR værdi, A2 i Arduino kode

        ulong distance = pulseIn(6);

        Debug.Log("loop started");

        //sætter servo til at vende lige ud

        if (followLine == true) //DET SKAL VÆRE ET IF STATEMENT (KAN IKKE LAVE WHILE LOOP I UPDATE FUNCTION I UNITY)
        {
            Debug.Log("FollowLine");
            servo.write(70);
            //if both sensor reads black then stop
            Debug.Log(distance);
            if (leftLDRValue < 500 & rightLDRValue < 500)
            {
                Debug.Log("Stop");
                analogWrite(motorRight1, motorspeedStop);
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, motorspeedStop);
                analogWrite(motorLeft2, motorspeedStop);

                yield return delay(500);
                followLine = false;
                followSonar = true;
            }

            //right
            if (leftLDRValue > 500 & rightLDRValue < 500)
            {
                Debug.Log("Turn right");
                analogWrite(motorRight1, motorspeedStop);
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, turnSpeed);
                analogWrite(motorLeft2, motorspeedStop);

                //yield return delay(10);
            }

            //left
            if (leftLDRValue < 500 & rightLDRValue > 500)
            {
                Debug.Log("Turn left");
                analogWrite(motorRight1, turnSpeed); //forsøg
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, notTurnwheel);
                analogWrite(motorLeft2, 20);

                //yield return delay(20);//forsøg
            }

            //if both sensors see white then forward
            if (leftLDRValue > 500 & rightLDRValue > 500)
            {
                Debug.Log("Move forward");
                analogWrite(motorRight1, motorspeedGo);
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, motorspeedGo);
                analogWrite(motorLeft2, motorspeedStop);

                //yield return delay(10);
            }

            if(firstBox == false & distance < 700 & distance > 10)
            {
                Debug.Log("Turn right");
                analogWrite(motorRight1, motorspeedStop);
                analogWrite(motorRight2, 50);
                analogWrite(motorLeft1, 80);
                analogWrite(motorLeft2, motorspeedStop);
                yield return delay(450);

                Debug.Log("Move forward");
                analogWrite(motorRight1, motorspeedGo);
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, motorspeedGo);
                analogWrite(motorLeft2, motorspeedStop);
                yield return delay(500);

                Debug.Log("Turn left");
                analogWrite(motorRight1, turnSpeed); //forsøg
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, 15);
                analogWrite(motorLeft2, motorspeedStop);
                yield return delay(5100);
                firstBox = true;
            }
        }


        //servo and sonar
        if (followSonar == true) 
        {
            servo.write(75);
            Debug.Log("followSonar");
            //go forward for a small period of time
            Debug.Log("Move forward Sonar ");
            analogWrite(motorRight1, motorspeedGo);
            analogWrite(motorRight2, motorspeedStop);
            analogWrite(motorLeft1, motorspeedGo);
            analogWrite(motorLeft2, motorspeedStop);
            yield return delay(10);
            //Debug.Log(distance);

            if (thirdTurn == true & secondTurn == false & firstTurn == false)
            {
                Debug.Log("Move forward");
                analogWrite(motorRight1, motorspeedGo);
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, motorspeedGo);
                analogWrite(motorLeft2, motorspeedStop);
            }

            //kør frem indtil man er x cm fra væg, drej til venstre, drej servo 90 grader
            if (distance < 700 & thirdTurn == false & secondTurn == false & firstTurn == false)
            {
                Debug.Log("Turn left Sonar");
                analogWrite(motorRight1, 80); //forsøg
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, notTurnwheel);
                analogWrite(motorLeft2, 50);
                yield return delay(1070);
                firstTurn = true;
                secondTurn = false;
                thirdTurn = false;
                Debug.Log(distance);
                //pulseIn(6);
                //yield return delay(10);
            }
           
            if (distance > 600 & firstTurn == true)
            {
                Debug.Log("Move forward Sonar ");
                analogWrite(motorRight1, motorspeedGo);
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, motorspeedGo);
                analogWrite(motorLeft2, motorspeedStop);
               // yield return delay(10);
                Debug.Log(distance);
            }
            

            if (distance < 680 & firstTurn == true)
            {
                Debug.Log("Turn right Sonar");
                analogWrite(motorRight1, motorspeedStop);
                analogWrite(motorRight2, 50);
                analogWrite(motorLeft1, 80);
                analogWrite(motorLeft2, motorspeedStop);
                secondTurn = true;
                firstTurn = false;
                thirdTurn = false;
                yield return delay(900);
                Debug.Log(secondTurn);
                Debug.Log(distance);
            }

            if (distance > 600 & secondTurn == true)
            {
                Debug.Log("Move forward Sonar ");
                analogWrite(motorRight1, motorspeedGo);
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, motorspeedGo);
                analogWrite(motorLeft2, motorspeedStop);
                // yield return delay(10);
                Debug.Log(distance);
            }


            if (distance < 650 & secondTurn == true)
            {
                Debug.Log("Turn right sonar 2");
                analogWrite(motorRight1, motorspeedStop);
                analogWrite(motorRight2, 50);
                analogWrite(motorLeft1, 80);
                analogWrite(motorLeft2, motorspeedStop);
                yield return delay(950);
                thirdTurn = true;
                firstTurn = false;
                secondTurn = false;
                Debug.Log(distance);
            }

            if (distance > 600 & thirdTurn == true)
            {
                Debug.Log("Move forward Sonar ");
                analogWrite(motorRight1, motorspeedGo);
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, motorspeedGo);
                analogWrite(motorLeft2, motorspeedStop);
                // yield return delay(10);
                Debug.Log(distance);
            }

            if (leftLDRValue < 500 & rightLDRValue < 500)
            {
                Debug.Log("Stop");
                analogWrite(motorRight1, motorspeedStop);
                analogWrite(motorRight2, motorspeedStop);
                analogWrite(motorLeft1, motorspeedStop);
                analogWrite(motorLeft2, motorspeedStop);

                yield return delay(1000);
            }
        }



        //Following region is implemented as to allow "yield return delay()" to function the same way as one would expect it to on Arduino.
        //It should always be at the end of the loop()-function, and shouldn't be edited.
        #region DoNotDelete
        //Wait for one frame
        yield return new WaitForSeconds(0);
        //New loop():
        yield return loop();
        #endregion DoNotDelete 
    }

Konklusion

Robotten løser den stillede opgave, uden at berøre nogle af de røde vægge og kører kun af den sorte streg for at undvige den lille firkantede røde væg i starten af banen. Så hvis vi kigger på hvor godt den kører banen igennem uden at tage tid i betragtning, løser robotten opgaven godt.

Perspektivering

I den fysiske verden hvor der ville være brug af konkurrencetid, ville det give god mening at gøre robotten hurtigere og derved få en hurtigere banekørsel

Det kunne give mening at optimere koden så den er mere dynamisk. Således at Sneglen også ville kunne køre andre baner uden at man havde ændret i koden

Kilder

Lignende projekter

https://create.arduino.cc/projecthub/robocircuits/line-follower-robot-arduino-299bae

https://circuitdigest.com/microcontroller-projects/line-follower-robot-using-arduino

Opsætning af DC motor og L293D modul

https://www.instructables.com/id/How-to-use-the-L293D-Motor-Driver-Arduino-Tutorial/

Datasheet for hvid LED

http://www1.futureelectronics.com/doc/EVERLIGHT%C2%A0/334-15__T1C1-4WYA.pdf

Sonar HC-SR04

Servo SM-S2309S

https://www.bananarobotics.com/shop/SpringRC-SM-S2309S-Micro-Analog-Servo-9.9g

http://descargas.cetronic.es/microservo.pdf

More

Corona avoidance-inator

En rapport af: Lasse Fisker (lafis17) og Alexander Rol (alrol17)

Demonstration af robottens gennemkørsel
NB: Timeren er gået i stykker, og viser derfor ikke tid.
Tiden er ikke en del af projektets formål og er derfor ikke forsøgt fikset.

Komponentliste

Til udarbejdelsen af denne robot skal der bruges følgende komponenter:

  • Arduino UNO microcontroller
  • 2 DC-motorer
  • L293D integreret kredsløb
  • 1 servomotor
  • 1 HC-SR03
  • 1 9V batteri
  • 2 hvide LED
  • 2 LDR
  • 2 32 ohm modstande
  • 2 10 kilo-ohm modstande
  • Ledninger

Derudover skal der bruges et udviklingsmiljø til at skrive Arduino kode i, eksempelvis Arduino IDE eller Visual Studio Code, samt robottens skelet, eksempelvis chassis og hjul.

Opbygning i TinkerCad

Robottens opbygning er skitseret i Tinkercad, som vist nedenfor på figur 1. For at overskueliggøre de forskellige dele af kredsløbet, er dette blevet delt op på to forskellige breadboards, som dog stadig bliver styret af én enkelt Arduino. Hvis systemet skulle produceres som endeligt produkt, ville dette naturligvis ikke være lavet på to forskellige boards, men hele systemet ville være integreret.

Figur 1 – Robottens sammensætning i TinkerCad

Den samlede opbygning kan deles op i flere delkomponenter, som kan ses på figur 2 nedenfor.

Figur 2 – Opdeling af sensorer, aktuatorer og controller

De tre delkomponenter er sensorer, aktuatorer og controlleren. På sensor-komponenten, findes en HC-SR03, som sender ultrasoniske bølger ud ved aktivering og måler hvor lang tid der går før den modtager de udsendte bølger. Derudover er der to hvide lysdioder (LED) og to lysmodstande (LDR). De to LED’ers formål er at sikre at der er konstant det samme udgangspunkt for at måle underlagets reflektion. LDR reagerer på reflekteret lys og vil derfor reagere på bølgelængden af det lys der bliver reflekteret i gulvet i lyset fra de to LED’er og justerer modstanden i henhold til det. Da mørke overflader og lyse overflader ikke reflekterer lige meget af lyset, vil de to LDR kunne få robotten til at følge den sorte linje, ved hjælp af Arduino koden.
Både LED og LDR har modstande enten før eller efter sig i deres respektive kredsløb. LED’erne har en modstand før sig for at der ikke løber for meget spænding igennem dem, så de sprænger. Disse modstande er beregnet i forhold til en hvid LED, som er fundet i et datablad (http://www1.futureelectronics.com/doc/EVERLIGHT%C2%A0/334-15__T1C1-4WYA.pdf). Det er beregnet at for at LED’erne får den mængde spænding (3,3V) som er midt mellem minimum- og maksimumsværdierne i databladet, i stedet for de 5V som Arduinoen giver den, skal der bruges en modstand på 32 ohm. Dette er beregnet på følgende måde:

Da vi ved at LED’en i sig selv bruger 3,3V og der er en spænding på 5V i kredsløbet, skal modstanden bruge 1,7V for at bruge resten i kredsløbet. Det er vores U-værdi i dette tilfælde. Da strømmen målt i kredsløbet ved LED’erne er 0,0535A, er dette værdien på variablen I.

R=U/I=1,7/0,0535=31,78 ohm

Dette rundes i dette tilfælde op til 32 ohm, og derved er modstandsværdien for de to resistorer til LED’erne beregnet.
Derudover er strømmen gået fra rundt regnet 53,5 mA til 34,3 mA. LED’ernes normale anbefalede mængde strøm er 20 mA, så den bliver ikke overbelastet nok til at der sker andet end at levetiden på dioden vil forringes en smule.

Grunden til at der skal være en modstand efter hver enkelt LDR i kredsløbet er at Arduino ikke kan aflæse modstand. Derimod kan den registrere spændingsforskelle. Ved at sætte en ekstra modstand efter en LDR, et såkaldt spændingsdeler-kredsløb, omdannes modstanden til en spændingsforskel, ved hjælp af Ohms første lov, som siger at spænding = modstand * strøm. Og denne værdi kan Arduino registrere og derved også ændre adfærd ud fra, ved hjælp af koden.

Hvis man kigger på aktuator-boardet, er der en servomotor, to DC-motorer og en H-bro ved navn L293D (Se figur 4), som sørger for at de to DC-motorer kan køre i begge retninger, alt efter hvilken vej strømmen løber.

Servomotorens opgave er at kunne dreje HC-SR03, således at vi kan sørge for at måle både til venstre, højre og ligefrem for selve køretøjet. I et virkeligt system, ville sensoren være fastspændt direkte på servomotoren, i stedet for at være på to forskellige breadboards her. Servomotoren styres ved hjælp af Arduino-boardet.

De to DC-motorer sørger for at drive robottens hjul for at skabe fremdrift og motorerne vil give et højere omdrejningstal jo mere spænding de får gennem sig. Da Arduino-boardet ikke kan konvertere digitale signaler til analoge signaler ved hjælp af en DAC (Digital-to-Analog Converter), bliver hastigheden på motorerne styret ved hjælp af PWM (Pulse Width Modulation). PWM fungerer på den måde at man skriver digitale signaler ud fra Arduinoen, som enten er HIGH eller LOW, hvor HIGH er maks hastighed og LOW er slukket. Ud fra hvor længe outputtet er HIGH i forhold til hvor længe det er LOW, får man en gennemsnitlig spænding. Eksempelvis vil man få et spændingsniveau på 50% af det maksimale hvis man har en duty cycle på 50%, det vil sige at PWM-signalet er lige lang tid på HIGH som det er på LOW. Da dette signal er vekslende, skal der noget til at jævne det ud for at DC-motoren kan fungere optimalt. En spole eller en transistor kan omdanne indkommende spænding til et magnetfelt, som lades op og derefter kan aflade jævnt igen, således at spændingen derefter er helt jævn. Dermed kan man også nemt implementere blød opstart eller generelle hastighedsændringer, ved at regulere PWM-signalerne til en lavere eller højere duty cycle En illustration af PWM vises herunder i figur 3.

Figur 3 – Illustration af PWM-signaler ved forskellige duty cycles
Figur 4 – L293D – H-Bro

L293D består af 16 forskellige terminaler, hvoraf nogle deler funktionalitet og kan drive to outputs på én gang.
Vcc 1 står for at have strømforsyning til den interne logik i chippen, mens Vcc 2 står for at modtage strøm som kan sendes videre ud i systemet, eksempelvis for at drive en DC-motor.
Enable-terminalerne står for at låse op for at et signal kan nå fra input-terminalerne til den interne logik i kredsløbet.
Input-terminalerne bruges til at sende et signal til kredsløbet, som beskriver en opgave man vil have udført. Dette vil blive behandlet internt og signalet om opgaven sendes videre til den respektive output-port, som eksempelvis sender signalet videre til en DC-motor for at drive den.
GND er for at sørge for at kredsløbet har jordforbindelse, så der ikke er overgang og for at fuldende kredsløbet (https://www.ti.com/lit/ds/symlink/l293d.pdf?ts=1588425062000).

Derudover er der brugt et 9V batteri, for at sørge for at kredsløbet havde nok strøm. Uden batteriet, ville der ikke være strøm nok til både aktuatorerne og sensorerne. Normalt er det ikke anbefalet at køre bare DC-motorer direkte fra Arduino boardet og da vi i dette tilfælde skal bruge tre forskellige motorer – 1 servomotor og 2 DC-motorer – Er det nødvendigt at have et 9V batteri til at agere strømforsyning til disse tre. Det skal også nævnes at disse motorer oftest bruger mere strøm i opstarten end når de er ved den ønskede hastighed, så der vil være mere pres på strømforsyningen indtil motorerne kommer op i fart.

Opbygning af program

Følgende kodestykke er skrevet i Visual Studio ved brug af C#, da situationen med Covid-19 gør det umuligt at kunne fremstille en fysisk version af robotten. Der er derfor blevet udleveret en bane i Unity vi bruger til at teste robottens opførsel.

Banens opbygning kan ses i videoen i toppen af denne blog.
Vi vil ikke komme nærmere ind på Unity og dets implementering, da dette ikke er en del af kurset.

Grundprincipperne ved kørslen af robotten er, at den altid vil starte med at forsøge at følge linjen på gulvet. Mens dette sker, vil HC-SR03 holde øje med om der er forhindringer forud for robotten. Skulle dette ske, vil sensoren giver udslag og robotten vil forsøge at undgå forhindringen ved hjælp af en serie af sving på 90 grader samt forsøge at komme tilbage på stregen igen.
Når robotten opfanger sort på begge LDR, vil den slå disse sensorer fra, og gå i sonar-mode. I denne mode vil den holde øje med væggen til højre resten af vejen til målet.

HC-SR03 stying:
I Arduino er HC-SR03 sat op til at sende et signal ud i 10 mikro-sekunder for derefter at lytte efter et svar på dette signal. Dette køres over 1 pin på Arduinoen, hvilket gør at vi er nødt til at ændre signaltypen på den valgte pin fra at forvente et output til et input efter vi har sendt signalet i 10 mikro-sekunder.
Resultatet vi får tilbage er af datatypen long som vi herefter evaluerer på i forhold til at bedømme om vi er over eller under vores tolerancegrænse som bestemmes efter for stor en afstand vi ønsker robottenhar før en undvigelse.

long readDistance(int triggerPin, int echoPin) {
  	pinMode(triggerPin, OUTPUT);  //Clear the trigger
 	digitalWrite(triggerPin, LOW);
  	delayMicroseconds(2);
  	//Sets the trigger pin to HIGH state for 10 microseconds
  	digitalWrite(triggerPin, HIGH);
  	delayMicroseconds(10);
  	digitalWrite(triggerPin, LOW);
  	pinMode(echoPin, INPUT);
  	long result = pulseIn(echoPin, HIGH);
  	digitalWrite(echoPin, LOW);
  	pinMode(triggerPin, OUTPUT);  //Clear the trigger
  	
  	//Reads the echo pin, and returns the sound wave travel time in microseconds
  	return result;
}

Servo styring:
Ved brug af servo benytter vi et import i Arduino “#include <Servo.h>” som gør os i stand til at kommunikere med vore servo gennem simple kommands. Det eneste vi skal gøre er at give vores servo et variabel navn (i dette tilfælge “myServo” og derefter kalde dette navn med den grad du gerne vil have den skal vende (“myServo = 45″ sætter servo til 45 grader). Hvis ikke en værdi sættes på denne servo, vil den pr. automatik gå til 90 grader som er midterste værdi (minimum på 0 og maximum på 180 grader).

Motor styring:
Vores motor er forbundet 2 steder i vores Arduino. Den ene er til bevægelse fremad og den anden er til bevægelse bagud. Dette er gjort, da vi benytter os af en L293D, som du kan læse mere om under “Opbygning i TinkerCad”. Kort sagt, kan vi styre motorens retning afhængigt af hvilken vej strømmen føres gennem motor-komponenten. Dette styres gennem vores pin(s) og forbindelser til L293D.
Det vi gør herfra er at fortælle Arduino hvilken pin vi har sat motoren på, og derefter fortælle om output’et på den/de valgt(e) pin(s) skal være “HIGH” eller “LOW”. Disse er en indikation på om vi vil have bevægelse i den valgte retning eller ej.
Det er altid en god idé at navngive nummeret på den pin der bruges, på en måde, så man kan læse sig til hvad den pågældende pin kontrollerer (f.eks. “leftForward“).

NB: Koden beskriver HC-SR03 ved sonar-mode. Dette er ikke faktuelt, da sonar primært forholder sig til undervands-afstandsmåling. Funktionerne er meget ens, men teknologiens navn er forskellig.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;

public class ArduinoMain : MonoBehaviour
{
    public Breadboard breadboard;
    public Servo servo;
   

    ulong rightDist;
    ulong middleDist;
    ulong leftDist;
    ulong distanceThreshold = 400;

    int servoAngle = 0;
    bool maxServoAngle = false;
    bool sonarMode = false;
    bool initialMeasurementDone = false;

    bool avoided = false;
    bool wallFound = false;

    ulong readDistance()
    {
        ulong result = breadboard.pulseIn(2);
        Debug.Log("ReadDistance: " + result);
   
        return result;
    }

    void reverse(int speed)
    {
        Debug.Log("tilbage");
        breadboard.analogWrite(3, 0);
        breadboard.analogWrite(4, 0);
        breadboard.analogWrite(5, speed);
        breadboard.analogWrite(6, speed);
    }

    void driveForward(int speed)
    {
        Debug.Log("Frem");
        breadboard.analogWrite(5, 0);
        breadboard.analogWrite(6, 0);
        breadboard.analogWrite(3, speed);
        breadboard.analogWrite(4, speed);
    }

    void turnLeft(int speed)
    {
        Debug.Log("venstre");
        breadboard.analogWrite(3, 0);
        breadboard.analogWrite(4, speed);
    }

    void turnRight(int speed)
    {
        Debug.Log("højre");
        breadboard.analogWrite(3, speed);
        breadboard.analogWrite(4, 0);
    }

    void stop()
    {
        breadboard.analogWrite(3, 0);
        breadboard.analogWrite(4, 0);
        breadboard.analogWrite(5, 0);
        breadboard.analogWrite(6, 0);
    }

    void followLine()
    {
        Debug.Log("Followline");
        int leftSensor = analogRead(0);
        int rightSensor = analogRead(1);

        if(servoAngle != 90)
        {
            servo.write(90);
        }
        
            if (leftSensor > 945 && rightSensor > 945)
            {
                //Go straight
                breadboard.analogWrite(3, 50);
                breadboard.analogWrite(4, 50);
            } else if (leftSensor < 945 && rightSensor > 945)
            {
                //Left correction
                breadboard.analogWrite(5, 5);
                breadboard.analogWrite(3, 0);
                breadboard.analogWrite(4, 50);
            } else if (leftSensor > 945 && rightSensor < 945)
            {
                //Right correction
                breadboard.analogWrite(6, 5);
                breadboard.analogWrite(3, 50);
                breadboard.analogWrite(4, 0);
            } else if (leftSensor < 945 && rightSensor < 945)
            {
            Debug.Log("Entered STOP");
                //Stop
                breadboard.analogWrite(3, 0);
                breadboard.analogWrite(4, 0);
                breadboard.analogWrite(5, 0);
                breadboard.analogWrite(6, 0);
                sonarMode = true;
                servo.write(90); //preparing servo for forward motion and detection.
                Debug.Log("sonarmode " + sonarMode);
            }
    }

IEnumerator setup()
    {
        //Your code goes here:
        servoAngle = 90;
        servo.write(servoAngle);

        //Example of delay:
        Debug.Log("pre-delay log");
        yield return delay(2000); //2 second delay
    }

 IEnumerator loop()
    {
        Debug.Log("Sonarmode: " + sonarMode);
        if (!sonarMode)
        {
            ulong result = readDistance();
            //Distance sensor returns 0 if no obstacle is found
            if(result == 0)
            {
                followLine();
            } else if (result > distanceThreshold) 
            {
                followLine();
                Debug.Log("If result");
            } else
            {
                if (avoided == false)
                {
                    Debug.Log("avoidobst");
                    Debug.Log(middleDist);
                    avoided = true;
                    //AVOID OBSTACLE
                    reverse(25);
                    yield return delay(200);
                    turnLeft(50);
                    yield return delay(2600);
                    driveForward(25);
                    yield return delay(1800);
                    turnRight(50);
                    yield return delay(2300);
                    driveForward(25);
                    yield return delay(2000);
                    turnRight(50);
                    yield return delay(2300);
                    driveForward(25);
                    yield return delay(200);
                    turnLeft(50);
                    yield return delay(2300);
                    reverse(25);
                    yield return delay(1400);

                    stop();

                    Debug.Log("DONE");
                }
            }
            
        } else
        {
            breadboard.analogWrite(3, 25);
            breadboard.analogWrite(4, 25);

            if (!wallFound)
            {
                if (readDistance() >= 900 && readDistance() <= 950)
                {
                    wallFound = true;
                    turnLeft(50);
                    servo.write(180);
                    yield return delay(2300);
                }
            } else if(wallFound)
            {
                if(readDistance() > 950)
                {

                    Debug.Log("højre");
                    breadboard.analogWrite(3, 50);
                    breadboard.analogWrite(4, 15);
                   // turnRight(50);
                    yield return delay(6500);

                    Debug.Log("Complete 180");

                    breadboard.analogWrite(3, 50);
                    breadboard.analogWrite(4, 50);

                    yield return delay(8000);

                    breadboard.analogWrite(3, 0);
                    breadboard.analogWrite(4, 0);

                    yield return delay(9999999); //Stop when passing finish line (Infinite delay)
                }
            }
        }
    }

Forhindringen:
For at sikre at vi undgår forhindringen på samme måde, hver gang, har vi valgt at kode ruten rundt om. Dette er ikke det bedste valg, men da forhindringen var så smal som den var så vi på nogle af de tidlige tests, at HC-SR03 ikke så forhindringen under omkørslen.
Denne måde at gøre det på tog lang tid at implementere, men har ført til et resultat der gør at der er minimal afvigelse fra de forskellige kørsler, og vi kan derfor regne med at forhindringen passeres korrekt hver gang. Dog er koden til dette ikke det flotteste at kigge på.
Der er lagt en forsinkelse ind efter hver bevægelse, for at sikre, at robotten når at udføre denne operation førend vi beder den udføre den næste. Uden forsinkelserne, vil robotten gennemføre alle disse linjer på mindre end 1 sekund og stadig ikke være forbi forhindringen.
Samme princip er også benyttet ved andre forsinkelser i koden.

Debug.Log("avoidobst");
                    Debug.Log(middleDist);
                    avoided = true;
                    //AVOID OBSTACLE
                    reverse(25);
                    yield return delay(200);
                    turnLeft(50);
                    yield return delay(2600);
                    driveForward(25);
                    yield return delay(1800);
                    turnRight(50);
                    yield return delay(2300);
                    driveForward(25);
                    yield return delay(2000);
                    turnRight(50);
                    yield return delay(2300);
                    driveForward(25);
                    yield return delay(200);
                    turnLeft(50);
                    yield return delay(2300);
                    reverse(25);
                    yield return delay(1400);

                    stop();

                    Debug.Log("DONE");

Samspil – mekanik, elektronik og software

I dette afsnit vil det blive illustreret og beskrevet hvordan denne robot udnytter både mekanik, elektronik og software til at udføre den ønskede opgave. Til illustrationer er der udarbejdet forløbsdiagrammer der viser forløbet i de tre forskellige tilstande robotten kan være i. Disse faser er avoidObstacle, hvor robotten skal navigere udenom en forhindring, som i dette tilfælde er en søjle, followLine hvor robotten følger den sorte linje i gulvet og til sidst sonarMode, hvor robotten navigerer ved hjælp af HC-SR03.

Samspillet mellem mekanik, elektronik og software foregår generelt sådan at vi får en værdi indlæst gennem noget elektronik, eksempelvis de to lysmodstande, som Arduinoen (software) processerer og ved hjælp af dens logik, eksempelvis sender signal til L293D og derved får de to DC-motorer til at udføre noget mekanisk arbejde.

Herunder følger forløbsdiagrammer for de tre forskellige faser der er nævnt ovenover.

Figure 5 – AvoidObstacle
Figur 6 – FollowLine
Figur 7 – SonarMode

Konklusion

I vores personlige mening, kører robotten bedre end forventet. Mængden af iterationer pr. millisekund er langt langt højere end først antaget, hvilket kommer til fordel for kørslen, da robotten er meget hurtigere til at registrere når den rammer linjen på gulvet. Dette er også grunden til at vi har valgt at flytte de to lysmodstande tættere på hinanden, for at få en mere præcis og ensartet kørsel hver gang.
Overordnet klarer robotten opgaven, da den kan navigere gennem banen uden at støde ind i forhindringerne og benytter dens sensorer på en måde der gør at den har nemmere ved at komme igennem banen end hvis den ikke havde dem.

Eventuelle forbedringer

Grundet Covid-19, har vi været nødsaget til at simulere hele dette miljø gennem Unity og C#. Brugen af C# har gjort at vi ikke har kunnet følge den normale praksis for udførelse af loops mv. som man normalt skal gøre i Arduino, men da vi ikke har haft mulighed for at benytte delay i void-metoder, var det nødvendigt at bryde den normale praksis.

I sidste del af banen ville det være mere smart hvis robotten kunne navigere udelukkende ved hjælp af HC-SR03, som hele tiden scanner miljøet omkring den og ikke kun bruge den til at finde en væg for at udløse at event i robottens logik.

I det hele taget kunne robotten være mere autonom, eksempelvis ved at bruge HC-SR03 til at navigere rundt om forhindringen, i stedet for en kodet række operationer, der kun gælder lige præcis den ene forhindring.

More

RoboSchneider

Mads Kildegaard Kristjansen & Lucas Brynk Jensen

I denne tutorial vil vi gennemgå hvordan man bygger en robot, som ved hjælp af diverse sensorer kan køre efter en streg på gulvet indtil stregen ophører, hvorefter robotten vil køre efter lydbølger for at undgå mulig kollision. Robotten i denne tutorial er navngivet RoboSchneider (efter skuespilleren) og den er udarbejdet af Lucas Brynk og Mads K. Kristjansen. Det færdige produkt, samt forventninger til produktets opførelse kan ses på nedenstående videoer. 

Video af testkørsel i Unity
Video af test i TinkerCAD

Komponenter & TinkerCAD

Vi starter med at åbne TinkerCAD. For at kunne forstå hvad der sker med den færdige robot i Unity, er vi nødt til at opbygge RoboSchneider i TinkerCAD først. De nødvendige elementer kan ses på nedenstående billede samt en liste med navne på elementerne. 

Hvordan selve elementerne bliver sat i forhold til hinanden er stort set lige meget i dette projekt, da TinkerCAD modellen ikke skal bruges andre steder. For forståelsens skyld opfordres der til at opsætte elementerne som skulle man bygge en “bil”.  

Første skridt er at sætte 9V batteriet til breadboardet så vi kan få strøm rundt i kredsløbet. Lad plus køre igennem en sliderswitch for at kunne slukke og tænde for RoboSchneider. Det første breadboard bliver vores H-bridge board. Sæt L293D H-Bridge (H-bro) fast i midten af breadboarded og forbind de to motorer på hver side af H-broen. Grunden til at anvende en H-bro er for at styre strømmens polaritet gennem motorerne. Dette gør at vi kan bestemme hvilken vej hjulene skal dreje. For den ene motor vil vi lade Input 1 og 2 fra H-broen gå til pin 10 og 11 på arduinoen. Man kan bruge alle digital pins til dette, men da Arduino Uno er begrænset i antallet af Digitale pins (især PWM pins), og vores servo kommer til at optage pin 9 eller 10, så er vi nødt til at bruge hvad der er tilbage. Ligeledes for den anden motor vil køre Input 3 og 4 fra H-broen hen til pin 5 og 6 på arduinoen. På første breadboard er der lidt plads tilbage, nede i den anden ende, til at sætte en 5V regulator [LM7805]. Fra 5V Regulatorens output trækker vi en plus og minus videre til næste breadboard. 

Andet breadboard bliver LDR-LED boarded. Her sætter vi 2 LDR-resistorer samt 2 modstande. Modstandene skal være på 10 kOhm grundet følgende beregninger: 

Da vi kender Ohms lov kan vi stille den om og finde den ønskede modstandsværdi ved den benyttede spænding. Hertil skal det siges at vi antager fra tidligere projekt at LDR-modstandene ved maksbelysning er 20-230kOhm, samt ved totalt mørke, vil være 2MOhm.

Vout = Vin * (R2/R1+R2) 

Ved at sætte den valgte modstand ind, samt indsætte de kendte værdier, kommer formlen til at se sådanne ud ved 20kOhm, 230kOhm og 2MOhm:

5V * (20000/20000+10000) = 3,33V 
5V * (230000/230000+10000) = 4,79V 
5V * (2000000/2000000+10000) = 4,98V

Da vi ikke stiger over 5V på noget tidspunkt kan vi konstatere at 10kOhm er en passende størrelse for de ønskede modstande. 

Lad LDR-resistorerne køre til hhv. pin A2 og A3. (Du kan i princippet benytte enhver A pin i dette projekt, da ingen A pins skal bruges til noget andet). Sæt herefter 2 LED-resistorer direkte til 5V med en modstand imellem. Størrelsen af modstanden vælges ud fra den givende LED’s data. LED-resistor Data kan findes i et Datablad. Da dette projekt er virtuelt har vi ikke ægte LED-modstande at undersøge, derfor må vi gå ud fra at vi har at gøre med en standard hvid LED 5mm (3542serialnumber). En sådanne LED-modstand kan maximalt håndtere 20mA, i TinkerCAD, før det begynder at gå ud over levetiden på LED-modstanden, og da den bliver tilført 5V kan vi regne os frem til, med hjælp fra Ohms lov, at en 250 Ohms modstand vil være at fortrække. 

U=R*I
R=U/I 
R=5/0,02 = 250 Ohm 

Nu skal Micro Servo’en sættes til. For at give den nødvendige spænding til Micro Servo’en, samt den Ultrasonic Distance Sensor, vælger vi at trække plus og minus til de to elementer direkte fra Arduino’ens 5V udgang og GND. Micro Servo’ens Signal skal køre til pin 9 (Den skal altid sættes til enten pin 9 eller 10, da det er de eneste to pins der er registreret i Arduinosprogets Servo.h bibliotek). 

Ultrasonic Distance Sensor’s TRIG, og ECHO skal køre til henholdsvis pin 3 og pin 12 (TRIG skal altid forbindes til en PWM digital pin, og ECHO skal altid forbindes til en normal digital pin). Hvis systemet er opsat korrekt burde det se nogenlunde ud som på billedet nedenfor. 

TinkerCAD code

I vores Arduino kode, som kan ses forneden, vil vi først og fremmest declare(erklære) vores variabler vi gør brug af igennem programmet. En vigtig ting at have i mente er, at når man vil gøre brug af en servo, så skal man huske at anvende komponentens bibliotek via #include <Servo.h>. Uden dette vil der opstå fejl i programmet. Foruden vores variabler anvender vi også to bools, der agerer som “triggers” for at programmet ikke springer over nogle af vores funktioner. Inde i vores setup() angiver vi vores pins og servo så vi kan bruge dem i programmet, og sætter vores motorer i en stillestående tilstand. 

I vores loop(), hvor hele programmet egentlig kører, starter vi med at aflæse værdierne fra vores to LDR-modstande. Disse værdier bruger vi til at vurdere om RoboSchneider følger linjen, eller om vi skal rette kursen. Her har vi også vores første bool (onLine), som sørger for at denne del af programmet kun kører imens RoboSchneider faktisk kører på linjen. Vi bruger værdierne fra vores LDR-modstande til at bestemme hvilke motor pins der skal tilføres strøm. Hvis den ene side måler for lavt, så retter vi op på kursen. Når vi rammer den første målstreg, hvilket sker når begge LDR-modstande måler under den angivet minimumsværdi, ændrer vi vores onLine bool til at være false for at komme videre til den næste del af programmet. 

Den næste del af koden foregår kun efter vi har ramt den første målstreg. Her anvender vi vores Ultrasonic Distance Sensor (afstandsmåler) til at navigere den resterende del af vores opstillede bane. Vi starter med at aktivere pin 3, som sender en puls ud der bliver opfanget igen af afstandsmåleren. Denne værdi omregner vi til en målbar enhed som vi kan bruge til at vurdere afstanden fra RoboSchneider og de omkringliggende vægge. Hvis afstanden til væggen foran RoboSchneider ikke er under den angivet minimumsværdi, så kører den bare ligeud indtil den registrerer en genstand inden for minimumsafstanden. Når afstandsmåleren registrerer at vi er under minimumsgrænsen, så stopper vi RoboSchneiders fremgang for at undgå kollision. I praksis ville vi placere vores afstandsmåler ovenpå vores servo således vi kan rotere den og måle i forskellige retninger. Med servo.write(), i vores eksempel har vi navngivet vores for myServo, så kan man indsætte hvilken vinkel man vil indstille servoen til, eksempelvis 0, 90, eller 180 grader. Vi måler de tre retninger og gemmer dem i hver deres variabel. Disse variabler kan vi bruge til at sammenligne og vurdere hvor der er en væg, og hvor der er fri bane. Afhængig af hvilken retning der er fri, så justerer vi RoboSchneiders retning, hvorefter vi indstiller den til at køre fremad indtil der bliver registreret en ny væg/forhindring. Med denne opsætning er RoboSchneider i stand til at køre igennem en bane som starter med en streg i gulvet, som naturligvis skal følges til ende. Når enden af stregen er nået starter RoboSchneider på at køre efter Ultrasonic Distance Sensor (afstandsmåler) for at navigere igennem resten af banen, til målstregen bliver mødt. Hvis alt er opsat korrekt vil RoboSchneider opføre sig som vist på den første video oppe i toppen af bloggen her på siden. 

#include <Servo.h>


//Declare the needed variables
int left = A5;
int right = A4;
int sensorValue1;
int sensorValue2;
int minDiff = 50; //Maximum allowed difference between left and right sensor values

bool onLine = true;
bool firstDrive = false;

int dis = 100;
long tid;
int distance;
int servoLeft = 0;
int servoFront = 0;
int servoRight = 0; 

Servo theServo;

//Setup what needs to be stated before the robot begins its Loop
void setup()
{
  pinMode(11, OUTPUT);//right plus
  pinMode(10, OUTPUT);//right minus
  pinMode(6, OUTPUT);//left plus
  pinMode(5, OUTPUT);//left minus
  
  pinMode(3, OUTPUT); // Sets the trigPin as an OUTPUT
  pinMode(12, INPUT); // Sets the echoPin as an INPUT
  
  theServo.attach(9); // The Servo
  
  //Make sure motors are off
  digitalWrite (11, LOW);
  digitalWrite (10, LOW);
  digitalWrite (6, LOW);
  digitalWrite (5, LOW);
  
  Serial.begin(9600);
  
}

//The Loop Begins

void loop()
{
  //Assigning values to var's
  sensorValue1 = analogRead(left);
  sensorValue2 = analogRead(right);
  
  int diff = abs(sensorValue1 - sensorValue2);
  
  //"If" robot is following the ground-line; Do this:
  if (onLine == true)
  {
    // If the robot follows the line fine:
    if (diff <= minDiff && sensorValue1 >= 100 && sensorValue2 >= 100)
    {
        analogWrite(6, 100);
        analogWrite(5, 0);
        analogWrite(11, 100);
        analogWrite(10, 0);
        delay(1000);
      	firstDrive = true;
    }
    //If the robot is turning right, it turns left slightly
    else if (sensorValue1 < sensorValue2)
    {
    	analogWrite(6, 0);
        analogWrite(5, 0);
        analogWrite(11, 50);
        analogWrite(10, 0);
      	delay(1000);
    }
        // Vice Versa
    else if (sensorValue2 < sensorValue1)
    {
        analogWrite(6, 50);
        analogWrite(5, 0);
        analogWrite(11, 0);
        analogWrite(10, 0);
      	delay(1000);
    }
    
    //When the robot hits the ending of the ground line it stops and shifts over to "Off-Line" mode 
    else if (sensorValue1 <= 100 && sensorValue2 <= 100 && firstDrive == true)
    {
        analogWrite(6, 0);
        analogWrite(5, 0);
        analogWrite(11, 0);
        analogWrite(10, 0);
        onLine = false;
      	Serial.print("online is now false");
      	delay(1000);
    }
  }
        //THIS IS OFF-LINE MODE
        //If the end of the groundline is reached
  if (onLine == false)
  {
    digitalWrite(3, LOW);
  	delayMicroseconds(2);
  	digitalWrite(3, HIGH);
  	delayMicroseconds(10);
  	digitalWrite(3, LOW);
  	tid = pulseIn(12, HIGH);
  	distance = tid * 0.034 / 2;
  
  //if the SonarSensor signal is bigger than the chosen minimum distance it goes forward 
  	if (distance >= dis)
    {
    	analogWrite(6, 10);
        analogWrite(5, 0);
        analogWrite(11, 10);
        analogWrite(10, 0);
    }
    //if signal is smaller:
    else if (distance <= dis)
    {
      	analogWrite(6, 0);
        analogWrite(5, 0);
        analogWrite(11, 0);
        analogWrite(10, 0);
    
        //The check consists of 4 steps
        //Step 1 check distance to nearest wall in front of robot
        servoFront = distance;
        theServo.write(90);
        delay(2000);
        //Step 2 check distance to nearest wall left of robot
        servoLeft = distance;
        theServo.write(0);
        delay(4000);
        //Step 3 check distance to nearest wall right of robot
        servoRight = distance;
        theServo.write(90);
        delay(2000);

        //And step 4
        //Compare the 3 checks and turn to the wall farest away.

        //if the wall at left and the wall at right is not existing, it means the end is reached. The robot will turn right and drive off the ground.
        if (servoLeft == 0 && servoRight == 0)
        {
        	analogWrite(6, 55);
            analogWrite(5, 0);
            analogWrite(11, 0);
            analogWrite(10, 55);
            delay(1000);
            analogWrite(6, 0);
            analogWrite(5, 0);
            analogWrite(11, 0);
            analogWrite(10, 0);
            delay(1000);
            analogWrite(6, 255);
            analogWrite(5, 0);
            analogWrite(11, 255);
            analogWrite(10, 0);
            delay(1000);
        }
        //Turn to left wall
        if (servoLeft >= servoFront && servoLeft >= servoRight && servoRight != 0 || servoLeft == 0)
        {
        	analogWrite(6, 0);
            analogWrite(5, 55);
            analogWrite(11, 55);
            analogWrite(10, 0);
            delay(1000);
            analogWrite(6, 0);
            analogWrite(5, 0);
            analogWrite(11, 0);
            analogWrite(10, 0);
            delay(1000);
            analogWrite(6, 130);
            analogWrite(5, 0);
            analogWrite(11, 130);
            analogWrite(10, 0);
        }
        //Turn to right wall
        if (servoRight >= servoFront && servoRight >= servoLeft && servoLeft != 0 || servoRight == 0)
        {
        	analogWrite(6, 55);
            analogWrite(5, 0);
            analogWrite(11, 0);
            analogWrite(10, 55);
            delay(1000);
            analogWrite(6, 0);
            analogWrite(5, 0);
            analogWrite(11, 0);
            analogWrite(10, 0);
            delay(1000);
            analogWrite(6, 130);
            analogWrite(5, 0);
            analogWrite(11, 130);
            analogWrite(10, 0);
		}
        //Continue forward
        if (servoFront >= servoLeft && servoFront >= servoRight)
        {
        	analogWrite(6, 130);
            analogWrite(5, 0);
            analogWrite(11, 130);
            analogWrite(10, 0);
          	delay(1000);
        }
     }
  }
}

Unity

Nu tænder vi for Unity. Det første der skal gøres når Unity er åbent, er at importere den udleverede Unity.package så vi har et udgangspunkt at starte fra. Den bane vi gerne vil have RoboSchneider til at køre igennem kan ses på billedet nedenfor.

Vi starter med at trække “Robot root” ind i scenen: “PF2-afleveringsbane”. På Robot root sætter vi Wheels, Breadboard, Servo og Linesensor, som “child” til “CarChassis”. Her kan man lege lidt rundt med hvordan ens Robot skal se ud. Vi har valgt at lade Robotten se ud som på billedet nedenfor.

Her er det vigtigt at huske at gøre følgende:  

  •  Inde i Left Motor, nede i komponentet “Configurable Joint” skal CarChassis sættes ind i “Connected Body”. Ellers vil hjulene ikke følge med Robotten. Gør det samme for Right Motor.  
  • På Breadboard skal H-bridge LeftMotor og H-bridge RightMotor sættes som “child” af Breadboard. Herefter skal Left Motor sættes ind i Komponentet H Bridge (Script) for at den har en reference. Det samme skal gøre for RightMotor (med Right Motor i stedet for Left Motor selvfølgelig) 
  • På Breadboard skal der tilføjes 7 elementer i komponentet Breadboard(Script). (Man ændrer størrelsen af Arduino Object Pins ved at sætte “Size” til den ønskede størrelse). Indsæt H-bridge forwards pin LeftMotor til Element 0. Indsæt H-bridge backwards pin LeftMotor til Element 1. Indsæt H-bridge forwards pin RightMotor til Element 2. Indsæt H-bridge backwards pin RightMotor til Element 3. Indsæt DistanceSensor til Element 4. Indsæt LineSensorLeft til Element 5 samt LineSensorRight til Element 6. Disse 7 Elementer er referencer for Breadboardet, og virker som “pins” i virkeligheden.  

Nu er Robotten bygget, så nu skal der skrives kode, så RoboSchneider bliver funktionel. Man kan med fordel tage koden fra TinkerCAD og bruge som skabelon i Unity. Desværre er kodesproget ikke det samme for Arduino, som det er for Unity, så vi skal skrive noget af koden om, samt tilføje mere kode. Gå ind i ArduinoMain.cs. 

I Unity (C#) skal man ikke Declare (erklære) de forskellige pinModes, da vi har referencerne liggende ude i selve Editoren. #include <Servo.h> skal ikke indgå i C# koden, derfor heller ikke “Servo theServo; og theServo.attach(pin);. Måden vi refererer til Servo’en i Unity er gennem Editoren hvor Servo’en bliver sat ind som en public Servo servo; i ArduinoMain.cs scriptet. Desuden skal “long” ændres til “ulong”. Serial virker heller ikke i C#, derfor bruger vi Debug.Log();.  

Den første del der skal tilføjes i C# koden, er en bool samt en “if” statement til at fortælle os hvornår Robotten kommer til den første forhindring. Den kalder vi firstBlock. I denne “if” statement hardkoder vi en omkørsel, som sørger for at RoboSchneider kører uden om den første forhindring. Da man kun støder på én forhindring i denne bane vælger vi at hardkode denne del. Hvis der var flere forhindringer i banen, kunne man med fordel lave en funktion i stedet for at skrive hele omkørslen hver gang RoboSchneider skulle forbi en forhindring. Til sidst skal alle “delay()” ændres til “yield return new WaitForSeconds(); da “delay()” funktionen ikke er en del af C# dokumentationen. 

Hvis koden er skrevet korrekt skulle den gerne ligne noget i stil med den kode der står herunder. 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;

public class ArduinoMain : MonoBehaviour
{
    public Breadboard breadboard;
    public Servo servo;
    
    //Setup what needs to be stated before the robot begins its Loop
    IEnumerator setup()
    {
        //Set Servo Motor on 90 degrees (look forward)
        servo.write(90);
        Debug.Log("GO");

        #region PremadeSetup
        yield return StartCoroutine(loop()); ;
        #endregion PremadeSetup
    }
    //Declare the needed variables
    int left = 0;
    int right = 0;
    int minDiff = 50;
    
    bool onLine = true;
    bool firstBlock = false;

    ulong dis = 900;

    ulong servoLeft = 0;
    ulong servoFront = 0;
    ulong servoRight = 0;

    //The Loop Begins
    IEnumerator loop()
    {
        //Read the LDR-Sensores
        left = analogRead(5);
        right = analogRead(6);

        int diff = Mathf.Abs(left - right);
        
        //Read the SonarSensor
        ulong tid = pulseIn(4);
        //Read the ServoMotor
        int servoPos = servo.read();
        
        //As long as we are on the groundline
        if (onLine == true)
        {
            //If the robot is turning right, it turns left slightly
            if (left < right)
            {
                analogWrite(0, 0);
                analogWrite(1, 0);
                analogWrite(2, 50);
                analogWrite(3, 0);
            }
            // Vice Versa
            else if (right < left)
            {
                analogWrite(0, 50);
                analogWrite(1, 0);
                analogWrite(2, 0);
                analogWrite(3, 0);
            }
            // If the robot follows the line fine:
            else if (diff <= minDiff)
            {
                //it goes forward
                if (tid >= dis || tid == 0)
                {
                    analogWrite(0, 100);
                    analogWrite(1, 0);
                    analogWrite(2, 100);
                    analogWrite(3, 0);
                }
                // else it hits an obsticle
                else if (tid <= dis)
                {
                    //There is a fail in the program that makes it delay just enough for the robot to bug on start-up. This "if" prevents it from stopping
                    if (firstBlock == false)
                    {
                        firstBlock = true;
                        Debug.Log("FirstBlockPassed");
                    }
                    //After the bug, the robot avoits absticles but driving around them as shown below.
                    else if (firstBlock == true)
                    {
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 0);
                        analogWrite(1, 55);
                        analogWrite(2, 55);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(1.1f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 100);
                        analogWrite(1, 0);
                        analogWrite(2, 100);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.7f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 55);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 55);
                        yield return new WaitForSeconds(1f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 100);
                        analogWrite(1, 0);
                        analogWrite(2, 100);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(1.5f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 55);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 55);
                        yield return new WaitForSeconds(1f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 100);
                        analogWrite(1, 0);
                        analogWrite(2, 100);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.6f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                        analogWrite(0, 0);
                        analogWrite(1, 55);
                        analogWrite(2, 55);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(1f);
                        analogWrite(0, 0);
                        analogWrite(1, 0);
                        analogWrite(2, 0);
                        analogWrite(3, 0);
                        yield return new WaitForSeconds(0.5f);
                    }
                }
            }
            //When the robot hits the ending of the ground line it stops and shifts over to "Off-Line" mode 
            if (left <= 1000 && right <= 1000)
            {
                analogWrite(0, 0);
                analogWrite(1, 0);
                analogWrite(2, 0);
                analogWrite(3, 0);
                onLine = false;
            }
        }
        //THIS IS OFF-LINE MODE
        //If the end of the groundline is reached
        if (onLine == false)
        {
            //Debug.log("Offline") to make it obvious for the spectators
            Debug.Log("OffLine");

            //if the SonarSensor signal is bigger than the chosen minimum distance it goes forward 
            if (tid >= dis)
            {
                analogWrite(0, 130);
                analogWrite(1, 0);
                analogWrite(2, 130);
                analogWrite(3, 0);
            }
            //if the SonarSensor signal is smaller than the chosen minimum distance it stops and makes a check 
            else if (tid <= dis)
            {
                analogWrite(0, 0);
                analogWrite(1, 0);
                analogWrite(2, 0);
                analogWrite(3, 0);
                yield return new WaitForSeconds(2);

                //The check consists of 4 steps
                //Step 1 check distance to nearest wall in front of robot
                servoFront = pulseIn(4);
                Debug.Log("Front Servo =" + servoFront);
                servo.write(0);
                yield return new WaitForSeconds(2);
                //Step 2 check distance to nearest wall left of robot
                servoLeft = pulseIn(4);
                Debug.Log("Left Servo =" + servoLeft);
                servo.write(180);
                yield return new WaitForSeconds(4);
                //Step 3 check distance to nearest wall right of robot
                servoRight = pulseIn(4);
                Debug.Log("Right Servo =" + servoRight);
                servo.write(90);
                yield return new WaitForSeconds(2);

                //And step 4
                //Compare the 3 checks and turn to the wall farest away.

                //if the wall at left and the wall at right is not existing, it means the end is reached. The robot will turn right and drive off the ground.
                if (servoLeft == 0 && servoRight == 0)
                {
                    Debug.Log("ENDGAME");
                    analogWrite(0, 55);
                    analogWrite(1, 0);
                    analogWrite(2, 0);
                    analogWrite(3, 55);
                    yield return new WaitForSeconds(1f);
                    analogWrite(0, 0);
                    analogWrite(1, 0);
                    analogWrite(2, 0);
                    analogWrite(3, 0);
                    yield return new WaitForSeconds(1f);
                    analogWrite(0, 255);
                    analogWrite(1, 0);
                    analogWrite(2, 255);
                    analogWrite(3, 0);
                    yield return new WaitForSeconds(10f);
                }
                //Turn to left wall
                if (servoLeft >= servoFront && servoLeft >= servoRight && servoRight != 0 || servoLeft == 0)
                {
                    analogWrite(0, 0);
                    analogWrite(1, 55);
                    analogWrite(2, 55);
                    analogWrite(3, 0);
                    yield return new WaitForSeconds(1f);
                    analogWrite(0, 0);
                    analogWrite(1, 0);
                    analogWrite(2, 0);
                    analogWrite(3, 0);
                    yield return new WaitForSeconds(1f);
                    analogWrite(0, 130);
                    analogWrite(1, 0);
                    analogWrite(2, 130);
                    analogWrite(3, 0);
                }
                //Turn to right wall
                if (servoRight >= servoFront && servoRight >= servoLeft && servoLeft != 0 || servoRight == 0)
                {
                    analogWrite(0, 55);
                    analogWrite(1, 0);
                    analogWrite(2, 0);
                    analogWrite(3, 55);
                    yield return new WaitForSeconds(1f);
                    analogWrite(0, 0);
                    analogWrite(1, 0);
                    analogWrite(2, 0);
                    analogWrite(3, 0);
                    yield return new WaitForSeconds(1f);
                    analogWrite(0, 130);
                    analogWrite(1, 0);
                    analogWrite(2, 130);
                    analogWrite(3, 0);

                }
                //Continue forward
                if (servoFront >= servoLeft && servoFront >= servoRight)
                {
                    analogWrite(0, 130);
                    analogWrite(1, 0);
                    analogWrite(2, 130);
                    analogWrite(3, 0);
                }
            }
        }
        
        #region DoNotDelete
        //Wait for one frame
        yield return new WaitForSeconds(0);
        //New loop():
        yield return loop();
        #endregion DoNotDelete 
    }
    
    #region PremadeDefinitions
    void Start()
    {
        servo = FindObjectOfType<Servo>();
        if (servo == null)
        {
            Debug.Log("No servo found in the scene. Consider assigning it to 'ArduinoMain.cs' manually.");
        }
        Time.fixedDeltaTime = 0.005f; //4x physics steps of what unity normally does - to improve sensor-performance.
        StartCoroutine(setup());


    }

    IEnumerator delay(int _durationInMilliseconds)
    {
        float durationInSeconds = ((float)_durationInMilliseconds * 0.001f);
        yield return new WaitForSeconds(durationInSeconds);
    }

    public long map(long s, long a1, long a2, long b1, long b2)
    {
        return b1 + (s - a1) * (b2 - b1) / (a2 - a1);
    }

    public ulong millis()
    {
        return (ulong)(Time.timeSinceLevelLoad * 1000f);
    }

    public ulong abs(long x)
    {
        return (ulong)Mathf.Abs(x);
    }

    public long constrain(long x, long a, long b)
    {
        return (x < a ? a : (x > b ? b : x));
    }


    #endregion PremadeDefinitions

    #region InterfacingWithBreadboard
    public int analogRead(int pin)
    {
        return breadboard.analogRead(pin);
    }
    public void analogWrite(int pin, int value)
    {
        breadboard.analogWrite(pin, value);
    }
    public bool digitalRead(int pin)
    {
        return breadboard.digitalRead(pin);
    }
    public void digitalWrite(int pin, bool isHigh)
    {
        breadboard.digitalWrite(pin, isHigh);
    }

    public ulong pulseIn(int pin)
    {
        return breadboard.pulseIn(pin);
    }
    #endregion InterfacingWithBreadboard
}

Konklusion

RoboSchneider fuldfører den opstillede bane på omtrent 87 sekunder. Den navigerer rundt ved hjælp af en række sensorer, som var påkrævet af opgavebeskrivelsen, nemlig LDR-modstande samt ultralydafstandsmåler. Den er i stand til at nå banens ende og undgå al kollision undervejs. 

Perspektivering 

RoboSchneider er ikke i stand til at fuldføre banen fuldstændig på egen hånd. Med dette menes, at vi var nødsaget til at implementere en lille smule “hard coding” til at navigere rundt om den første forhindring, hvilket vi også belyser tidligere i indlægget. Ydermere har RoboSchneider ikke nødvendigvis den hurtigste tid, men den formår at opnå en sikker og stabil gennemførelse af banen. Med lidt mere justering og testing kan denne tid forbedres, men vi har valgt at beholde den i dens nuværende tilstand med en solid performance for opgavens og forståelsens skyld. 

Afslutningsvist nævner vi, at delay() funktionen ikke virker i C#. Da dette teknisk set er sandt, så er vi opmærksomme på, at Jamie og Christian var med til at udarbejde en IEnumerator “delay” funktion, men da vi havde problemer med at implementere den, så var vi nødsaget til at bruge WaitForSeconds() i stedet. 

More