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

Leave a Reply