Making a Rocket Controller Using a Sonar Sensor
by Doedeledaddel in Circuits > Arduino
1192 Views, 3 Favorites, 0 Comments
Making a Rocket Controller Using a Sonar Sensor
Hello there! I decided to make a simple controller bearing a button, three LED's, and is capable of detecting movement respective to the surface it's above.
The idea is that you could use this type of controller in a game where the upward movement of your controller is replicated in the game you're playing. I made a little demo in Unity to showcase this, in which you need to rapidly press the button to charge your fuel, and move the controller upward to propell yourself using the fuel you built up.
I had little soldering experience when starting this project, but I've learned quite a bit of technique from soldering without supervision for the first time. Beforehand I had not the slightest clue on how to merge wires with each other, but figuring out how to do so turned out to be a pretty fun learning experience.
Creating the hull also gave me a little bit of experience in the field of woodworking. Having to saw and drill.
From a techincal standpoint I thought it was interesting to figure out how communication between an external device, and something as familiar as a computer would work. Luckily it turned out to be rather simple!
I'm assuming you already have a basic understanding of programming and soldering. But just in case, I'm clarifying some things here and there. So let's get into it.
Supplies
Circuit Supplies:
- Arduino Uno
- USB-A USB-B cable
- Button (16mm in diameter)
- Red LED (5mm in diameter)
- Yellow LED (5mm in diameter)
- Green LED (5mm in diameter)
- Ultrasonic sensor
- 220Ω Resistors 2x
- 10kΩ Resistor 1x
- Red jumper wire 5x
- Black jumper wire 5x
- Green jumper wire 2x
- Orange jumper wire 1x
- Breadboard (Optional)
Hull Supplies:
- PVC pipe (60mm in diameter)
- Wooden plank (Preferably 53mm in diameter. Can be sawed)
- Nails
- Screws
- Piece of thin cardboard
- Aluminium foil
Tools:
- Laptop or PC
- Soldering Iron
- Wire Stripper
- Wire Cutter
- Glue
- Duct tape
- Hacksaw
- Saw (Optional)
- Wood File (Optional)
Software:
- Arduino IDE
- Visual Studio
- Unity Editor
Designing the Circuit
Stripped down to it's core, the controller has two important components. The sonar sensor, and the button. But for the sake of flair we're adding three LED's to indicate how well you're pressing the button.
Sonar's work by emitting a high-frequency sound, and listening to it afterward. The time it takes to hear the sound indicates how much distance the sound has traveled. So for wiring, we'll have to supply it with voltage, send a singal through the 'trigger' pin, read the signal through the 'echo' pin, and connect it to ground.
As for the button, we simply connect it to a voltage output. Connect it to a an input pin, with a pull-down resistor to stabilize the reading.
Writing the Code
Sending information from the arduino to your computer can be done using the serial port. In our case, we want to send a message as soon as the button is pressed down. And, we also want to send a message whenever the controller is moving upward from the surface.
To achieve this, I implemented an object oriënted approach. Both the button, and the sonar have their own classes. And, both of them have a function that works similairly. That is, calculate the information we need, and send it to the serial port. We'll call it 'measure()'.
First, let's define the header files of the classes. In C++ header files contain the defenition of a class. Which variables it has, which functions, etc. See Button.h, and Sonar.h, to see how they are written.
Then, we will implement the functionality of the functions within the source files. See Button.cpp, and Sonar.cpp.
bool Button::measure()
{
// Evaluate how much time since the last press.
clicksPerSecond = 1 / ((millis() - timePressed) / 1000);
// Calculate performance based on the expected clicks per second we configured.
int performance = 0;
if (clicksPerSecond > expectedCPS / 2) performance = 1;
if (clicksPerSecond > expectedCPS) performance = 2;
// Light the LED's according to the calculated performance.
if (performance >= 1) digitalWrite(orange, HIGH);
else digitalWrite(orange, LOW);
if (performance >= 2) digitalWrite(green, HIGH);
else digitalWrite(green, LOW);
// Return if the button is not pressed down.
if (digitalRead(in) == LOW)
{
alreadyPressed = false;
return false;
}
// Return if the button was already pressed the previous iteration.
if (alreadyPressed) return false;
// Print info. Only use for debugging, as this will clutter the serial port!
/*
Serial.print("It has been " + String((millis() - timePressed) / 1000) + " seconds since last press.");
Serial.print("\t");
Serial.print("Clicks per second is: " + String(clicksPerSecond) + ".");
Serial.print("\t");
Serial.print("Performance: " + String(performance) + ".");
Serial.println();
*/
// Send information to the serial port.
Serial.println("pressed!:");
// Save variables for next iteration.
clicks++;
timePressed = millis();
alreadyPressed = true;
// Return true.
return true;
}
As you can see, in both cases we wanna limit how much information gets sent through the port, and only send it once we want something to happen in our game. The button only sends information whenever it is pressed down, and was not pressed in the previous iteration.
bool Sonar::measure()
{
unsigned long timeStart = micros(); // Record time at start of measurement.
// Fire sound.
digitalWrite(trig, HIGH);
delayMicroseconds(10);
digitalWrite(trig, LOW);
// Retrieve sound travel time, and calculate distance.
unsigned long duration = pulseIn(echo, HIGH);
float distance = (speedOfSound * duration) / 2; // (Distance needs to be halved, since the sound needs to travel to, and back from the object.
// If no previous distance measurement is saved, we can't calculate the velocity.
if (!started)
{
started = true;
previousDistance = distance;
return;
}
// Calculate velocity.
float timePassed = (micros() - timeStart) / 1000; // Calculate time passed in ms.
float distanceTraveled = distance - previousDistance; // Calculate distance traveled in cm.
float velocity = distanceTraveled / (timePassed / 1000); // Calculate velocity in cm/ms.
// Print info. Only use for debugging, as this will clutter the serial port!
/*
Serial.print(" Distance is " + String(distance) + ".\t Traveled " + String(distanceTraveled) + " cm," + "\t" + "after " + String(timePassed) + " milliseconds.");
Serial.print("\t");
Serial.print("Estimated velocity is at: " + String(velocity) + " cm/s.");
Serial.println();
*/
// Cache values for next iteration.
previousDistance = distance;
// If the velocity is within the margin of error, don't do anything.
if (velocity < errorMargin) return false;
// Otherwise, send the information through the serial port.
Serial.println("velocity:" + String(velocity));
// Return true.
return true;
}
The sonar only sends a message when upward velocity is detected above our defined margin of error.
Finally, the two classes are used by the main .ino file. You might be wondering why both measure() functions return a boolean. That's because I chose to limit the amount of information sent through the serial port. If the button is pressed. We wanna save the space in the serial port for that message, since in my opinion, the button takes higher priority.
Once you've implemented these scripts, then that should be all you have to do in the Arduino IDE!
Testing the Circuit
Before I moved on to permanent solutions such as soldering, I wanted to test the circuit with some prototype code. Just to see if the code would work the way I want to
To simulate a pushbutton, I just had two jumper wires touch. This way I could control whether electricity would flow to the rest of the circuit. I made sure I had the pull-down resistor working, and that all of the components worked in general.
Soldering the First LED
Now let's move on to soldering the first components. As you saw in the illustration, I want my LED's to be attached to different output pins, but have all their ground wire merged into one.
I soldered a 220Ω resistor directly to the anode of the LED, and proceeded to solder a red wire to the resistor as well.
Then I soldered a black wire to the cathode of the LED.
After that, I wanted to insulate the two exposed wires from each other to prevent a short-circuit. I used electrical tape, which does the job for small amounts of electricity like this.
In the end, you should have a working LED which you can plug into your Arduino!
Soldering the LED Cluster
If you repeat the last step for every LED, you're left with three individual LED's. To merge the black wires into one, we're gonna have to strip them, and solder them together.
This can be done by twisting the ends of the exposed wire together, and soldering them in that state.
Since there won't be a risk of short-circuit, we can wrap the convergence in electrical tape as well, and we have an LED cluster!
Soldering the Button
Like the LED's, I want the button's wire to have a pull-down wire built-in.
I stripped the green wire and left quite a bit of copper exposed. I then attached a 10kΩ resistor to it. This resistor was also attached to a black jumper wire, which we can stick into the ground pin.
The other path, which was a cluster of copper strands, was soldered to another green jumper wire, to stick into an input pin.
After isolating the delicate composition with electrical tape, it should look like a T-split.
Since the button has pins with holes in them, I decided to make use of the jumper wire's solid pins, and do a through-hole solder on them. The button I'm using has a DO (Default Open), DC (Default Closed), and Common configuration. So I soldered the red input wire to the DO pin, which only makes connection to the Common pin when the button is pushed. And finally, the common pin was basically it's cathode, so I soldered the green output wire to it.
Soldering the Sonar
This was a pretty straightforward solder. Much like the illustration of the circuit, I attached the corresponding wires to the pins by stripping them, wrapping them around the pin, and soldering it.
Like the LED's, I isolated the pins from each other by recursively wrapping electrical tape around them.
After that, you should have a soldered sonar!
Preparing the Hull
For the hull of the circuit, I decided to use a 60mm PVC pipe.
Using a hooksaw, I trimmed the pipe to about 3x it's diameter. About 180mm long.
Next, I drilled holes corresponding to the diameters of the LED's (5mm), and the button (15mm). I planned for those components to stick through these holes from the inside.
When that was done, I decided on the semi-permanent approach of cutting the pipe in half, since placing the wires in the Arduino's pin, while having them already mounted in the pipe, could not be done with my hands, since they were simply too big. And yours most likely are too.
Rather than trying to stick my hands in there, the wires could be connected with this open configuration, and then shut later.
Securing the Arduino Board
I had to find a way to secure the Arduino board inside the pipe, so I decided to use a wooden plank, on which the arduino board could be screwed. Then the wood could slide inside of the pipe, and be secured.
As you can see, I marked the width of the Arduino board on the plank, and trimmed it to that size.
Then, I marked the curve of the pipe to the side, and used a wooden file to have the plank match the pipe's shape.
Your Arduino board should have four holes on each corner. These can be used to screw the board onto a solid object, such as wood! And such I did.
Attaching the Sonar to the Wood
Like the arduino board, the sonar has four little holes as well. Since our screws were too large to fit through them, I decided to use nails instead, and carefully hammered the nails into the wood.
Make sure your jumper wires are long enough to reach their pins, and that the sonar doesn't obscure the USB port! I had to angle my Arduino board a bit, but that made it fit in the end.
Securing the Wood to the Pipe
After laying the wooden piece inside of the pipe, I drilled holes in the side for screws to secured in. This made sure that the wood was rigidly attached to the outer hull.
Mounting the Accessoires
Coming back to those holes we drilled. I was pretty lucky to have a button that can tighten itself against a surface, but if you don't have one like that, consider using the same approach as I took for the LED's. That was glueing them against the hole. As you can see. The main interface of the controller is starting to take shape at this point.
Reassembling the Hull
Now that I had all my components in place, I stuck all the wires in their corresponding pins, and glued the hull back together. To reinforce the connection, I used duct tape on both sides of the pipe. This was a relatively tricky part, since the glue wasn't strong enough to prevent total obliteration when squeezing the hull too hard.
Decorating the Hull
For our last practical step, I decided to close off both ends of the hull with some decoration. Using a piece of carboard, a cone shape could be made. After wrapping some aluminium foil around it, it was attached to the top. This can be done using either glue, or using adhesive tape.
The bottom of the hull is made by attaching aluminium foil around the circumference of the pipe, and scrunching it so that the bottom is sealed, and the necessary components still fit through.
Communicating With Unity
To communicate with Unity, I made a dedicated class to retrieving, and storing information sent through the serial port.
The class makes use of the System.Threading namespace, but more remarkable the System.IO.Ports namespace, which requires a bit of tinkering to make it work.
The System.IO.Ports namespace is not recgonized unless you install it's package in Visual Studio's built in package manager, or install it alternatively here: https://www.nuget.org/packages/System.IO.Ports/8.0.0-preview.7.23375.6.
On top of that, make sure that your Api Compatibility Level is set to .NET Framework, inside the Project Settings!
using System.IO.Ports;
using System.Threading;
using UnityEngine;
public class ArduinoReader : MonoBehaviour
{
private Thread m_thread = new Thread(ThreadLoop);
private static SerialPort m_sp = null;
private static string m_message = "";
private static void ThreadLoop()
{
// Create a new serial port, and open it.
// Make sure the com port, and serial bitrate are the same as in your Arduino IDE!!
m_sp = new SerialPort("COM4", 9600);
m_sp.Open();
// During the loop..
while (true)
{
m_message = m_sp.ReadLine(); // Store the incoming message in a static field.
Thread.Sleep(30); // Sleep for the same amount as the arduino's delay.
}
}
private void OnDestroy()
{
// Abort the thread, and close the serial port if the object is destroyed / the game is closed.
m_thread.Abort();
m_sp.Close();
}
private void Start()
{
// Start the thread at the start of the game.
m_thread.Start();
}
/// <returns>The received message stored in the arduino reader class.</returns>
public static string GetMessage(bool clear = false)
{
var message = m_message;
if (clear) m_message = "";
return message;
}
}
The attached script is what I'm using to read incoming messages through the serial port. I've set the Serial port's bitrate to 9600, and the com port on 4. Set these to whatever you've configured in the Arduino IDE. As you can see, I called Serial.Begin on bitrate 9600, and on the bottom-right of your Arduino IDE you can see which com port you're using when the arduino is connected to your computer.
Attach the script to an object in your scene, and you should be able to use it!
Rejoice!
If you've got that set up in your Unity project, you're good to go. You can call ReadMessage() to read the message in the latest line of the serial port, and pass in 'true' as a boolean to clear the message inside the script.
using System.Globalization;
using UnityEngine;
using UnityEngine.Events;
public class Player : MonoBehaviour
{
[Header("Settings:")]
[SerializeField] private float m_maxFuel;
[SerializeField] private float m_fuelAddition;
[SerializeField] private float m_fuelLeak;
[Space]
[SerializeField] private float m_rotationTime;
[Header("Events:")]
[SerializeField] private UnityEvent<float, float> m_onFuelChange;
// Properties:
private float m_fuel = 0f;
private float m_rotationVelocity = 0f;
// Reference:
private Rigidbody2D m_rb = null;
private ParticleSystem m_particles = null;
public State state { get; private set; }
public float fuel
{
get => m_fuel;
set
{
m_fuel = value;
m_onFuelChange.Invoke(value, m_maxFuel);
}
}
public float velocity { get; private set; }
private void Awake()
{
m_rb = GetComponent<Rigidbody2D>();
m_particles = GetComponentInChildren<ParticleSystem>();
}
private void Start()
{
state = State.Idle;
fuel = 0f;
// Disable particles at the start.
m_particles.enableEmission = false;
}
private void Update()
{
// Cache the incoming message, while clearing it from the arduino reader.
var incomingMessage = ArduinoReader.GetMessage(true);
// Debug purposes: Log the incoming message.
//if (!incomingMessage.Equals("")) Debug.Log(incomingMessage);
switch (state)
{
// In the idle state..
case State.Idle:
// Deplete fuel by default.
if ( fuel > 0)
{
fuel -= m_fuelLeak * Time.deltaTime;
if (fuel < 0) fuel = 0;
}
// If the button is pressed, add fuel.
if (incomingMessage.Equals("pressed!:"))
{
fuel += m_fuelAddition;
if (fuel > m_maxFuel) fuel = m_maxFuel;
break;
}
// If player has fuel, and upward velocity detected, switch to other state.
if (fuel > 0 && incomingMessage.Contains("velocity:"))
{
var velocityString = incomingMessage.Remove(0, 9); // Remove "velocity" from the string.
velocity = float.Parse(velocityString, CultureInfo.InvariantCulture); // Parse the remaining numbers to a float.
velocity /= 100; // Also divide by 100 to convert it to m/s instead of cm/s.
state = State.Propelling;
m_particles.enableEmission = true; // Enable particle emission.
}
break;
// In the propelling state..
case State.Propelling:
m_rb.velocity = transform.up * velocity; // Apply upward velocity to rigidbody.
fuel -= Time.deltaTime; // Rapidly deplete fuel.
m_rb.angularVelocity = Mathf.SmoothDamp // Apply torque so the cube rotates a bit.
(
m_rb.angularVelocity,
Random.Range(-90f, 90f),
ref m_rotationVelocity,
m_rotationTime,
Mathf.Infinity,
Time.fixedDeltaTime
);
// If fuel is depleted switch back to idle.
if (fuel <= 0)
{
fuel = 0; // Clamp fuel to zero.
state = State.Idle; // Switch state.
m_particles.enableEmission = false; // Disable particles.
}
break;
}
}
public enum State
{
Idle,
Propelling
}
}
See the attached script to peek at how I implemented the ArduinoReader in the player class of the little demo I made. But all in all, have fun creating your own little games with this tech!