OS Project of Fall 2017: "Cartography"

Algorithms

Carthography strategy

What would you do if you suddently woke up in an unknown place? I guess you would look around and then figure out where you are by exploring the place you are in. If there are different rooms, you would go into each one of them an see what they contain, right? Taking this same approach, Suka wakes up in the arena, and starts scanning around itself. Then it determines where it has the most chance to scan a big proportion of the arena and goes on until all of it has been discovered.

File Structure

Path Description
client/robotclient.c This is the file containing the main functions
client/constants.m File defining a number of useful global variables used by many functions
client/classes.c Here we define a few fundamental structures helpful to handle the positioning of the robot and the mapping in general
client/makefile Builds both the robot client and the test files we made during the project
client/gsyst.c Defines functions for the interactions with the motors and sensors
client/servercom.c Functions for the communication with the server
client/map.c Map related functions for scanning the arena
client/spot.c Determines in which spot to be in order to scan optimaly
client/path.c The pathfinding functions helping the robot to move around the arena

The makefile : compile from multiple sources [expand]

Our makefile detects the system architecture and compiles accordingly. This helps if you use docker to compile your program, which you should because the robot is very slow at compiling. The makefile will know wether it's executing in docker/your computer or the robot using the uname -m bash command.
The order in which we present the structure of the makefile is meant to help understanding, but is different from the actual file.


ARCH=$(shell uname -m)

ifeq ($(ARCH),x86_64) #this means we are on the docker platform
	CC=arm-linux-gnueabi-gcc
	CFLAGS=-W -Wall -ansi -pedantic
	LDFLAGS=-I/src/ev3dev-c/source/ev3/ -lm /src/ev3dev-c/lib/libev3dev-c.a
	BT=  #we didn't manage to make the bluetooth work when compiling from docker.
endif
ifeq ($(ARCH),armv5tejl) #this means we are compiling on the brick
	CC=gcc
	CFLAGS=-W -Wall -ansi -pedantic
	LDFLAGS=
	BT=-lbluetooth  #we didn't manage to make the bluetooth work when compiling from docker.
endif

%.o: %.c is triggered whenever a .o file is requested, and depends on the homonymous .c file. This is meant to pre-compile our modules.

%.o: %.c constants.m
	$(CC) -o $@ -c $< $(LDFLAGS)

The compililation of the main executable is then pretty straight forward :

EXEC=robotclient

all: $(EXEC)

$(EXEC): $(EXEC).o gsyst.o path.o spot.o servercom.o map.o
	$(CC) $^ $(LDFLAGS) -o $(EXEC)

$(EXEC).o: robotclient.c gsyst.h constants.m path.h spot.h servercom.h map.h
	$(CC) -o $@ -c $< $(LDFLAGS)

During the project, we kept a number of test files for each module in order to test out their functions.



module_name: module_name_test.o module1.o module2.o module3.o module4.o
	$(CC) $^ $(LDFLAGS) -o spot
module_name_test.o: module_name_test.c module1.h module2.h module3.h module4.h constants.m
	$(CC) -o $@ -c $< $(LDFLAGS)

Finally, a cleaning command is made to delete the binary files (useful to do before a git add . to avoid including them in your repo)

.PHONY: clean mrproper #tells that clean and mrproper are not filenames

clean:
	rm -rf *.o

mrproper: clean
	rm -rf $(EXEC)
	rm -rf module_name

Our protocol

Upon receiving the start signal, the robot starts by creating a thread that will send the position to the server every 2 second through the use of pthread_create(&positioning, NULL, thSendPosition, NULL) and a second thread to receive any secial instructions sent from the server : pthread_create(&receiving, NULL, thReceiveFromServer, NULL)
Then, the initial map is generated! char* map = get_new_local_map(width, height); The map's width and height is given as arguments when launching the program.
Each character represents a tile -- or pixel -- for wether it is a free tile, a wall or a yet unknown space in the arena.

/* Code for the rest of the program on the large arena */
// position the robot at one extremity of the map
start_position = tCoord_new(width/2, height - 300*MM_TO_PIX_SIZE); //This automaticaly creates a virtual wall behind the robot

