Introduction

Goal

For this milestone, we needed to implement a working maze traversal algorithm, both in simulation and in real-life. Our goal was to implement the simulation portion using python and the real-life portion in the Arduino IDE. Additionally, both parts of our implementation needed an indicator to show when the robot has finished traversing the maze.

In Simulation

For the simulation portion, we chose to implement a Depth First Search algorithm using python. The advantage of the simulation portion is that all the wall information for the maze is known beforehand. The idea of the Depth First Search (DFS) is to start with a graph (maze) of all unvisited nodes. At each iteration of the algorithm, move forward to an unvisited node in the graph, marking this new node as visited. If you encounter a node from which it is impossible to move forward to a new, unvisited node, move backward to the node you just came from and check if it is possible to move to another unvisited node from that position. If it is possible, begin moving forward again. If it is not possible, backtrack once again.

If you backtrack all the way to the starting node and there are no possible moves to an unvisited node from that position, the algorithm is finished. If all the nodes in the graph have been visited, great! If some nodes have not been visited, mark them as unreachable.

Before discussing the specifics of our implementation in python, we should mention how we chose to encode maze information. For our test cases, we created 5x4 arrays of 7-bit values to represent mazes. The information each bit encodes is as follows:

  • 0th bit represents the north wall
  • 1st bit represents the south wall
  • 2nd bit represents the east wall
  • 3rd bit represents the west wall
  • 4th bit represents that the node is unreachable
  • 5th bit represents that the node has been visited
  • 6th bit represents that robot is currently at the node

For example, 1101010 means that the robot is currently at the node, the node has been visited, the node is not unreachable, and the node has walls on its west and south sides.

For our maze exploration algorithm, we found the possible moves at each node using the following logic. If there is no wall to the north and the node to the north has not been visited, then moving north is a possible move. We used the exact same logic for determining whether south, east, and west were possible moves. In order to keep track of the possible moves at a node, we created an array called possible_moves. Once the possible moves were found, we randomly chose the next move to make using:


                            # move in random direction
                            next_move = random.choice(possible_moves)
                            self.maze[current_row][current_column] &= 0b0111111
                            backtrack.append((current_row, current_column))
                            if (next_move == 0b0001):
                            	current_row -= 1
                            elif (next_move == 0b0010):
                            	current_row += 1
                            elif (next_move == 0b0100):
                            	current_column += 1
                            elif (next_move == 0b1000):
                            	current_column -= 1
                            self.maze[current_row][current_column] |= 0b1100000
                            self.update_maze()
                            time.sleep(.1)
                        

First, the next move is chosen randomly. Then, we set the MSB of the previous node low, indicating that it is no longer the robot's current position. Next, the previous node is added to an array called backtrack, which we will discuss shortly. The if/elif statements are what make the actual move. The movements are encoded as: 0b0001 = move north, 0b0010 = move south, 0b0100 = move east, and 0b1000 = move west. We then set the two MSBs of the new node high, indicating that it is now the robot's current position and has been visited. Finally, we update the maze GUI, which we will discuss shortly, as well.

As alluded to earlier, we used another array called backtrack to help facilitate moving backward in the algorithm. When we move forward from a node, we add it to this backtrack array, as was seen above. When we reach a node that has no valid forward moves to make (no unvisited nodes adjacent to it) we backtrack using:


                            # no more moves ahead -> backtrack
                            if (len(possible_moves) == 0):
                            	self.maze[current_row][current_column] &= 0b0111111
                            	prev_node = backtrack[-1]
                            	backtrack.pop()
                            	current_row = prev_node[0]
                            	current_column = prev_node[1]
                            	self.maze[current_row][current_column] |= 0b1100000
                            	self.update_maze()
                            	time.sleep(.1)
                        

When the length of possible_moves is zero, we know we need to backtrack. First, we clear the MSB of the current node to indicate that it will no longer be the robot's current position. We then take the node most recently added to backtrack and move to it, setting its two MSBs high. Finally, we update the maze GUI accordingly.

We created the maze GUI using python's tkinter package. Using this package, we made a 5x4 grid of colored squares to represent the maze. Each time the robot moves, we update the GUI. We colored the squares according to the following scheme:

  • White means no information about that position yet
  • Green means that position is the robot's current location
  • Blue means that position has already been visited
  • Red means that position is unreachable

Our maze exploration algorithm ends with the following:


                            for row in range(NUM_ROWS):
                                for column in range(NUM_COLUMNS):
                                	if (not self.maze[row][column] & 0b0100000): # unvisited
                                		self.maze[row][column] |= 0b0010000
                            self.update_maze()
		
                            self.done_sign = Tkinter.Label(text="Done Exploring!", fg="black", bg="white", state="normal")
                            self.done_sign.grid(columnspan = 1)
                        

