- Vivian Chou (MAE)
- Nozomu Harada (MAE)
- Daniel Walder (ECE)
The goal of our project is to, given an obstacle map obtained from a lidar, calculate and execute a path for the car to take whilst avoiding the obstacles. This requires robust and easily configurable hardware as well as modular and testable software. Our solution is tested and integrated into the class's current (December 2021) ROS2 robocar repository.
The base plate holds the Jetson Nano, electrical components, camera, and lidar in place on the car as shown in Figure _. The holes are designed for M3 fasteners and nuts to hold the base plate mount. The slots are designed to mount the Jetson Nano on the top and the 3D printed Electronic Component Housing on the bottom of the base plate. The slot design allows for adjustments when mounting the Jetson Nano and the Electronic Component Housing, as well as creating space for the wiring of the electronics from the bottom of the base plate into the Jetson Nano located on top of the base plate.
Camera & Lidar Mount
The Camera & Lidar Mount is a combination of a camera support (Figure XX.a), Lidar mount (Figure XX.b), and a tower (Figure XX.c). The camera mount fits to the tip of the camera support with a single M5 bolt, and on the other side of the camera support, there are two M5 slits that are meant to be bolted to the tower with three degrees of freedom (Up/Down, Forward/Backward, and Rotation) as shown in Figure XX, providing the user with a wide range of possible camera position to adapt to various situations. The lidar mount was also designed to be attached to the tower with some adjustability. Unlike the camera support though, it cannot be adjusted Up/Down and Forward/Backward. The mount is bolted to the tower with two long M2 bolts. One of them being the pivot point where the whole Lidar mount can be rotated about, giving approx. 10 degree of adjustability. The other bolt is simply there to fix the mount in position. The tower was designed with a primary purpose of keeping the entire car safe and protected besides that it holds the camera and the Lidar in place. This explains the heavy-duty design of the tower. In case of a crash where the car lands on the ground upside down, the tower acts as a roll bar and takes all the stress before hitting any other part of the car. Its secondary purpose is to ease the work on the car. As the base plate is flipped about the pivot of the rear base plate mount, the tip of the tower touches the ground and holds the plate in place without damaging Jetson Nano. The tower is attached to the base plate with five M3 bolts on each side. In case the adjustability of the camera mount and the Lidar mount themselves are not enough, the tower can be mounted either slightly forward or backward using less than five bolts on each side without compromising its structural integrity. It is, however, advised to use a minimum of three M3 bolts on each side to avoid any stress concentration that could potentially crack the acrylic base plate in case of a crash.
The camera mount was designed to hold the camera with four M2 bolts. The design includes a small rectangular opening where the connector of the camera sits in. The M5 hole on the backside is used to bolt itself to the camera support.
Rear Base Plate Mount
The rear base plate mount was designed to give significantly improved rigidity to the base plate over stand-off screws while achieving its functionality as a hinge such that the base plate can flip about its axis and opens up an access to both the chassis of the car and the backside of the base plate where the electric component housing is mounted. The mount consists of two major components. One (in red, shown below) is attached to the chassis of the car with two M3 bolts. Another (two swingarms in gray) is attached to the base plate with six M3 bolts on each side. It is not necessary to use all six bolts in terms of its structural integrity, so the base plate can be mounted either slightly forward or backward as desired. Two of these components are then connected by two long M5 bolts. They act as a shaft of the "hinge" with great rigidity with no play whatsoever.
Front Base Plate Mount
The front base plate mount consists of three sub parts. Two of them, as shown in FigureXX.a and FigureXX.b, were designed to withstand a massive force in the event of a crash. The part shown in FigureXX.a is bolted to the base plate using 6 M3 bolts, and the part shown in FigureXX.b is bolted to the chassis of the car using CC M3 bolts. These two parts are then bolted together using one long M5 bolt as shown in FigureXX.c. The primary feature of this 'one bolt' design was to make it easier to work on the car, as explained also in the description of the rear base plate mount. Previously when the base plate was mounted with four towers of stand-off screws, not only was it fragile and unstable, it was also very time consuming to remove and reattach the plate. With our design, it is only a single bolt that takes to create a full access to any portion of the car. To further maximize the ease of access, the part shown in FigureXX.d was introduced. As shown in FigureXX.e, this part is attached to the part shown in Figure XX.b using two M3 bolts and holds an M5 nut inside in place so it won't rotate. Therefore, when screwing or unscrewing the long M5 bolt, the user does not have to hold the nut on the other end. Most importantly, the front base mount was purposely designed and located right under the camera&Lidar mount such that the acrylic base plate experiences no exceeding amount of shear stress in the event of a crash that involves flipping and rolling of the car.
Electronic Component Housing
A 3D printed housing was made to fit the electronic components (PWM board, DC-DC converter, Wireless Remote Control Switch Receiver, and red and blue LED lights) in one location as shown in FigureXX. The housing allows for easy removal in situations when the base plate would break during car crashes.
Jetson Nano Case
The Jetson Nano Case created by ecoiras was modified. For the bottom and top of the Jetson Nano Case, 4 clearance hole mounts were added for M3 fasteners and nuts, which allowed the Jetson Nano to mount onto the base plate with a protective cover. Additionally, the top of the Jetson Nano Case increased in thickness for strength. Other modifications to the top of the Jetson Nano Case include adding a protective cover for the Jetson Nano fan and removing a few pieces of side slits for ease of wiring. The modified Jetson Nano Case assembly is shown below.
- Lipo 3-Cell Battery: Powers the wiring system through a 12V battery
- Battery Monitor: Monitors the 3-Cell Battery to prevent situations such as a completely drained out battery
- EMO Receiver: Receives a signal from a remote control that activates whether the car can or cannot start driving with the controller
- LED: Indicates whether the car can or cannot drive through blue and red lights
- Jetson Nano: Controls the car
- Controller: Allows the user to control how the car moves
- Camera: Allows the car to train through images created by the camera and move autonomously by locating the boundaries of the environment
- Lidar: Senses location
- DC-DC Converter: Converts 12V to 5V
- PWM Controller: Control ESC and Servo Motor
- Servo Motor: Steers the wheel
- ESC: Controls the motor
- Drive Motor: Allows the car to accelerate and decelerate
Calculating a path through a given environment
Pathfinding is a well known problem in today’s world, and there are many algorithms to choose from. The ones we were discussing were:
Simple but powerful algorithms that take the approach of heading towards the goal until an obstacle appears. Once the obstacle is reached, circumvent it until the vehicle is back on track to the goal.
Especially known in the Computer Science community, these algorithms spread from the “start node”. Each time a new node is encountered, the distance to the “end node” is calculated and the node with the lowest distance is selected to be the next center for discovering new nodes. A great advantage to these algorithms opposed to the Bug algorithms is that they take into account “weighted” paths, meaning that variable environments such as uphill or downhill terrain can be considered when calculating the most efficient path. While these algorithms are always correct (meaning they always produce the most efficient path), they can be computationally expensive.
RRT* is a response to the expensiveness of the Dijsktra/A* algorithms. Instead of exploring nodes one by one, it takes a more heuristic approach by selecting random spots on the map over and over again. Over time, this results in a nearly optimal path, but with potentially way less necessary computations.
After weighing the pros and cons of each algorithm, we’ve decided to go with Dijkstra/A*. This is because the obstacle maps we are working with aren’t overly big, so the computational cost is negligible. They give a more efficient path than the Bug algorithms, and are more reliant than the RRT* algorithm. Furthermore, there is already a great deal of implementations out there using Dijkstra/A*, meaning that we can take advantage of an already implemented and tested implementation.
Since we are developing our own package, it is important to use lightweight libraries whenever possible. This is why we chose the “pathfinding” library, implemented in Python, to include in our package. Given an obstacle map, where “1” means a traversable node, and “0” means an obstacle, we can calculate the most efficient path from a given start node to a given end node. An example obstacle map therefore looks as follows:
And, printed in terminal, wherein “#” represents an obstacle, “X” represents the path, “s” represents Start and “e” represents Finish , the resulting path looks as follows:
While the above image is nice to visualize the path, what is important for the program is the sequence of the given nodes, which for this example is as follows:
There it is, this is the path our car has to take in order to reach its destination whilst avoiding the obstacles. The only problem is, the car inherently does not understand what to do with just coordinates. They need to be translated into actual car commands, i.e. steering and throttle.
Translating the calculated path into car commands
To translate a sequence of nodes into actual car commands, we have to consider a couple of things:
- The physical distance between nodes (in meters)
- The speed of the car (in meters per second)
- The turning radius of the car (in meters)
- The current orientation of the car (simplified to North, East, South, West)
The algorithm we developed therefore goes as follows:
- Assume the car is already facing the direction of the path
- Look at the next and the next-next node in the path. If they are not both 1 unit away from the car, drive straight.
- If they are both 1 unit away from the car, that must mean we are at a corner. Turn in the appropriate direction (using the space of the cornerstone of the path for turning, and adjusting turning based on the physical distance between nodes and the turning radius) and then update the current orientation of the car.
- Repeat steps 2-3 until the goal is reached, then do a short reverse throttle to bring the car to a stop
The above steps are a simplified version of the actual algorithm we developed. The actual implementation required multiple data structures and consideration of edge cases. If interested in the actual implementation, check out our gitlab repository! Now, instead of just a list of nodes, we end up with a list of actual car commands, looking like this:
However, ROS2 does not understand our custom CarCommand data class. Time to add another translation layer.
Executing the path commands with ROS2
Effectively, we only require 2 nodes for our pathfinding algorithm to be applied:
Subscribes to obstacle_map messages. Whenever a new obstacle map is received, it runs the algorithm, translates the path into car commands and publishes twist messages.
Subscribes to twist messages. Whenever a new twist message is received, it forwards the contents to the servo for steering and the ESC for throttle.
Therefore, once all the car commands are calculated, the path_finding_node translates them into twist messages. For steering this is straightforward: Translate SteeringDirection.RIGHT into a 1.0 for angular.z, SteeringDirection.LEFT into a -1.0 for angular.z and SteeringDirection.STRAIGHT into a 0.0 for angular.z. However, for throttle we have to translate m/s into the range [-1,1], wherein negative -1 describes full backwards throttle and 1 describes full forward throttle. Therefore, calibration of the car across multiple charge states of the battery is required to determine the max and min speeds of the vehicle. Use the obtained limits to calculate the needed relative speed of the car and write it to the linear.x component of the twist message. Once the twist messages are compiled from the CarCommand list, we can publish them one at a time using the duration component of the CarCommand class.
The adafruit_twist_node then receives the twist messages one at a time, and translates the relative values into values specific to the ESC of the car (the ESC we are using works with relative values, but the range does not have to be [-1,1], but can be shifted, factored and/or inverted). Using the pwm board, they are then applied to the actual servo and motor of the car, therefore executing immediately.
To start up our package, we designed and implemented a launch dependency tree. Therefore, the user can conveniently start up only the path_finding_launch.launch.py file and everything is ready to go.
The algorithm executes and runs on the car as it is told. The car starts driving and depending on the calculated path occasionally turns left or right. However, the turns are not exactly 90 degrees as expected, and neither does the car drive the expected distances. In other words, while on paper the math checks out and the car should exactly follow the calculated path, in reality, this is not the case. This is because of our limited ability to correctly calibrate the car. Since there are infinitely many states the battery can be in, and the throttle of the car is relative to said battery charge, we can not accurately execute our commands. This can be resolved by using a different ESC, which does not depend on the battery state and instead drives the same speeds no matter the provided charge. Hence, although we did not achieve the correct results on our physical car, we conclude that our path finding solution and execution holds correct if improved hardware is provided.
As discussed above, the most beneficial improvement would be an upgrade of our ESC to allow for accurate execution of our path finding solution. However, there are other improvements that can be made to make our pathfinding package more robust.
- Get updates about current location. As of now, we are calculating and executing our path based solely on simple physics equations. Unfortunately, the real world is a little more complicated than that. Acceleration, slipping of tires, small bumps on the ground can all contribute to falling astray from the calculated path. This is why getting frequent updates about the current position of the car is critical. Our path_finding_node receives these updates and, based on them, recalculates the path and the car commands to be published.
- Allow for more than 4 directions. Our current solution does only allow for 4 possible directions of movement: North, East, South and West. This means that whenever something happens that puts the car in a direction different from those 4, subsequent commands, even if recalculated to reflect the updated position, will fail to put the car back into one of those 4 directions. One solution can be to implement a function that, upon receival of a direction error, inserts a custom car command to redirect the car into one of the 4 directions. Another, probably cleaner solution might be to simply increment the number of allowed directions to begin with, i.e. North-East or North-East-North or even 360 directions, one for each degree.
Advice for Future Teams
- Working extra on the wiring of the whole electronics at the beginning of the quarter can save a lot of time towards the end of the class. When working on the car, having to rewire everything can easily take 10 minutes or so. Designing something similar to what we did for our car is highly recommended.
- When designing a part to be 3d printed, design them to be robust. Having to reprint and reinstall any parts in case of an accident can be very time consuming.
- Make your codebase modular and test each part individually. If we wrote the whole package in one go without implementing and checking the console outputs at each step, we would have had a way harder time to isolate the issue of wrong car movement and attribute it to the ESC. But with regular testing, we were able to guarantee the correctness of our path finding algorithm and translation into car commands.
- Jack Silberman (Professor)
- Dominic Nightingale (TA)
- Haoru Xue (TA)