// scan a first time to get the surroundings :
scan(robotPosition, start_position, width, height, map);
tCoord previousSpots[4*width*height/(DIST_MIN_FROM_WALLS*DIST_MIN_FROM_WALLS)]; //A list of all the spots ou robot is going to go
size_t size_previousSpots = 0;
while(!mapComplete(map))
{   // determine a new spot in order to maximise the new area to explore
    tCoord spot = getNewSpot(Point_to_tCoord(fPoint_to_Point(robotPosition), start_position),
                             width,
                             height,
                             map);
    previousSpots[size_previousSpots++] = spot;
    if(getPathTo(spot))  // writes into a file the checkpoints we have to go by in order to avoid obstacles
    {
        printf("failed to find the path to the new coordinates");
        exit(EXIT_FAILURE);
    }
    FILE *path = fopen(PATH_PATH, "r"); //PATH_PATH is the path to the fie containing the checkpoints
    int pathLen = countlines(path);
    fclose(path);
    int i;
    for(i=0; i<pathLen; i++)
    {
        tCoord checkPoint = getCheckpoint(i, pathLen); //pops the next node from the file.
        if(moveTo(checkPoint, start_position, map))
        {
            printf("failed to move to the next coordinates");
            exit(EXIT_FAILURE);
        }
    }
    scan(robotPosition, start_position, width, height, map); //finally, when we get to the right spot, scan and start over
}
DONE_EXPLORING = 1; //signals the threads to stop
sendMap();

Learn more - our home made structures

We have made several useful structures to represent coordinates for either :

  • Cells in a table tCoord
  • Integer points Point
  • Floating points fPoint

They are used extensively in the project and we encourage looking at how we define and use them in classes.h and classes.c.


Guidance System

Here we show the core functions that makes the robot do it's magic.

int refresh_distance() refreshes the global variable distance with the output of the sonar.

int refresh_angle() refreshes the global variable angle with the output of the gyroscope. In order to increase presision, angle takes the average value of several consecutive measures.

int init_mov_motors() loads the pointers to use the motors into some global variables.

void start_straight(int speed_increment) makes the robot go forward or backward, depending on the sign of the increment.

void start_turn(int speed_increment) makes the robot turn on itself one way or another, depending on the sign of the increment.

void stop_mov_motors() stops the robot.