For each node in the graph, if the 2nd MSB is not high, indicating that the node has not been visited, we set the 3rd MSB high, indicating that the node is unreachable. Finally, the algorithm ends by printing a "Done Exploring!" label at the bottom of the GUI.

Here is our maze traversal algorithm and GUI in action!

In Real Life

Wall Sensors

For milestone 2, our robot implemented a wall detection algorithm with only one wall sensor. We decided to add two additional short range IR wall sensors to the left and right sides of the robot to facilitate wall detection when mapping the maze. By checking the wall sensor values via the Arduino Serial Monitor, we were able to identify a threshold value to determine when there is a wall. The mounted wall sensors are shown in the picture below on the front, left, and right sides of the robot.

Due to the addition of more sensors, we needed more analog input pins on the Arduino for our robot to function for Milestone 3. We decided to implement an 8-1 multiplexer with 3 select bits dependent on three bits from the digital Arduino pins. The Left, Front, and Right wall sensors were connected to the Y0 (000), Y1 (001), and Y2 (010) pins of the multiplexer respectively. For example, when calling the detectLeftWall() function the digital select pins S0, S1, and S2 are set to 000 via Arduino to indicate we are checking the wall sensor at Y0 of the multiplexer. The output of the multiplexer is then connected to an analog input pin on the Arduino.

Wall Detection

We had to update our code from Milestone 2 to include additional functions for detecting walls to the left and right of the robot. Notice in the code below for detectLeftWall() we digital write the select pins of the Arduino to 000 in order to select the correct wall sensor to check.


                        bool detectLeftWall() {

                            digitalWrite(wallbit2, LOW);
                            digitalWrite(wallbit1, LOW);
                            digitalWrite(wallbit0, LOW);

                            int wall = analogRead(wallpin);
  
                            if (wall>250) {
                                return true;
                            }
                            else {
                                return false;
                            }
                        }
                        

Traversing the Maze

The rest of the algorithm followed similar logic as in the simulation. We began by initializing multiple vairables to keep track of the robot's current state and the state of the maze. Keeping with the encoding system used in the simulation, we created a 5 by 4 unsigned char array of 1 byte values. Each byte represents a square in the maze and whether the square has been visited, is currently being visited, is unreacheable and which walls surround this junction (north, south, east and/or west). We also created a variable, dir, to represent the robot's current facing. If the robot is facing north, dir=0, if east, dir=1, if south, dir=2, and if west, dir=3. currentRow and currentColumn are also used to keep track of the current square that the robot is visiting. We assume that we always start at the bottom right corner facing north so, upon startup, the following code initializes the position and facing of the robot:


                          currentRow = 4;
						  currentColumn = 3; 
						  dir = 0;
						  maze[4][3] |= (CURRENT) | (VISITED);
                        
                        

We then update the square we are currently in by checking the walls around us and adjusting the byte representing the current sqaure appropriately. We do this using our helper function, check(), which is shown below.


                          void check(){
                          	  \\check the wall in front of us and or the current value with the appropriate wall value.
							  if(detectFrontWall()){
							    if(dir = 0){
							      maze[currentRow][currentColumn] |= NORTHWALL;
							    }
							    else if(dir = 1){
							      maze[currentRow][currentColumn] |= EASTWALL;
							    }
							    else if(dir = 2){
							      maze[currentRow][currentColumn] |= SOUTHWALL;
							    }
							    else if(dir = 3){
							      maze[currentRow][currentColumn] |= WESTWALL;
							    } 
							  }
							  \\check the wall to the left of us and or the current value with the appropriate wall value.
							  if(detectLeftWall()){
							    if(dir = 0){
							      maze[currentRow][currentColumn] |= WESTWALL;
							    }
							    else if(dir = 1){
							      maze[currentRow][currentColumn] |= NORTHWALL;
							    }
							    else if(dir = 2){
							      maze[currentRow][currentColumn] |= EASTWALL;
							    }
							    else if(dir = 3){
							      maze[currentRow][currentColumn] |= SOUTHWALL;
							    } 
							  }
							  \\check the wall to the right of us and or the current value with the appropriate wall value.
							  if(detectRightWall()){
							    if(dir = 0){
							      maze[currentRow][currentColumn] |= EASTWALL;
							    }
							    else if(dir = 1){
							      maze[currentRow][currentColumn] |= SOUTHWALL;
							    }
							    else if(dir = 2){
							      maze[currentRow][currentColumn] |= WESTWALL;
							    }
							    else if(dir = 3){
							      maze[currentRow][currentColumn] |= NORTHWALL;
							    } 
							  }
                        
                        

After we have checked the walls around us, we then search for a possible next move using the helper function, possibleMove(). We employed a simple depth first search algorithm. We first checked if there was not a wall north of our position (with north always representing the direction towards a lower valued row, aka the top of the maze). If there was no north wall blocking our way and the square to the north has not been visited before, we move north. Otherwise, we check the east. Then we check the south followed by checking the west. If we cannot move to an unvisited square in any direction, we backtrack. We use an array to keep track of the squares we have visited in the order that we visited them. A backTrackPointer variable keeps track of which node we should backtrack to in order to discover new squares in the maze. Below shows the function, possibleMove().


                        void possibleMove(){
							  maze[currentRow][currentColumn] &= 0x3F;
							  if((!(maze[currentRow][currentColumn] & NORTHWALL)) && (!(maze[currentRow-1][currentColumn] & VISITED))){
							    backTrack[backTrackPointer][0] = currentRow;
							    backTrack[backTrackPointer][1] = currentColumn;
							    backTrackPointer += 1;
							    moveNorth();
							  }
							  else if((!(maze[currentRow][currentColumn] & EASTWALL)) && (!(maze[currentRow][currentColumn+1] & VISITED))){
							    backTrack[backTrackPointer][0] = currentRow;
							    backTrack[backTrackPointer][1] = currentColumn;
							    backTrackPointer += 1;
							    moveEast();
							  }
							  else if((!(maze[currentRow][currentColumn] & SOUTHWALL)) && (!(maze[currentRow+1][currentColumn] & VISITED))){
							    backTrack[backTrackPointer][0] = currentRow;
							    backTrack[backTrackPointer][1] = currentColumn;
							    backTrackPointer += 1;
							    moveSouth();
							  }
							  else if((!(maze[currentRow][currentColumn] & WESTWALL)) && (!(maze[currentRow][currentColumn-1] & VISITED))){
							    backTrack[backTrackPointer][0] = currentRow;
							    backTrack[backTrackPointer][1] = currentColumn;
							    backTrackPointer += 1;
							    moveWest();
							  }
							  else{
							    backTracks();
							  }
							  
							}
                        
                        

You will notice that each of these cases calls other functions in order to facilitate the movement of the robot. In order to move in a particualr direction, we must first take into accound the robots current direction and make the appropriate turns before moving forward the find a new junction. Below shows our moveNorth() function. The functions to move in the other directions work in the same manner. The moveForward(), detectJunction() and turning functions are the same as in previous labs and milestones.


                        void moveNorth(){
							  if(dir==0){
							    while (detectJunction()==false){
							      moveForward();
							    }
							    stepPastJunction();
							  }
							  if(dir==1){
							    turnLeft();
							    while (detectJunction()==false){
							      moveForward();
							    }
							    stepPastJunction();
							  }
							  if(dir==2){
							    turnLeft();
							    turnLeft();
							    while (detectJunction()==false){
							      moveForward();
							    }
							    stepPastJunction();
							  }
							  if(dir==3){
							    turnRight();
							    while (detectJunction()==false){
							      moveForward();
							    }
							    stepPastJunction();
							  }
							  //update our current position and heading
							  dir = 0;
							  currentRow -=1;
							  maze[currentRow][currentColumn] |= VISITED;
							}
                        

Our backtracks() function needs to assess our current position in relation to the previous square we visited and then move towards that previous square. The code for that function is shown below.


                        void backTracks(){
							  char prevRow = backTrack[backTrackPointer][0];
							  char prevColumn = backTrack[backTrackPointer][1];
							  if(prevRow == currentRow){
							    if(prevColumn == currentColumn - 1){
							      moveWest();
							    }
							    else{
							      moveEast();
							    }
							  }
							    else if(prevColumn == currentColumn){
							      if(prevRow == currentRow - 1){
							        moveNorth();
							      }
							      else{
							        moveSouth();
							      } 
							  }
							  backTrackPointer -=1;
							}
                        

We continuously check for walls and move throughout the maze until backTrackPointer=0. This signifies that we have made every possible move and then backtracked to our initial position in the maze, letting us know we have finished traversing the maze. Once this occurs, we turn on an LED to signify our robot is done mapping the maze.

Current Issues

Unfortunately, our algorithm has not yet run successfully. After trying to debug the algorithm for a while, we realized that the main issue we were facing was invalid readings from our wall sensors. We had checked the readings multiple times, but only by printing out one of the values at a time. We then realized that when reading the values of all 3 wall sensors successively, the values became incorrect. The sensors routinely output values corresponding to walls even if no walls were present. This helped us identify that the main issue was with the mux switching quickly between input signals. We attempted to add delays before and after reading a wall sensor value to see if the values would then correct. Still, the values were unreliable. We were unable to finish debugging this problem in lab, but will focus on making sure our mux works reliably in the future. Our plan is to use the oscilloscope to further assess what is happening when we switch between signals in the mux.

We are confident that our current algorithm will function properly as soon as we fix the wall sensor data. The alogorithm is very similar to that in our simulation, and we expect it to function just as well. In the future we would like to optimize our algorithm by prioritizing the nodes to backtrack to and moving to them in a more efficient manner.