int turn_to_angle(float ang) makes the robot turn until angle == ang (obviously we don't really test if those floats are equal).

void continue_until(float goal) robot advances until distance < goal

int grab(int instruct) uses the front mechanism in one way or the other depending on the instruction.

Scanning

During the scan, the robot turns on itself and continuously asks for the output of the gyroscope and sonar. Then it modifies the map according to some rules :

  • Do not scan further than the distance SCANNING_MAX_DISTANCE
  • If the distance measured from sonar is under that specific value, then assign a wall to the measured position on the map
  • For the first position you measure, mark all of the pixels between that position and the robot as free
  • For the following measures, consider the triangle defined by the previous point, the current measured point and the position of the robot. Mark all the tiles inside it as free
  • Do not overwrite known pixels if they are outside your precision radius

For the sake of comprehension, the next snippet of code is a simplified (yet functional) version of the scan function. It does not necessarily respond to all of the previous criterias

int scan(tCoord robot_coord_on_map, char* map, int width_n, int height)
{
    Point O = Point_new(0,0);
    int mm_to_pixel_size = 10*PIXEL_SIZE; //can convert a size in mm to the length of a pixel
    float distance_before_setting_a_new_wall = SCANNING_MAX_DISTANCE - .4; //-.4 because of rounding errors
    Point measured_point; //coordinates of the tile/pixel the sonar hits into.
    // last_point will take the value of previously measured point.
    Point last_point =  Point_new(0,0); //initialised like O so as to know if nothing has been measured before

    refresh_angle(); //refreshes the global variable angle
    float starting_scan_angle = angle;
    start_turn(SCANNING_SPEED); //The robot starts turning
    while(abs(starting_scan_angle-angle)<FULL_TURN_ANGLE)
    {
        refresh_angle();
        refresh_distance();
        distance = min(SCANNING_MAX_DISTANCE, distance);
        fPoint fmeasured_point = fPoint_new(distance*cos(2*pi*angle/FULL_TURN_ANGLE)/mm_to_pixel_size,
                                            distance*sin(2*pi*angle/FULL_TURN_ANGLE)/mm_to_pixel_size);
        measured_point = fPoint_to_Point(fmeasured_point); //Converts a point's coordinates from floats to integers.                                     '
        tCoord coord = Point_to_tCoord(measured_point, robot_coord_on_map);
        if( Point_eq(last_point,O) )
        {
            free_pixels_between(O, measured_point, map);
        } else
        {
            free_pixels_in_trigon(O,last_point, measured_point, map);
        }
        // And set the furthest pixel to become a wall if appropriate.
        if(distance < distance_before_setting_a_new_wall)
        {
            set_char(coord,width_n,height,WALL_PIXEL,map);
        }
        last_point = measured_point;
    }
    stop_mov_motors(); //stops the robot from turning.
    return 0;
}

Technical difficulties [expand]

With this simple scanning function, we had the following problems :

  • Problem 1 : If the measured distance is farther than about 35cm, the scanning would skip some pixels over -- each measure on a wall would be non-adjacent pixels -- and therefore create a "hole in the wall"
  • Problem 2 : Stright walls would look curved due to the way we measure the angle and the delay before measuring distance
  • Problem 3 : The sonar would make inexplicable and arbitrary jumps on a continuous surface

Solutions :

Problem 1 :
  • code optimisation to work better
  • interpolation
Problem 2 :

The second problem was due to refresh_angle() recording a certain number of consecutives values from the gyroscope before refreshing angle with a precise value. Therefore, we chose to create a twin function devoid of that feature and use it here.

Problem 3 :

These jumps seem to happen on areas further away from the robot, so the rule of "Do not overwrite known pixels if they are outside your precision radius"

Optimisation

In order to help with both the first and the second issue, we proceeded to optimise a lot of the code involved during scanning. None of the following changes make an enormous speed increase, but are significant.

  • Prefer arithmetic solution over analityc ones
  • free_pixels_between(...) and free_pixels_in_trigon(...) were very poorly designed in that regard

  • If variables that serve the same purpose are created multiple times over will be put in common / make a variable serve multiple purposes.
  • Kill two birds with one stone ait'?

  • Avoid to do a task that would be unnecessary
  • It can happen that two measures can land in the same tile. In that case, the simplified code would start rewritting over the same pixels.

Interpolation

This is an option we didn't try extensively but the ideas is to record the duration between the measure of the angle and the distance. Then, a function would correct the current angle with the knowledge of the rotation speed


Pathfinding

Between two rounds of scanning, the robot needs to get to it's next viewpoint, preferably without hitting anything. This is why we need to find the right path using a "greedy" version of the Dijkstra algorithm (some people might say that we cut corners...)

Server Communications

To communicate with the game server, we have implemented the protocol described in the project repository. We used threads to let the robot listen for server messages and periodically send its position.

ssize_t read_from_server(int sock, char *msg, size_t maxSize) {
    printf("meh\n");
    ssize_t bytes_read = read(sock, msg, maxSize);
    printf("meh?\n");
    if (bytes_read <= 0) {
        fprintf(stderr, "Server unexpectedly closed connection...\n");
        close(sock);
        exit(EXIT_FAILURE);
    }
    printf("[DEBUG] received %d bytes\n", bytes_read);
    return bytes_read;
}

int send_to_server(int sock, char *data, size_t size) {
    return write(sock, data, size);
}

int send_POSITION(int sock, int16_t x, int16_t y){
    char string[9];
    *((uint16_t *) string) = msgId++;
    string[2] = TEAM_ID;
    string[3] = 0xFF;
    string[4] = MSG_POSITION;
    string[5] = x;
    string[6] = 0x00;
    string[7] = y;
    string[8] = 0x00;
    return send_to_server(sock, string, 9);
}

int send_MAPDATA(int sock, int16_t x, int16_t y, uint8_t R, uint8_t G, uint8_t B){
    char string[12];
    *((uint16_t *) string) = msgId++;
    string[2] = TEAM_ID;
    string[3] = 0xFF;
    string[4] = MSG_MAPDATA;
    string[5] = x;
    string[6] = 0x00;
    string[7] = y;
    string[8] = 0x00;
    string[9] = R; // red value of pixel
    string[10] = G; // green value of pixel
    string[11] = B; // blue value of pixel
    return send_to_server(sock, string, 12);
}

int send_MAPDONE(int sock) {
    char string[5];
    *((uint16_t *) string) = msgId++;
    string[2] = TEAM_ID;
    string[3] = 0xFF;
    string[4] = MSG_MAPDONE;
    return send_to_server(sock, string, 5);
}

void *thSendPosition() {
    while(!DONE_EXPLORING) {
        Sleep(2000);
        setPosition();
        send_POSITION(s, posX, posY);
    }
    pthread_exit(NULL);
}