Blast Off - Wireless Water Rocket Launcher Part 2

Daniel Koch and Liam Davies

Issue 56, March 2022

This article includes additional downloadable resources.
Please log in to access.

Log in

In part 2 of the project, we share and detail the code, and some discoveries we made along the way. By Daniel Koch & Liam Davies.


In last month’s water bottle rocket launcher project, we ran out of space to give enough detail about the code to be fair. Along the way, however, we found a few bugs and hiccups with the hardware, so we’ll share those as well. Our apologies to anyone who has already built the launcher, because there are a couple of small changes. They are fairly easy, however.


While the main build of the launcher was taking place, the coding was being done elsewhere. While DIYODE staff are somewhat concentrated in the office, some writers and all of our contributors work remotely from all over the country and the world. Because of this, we decided to build a test bed for the coding to take place in a home workshop, replicating the launcher and console on the electronic level while the physical build took place in the DIYODE office workshop. That meant a toggle switch in place of the pin-insert microswitch for power on the console, plus two small pushbuttons in place of the big launch pushbutton and momentary toggle arm switch.

On the launcher, all of the electrical interlock switches are unnecessary, as they operate independently of the code. All that was needed was some LEDs to represent the different outputs, a small servo to represent the big one, and a single PIR to take the place of the OR-connected group of four on the actual launcher. Both test beds were made with foam-core and hot-melt glue. It was on these test beds that we first began having hiccups with the random behaviour of the code.


Unfortunately, while we collaborated on the circuit at the theoretical stage, the physical build proceeded after we were happy with the way all of the inputs and outputs would be powered and driven, concurrent with the code development and test. This situation is not a thing most makers encounter at home, but anyone working in a team has probably experienced this at some point. We had engineered around a few problems that we foresaw, including the low output current of the ESP8266 I/O (some modern LEDs draw 50mA to 70mA, including the high-brightness deep red we wanted to use) being compensated for by driving the LEDs on the console with transistors. However, we were comfortable enough with our design based on what we knew then, for work to proceed simultaneously.

While the circuit initially worked the way we wanted it to, a couple of niggling problems came and went. They were erratic misbehaviours and the inconsistency made the cause hard to pin down. We started reading into the datasheets of the ESP8266 itself, rather than the lacklustre documents that came with the module which we had relied upon so far, and we discovered that while the module is fed via 5V, the I/O for the ESP8266 are all 3.3V. Careful inspection of the module revealed no level shifting onboard. It seems the 5V is only there for the USB operation, and the module manufacturer has simply added a small regulator to power the ESP8266 and not bothered with level shifting or to even flag this in the product documents.

We have used the ESP8266 on these same modules with 5V inputs before, without knowing this fact. Our multi-node doorbell project from Issue 48 is still running daily in the office with 5V fed via the pushbutton to the I/O. While we now know that long term damage may result, the doorbell doesn’t seem to care at all. We have still changed the console design with the use of Zener diodes, just to be safe. Only the inputs have changed, so the schematic shows only the section with the ESP8266, not the DC-DC converter, power switch, and limit switch. Use this drawing in conjunction with the bigger one from part 1. The diagram below just shows the additions. Thankfully, only additions were needed. The 330Ω resistor and 1N4728 3.3V Zener for each input were able to fit with no modification of the existing layout.

However, this didn’t seem to be
the cause of our problems. Deeper into the datasheets, we discovered that certain pins on the ESP8266 have to be either high or low during boot. If anything changes this state at the wrong time, the system can hang. In our case, the PIRs attached to input D8 were sometimes triggering as the processor was booting while we walked away. That presents a problem, as the whole point is to finally power the system, wait for a connection light, then walk away. If you move at all the system hangs. In normal operation, the user will be walking away at precisely this time.

Thankfully, the PIRs already have a 3.2V output despite being powered by 5V. Because there are no digital inputs on the launcher besides the PIRs, only outputs, nothing needed to be done to correct voltage. The module can still be powered by 5V, and the servo is powered by its own 7.4V supply, with a 3.3V signal still being adequate as a data signal. All that was needed was a wiring change to deal with pin D8 needing to stay low during boot. The answer was to desolder the header pins for D8 along with D0, which we discovered pulses during boot.

In the original circuit diagram from last month, D0 is connected to the fill solenoid. This was an error on the diagram, as were a couple of others. It resulted from having our awesome graphics team digitise the schematic from the hand-drawn version, before the build took place so they can work while we build. During the build, changes were made to better suit the physical layout. These changes were made before we built our board, so all the photos from part 1 are correct (except the solenoid ground issue discussed ahead). Unfortunately, the original digital drawing got linked to the file sent to the printer, not the updated one with the changes. Now, D7 and D6 are the relay control outputs, while D1 and D2 drive the stack lights. The solution for the use of D8 as an input was to solder a link across the board and utilise D3 with no need to change anything else on the circuit board.


With the changes made on the test beds as well, we had verified the code and loaded it onto two fresh ESP8266 modules. They were plugged into the boards on the launcher and in the console, and a test was begun. No air or rocket were used for the test, so we were listening for the operation of the solenoid valves and watching the servo. The solenoids made a very loud clunk, so they would be easily audible. All went well. An empty bottle served to make the collar and its limit switch work. The launcher missile switch was turned on, and the safety pin removed. The stack lights behaved accordingly. The safety pin was inserted into the launcher, and the appropriate light lit. Then we were greeted by the connection light on both console and launcher, indicating that the console (acting as client) had shaken hands with the launcher (acting as server).

The arm switch was held on, and the lights on the console did what we wanted them to do, as did the stack light. There was a sudden clunk as both solenoids activated: The vent solenoid, normally open, closed; and the fill solenoid, normally closed, opened. The launch button LED indicated it was ready, and the button was pressed. The servo moved! It did exactly what we wanted it to, pulling the launch collar down and then stopping. It did stop rather soon after the limit switch at the collar was opened, so that was a cause for pause. However, the solenoids were not turning off, which was a much bigger cause for pause. Then there was a cause for panic as the unmistakable smell of burning electronic components filled the room, and plumes of smoke emanated from under the launcher. The missile switch was slammed down before the launcher was tilted to the side and the battery disconnected. What had gone wrong?

The simple answer is: Clumsiness. At some point, I had absent-mindedly soldered the ground from each solenoid to the ground for the coil of each relay, which is sent to the BC337 transistors controlling each relay. The circuit diagram clearly shows them going straight to ground, and we had even made the power distribution circuit board with pins in the ground rail for this purpose. The solenoids draw around 1.5A each. The BC337s we have are rated at 500mA continuous for that particular brand and model (though some handle up to 800mA). My co-conspirators were most amused at my clumsiness, but thankfully it was an easy fix. It was, however, a lesson in how easily rushing, fatigue, or both, can affect someone.

With the transistors replaced, and the solenoid ground wires going where they should, the test was repeated successfully. Instead of adding capacitors to the 7.4V DC-DC converter output to run the servo for a moment after the limit switch is opened, to make sure the collar moves all the way off the cable ties, we decided to solve two problems at once. The original design has the limit switch hot-melt glued to a piece of electrical conduit at the right height. The idea is to change the conduit if using a different release/launch tube assembly screwed into the launcher base. However, instead, we have made a 3D-printed tube with a thumb-screw for tension and a flat spot to glue the limit switch, This way, it can slide up and down to fit different launcher tubes, and also be positioned lower on the collar so that the collar has cleared the cable ties before the limit switch is opened and power to the servo cut. We’ll describe how to assemble this at the end of the article.


The code for the launcher and console had to answer several criteria. With the wireless aspects of the ESP8266, one unit had to operate as a client, and the other the server. We decided to make the launcher the server and the console the client, for two main reasons. The first being is that the rocket launcher would likely stay on more often as compared to the console - this is beneficial as the launcher would have an easier time maintaining and broadcasting its WiFi hotspot. If on the console, the frequent power cycles would cause more connection issues.

The manner in which the launcher and console communicate is the second reason for this configuration. Since we’re using a simple HTTP server for our wireless communication, a typical transmission of data usually involves the client (the console) sending a GET request to the launcher. This communication can, of course, go both ways in that the server can send back a message, exactly the same way as your web browser fetches a message. However, with the exception of something like Websockets, the server doesn’t ever send packet data to the client uninvited.

In any case, the server has to monitor for new client connections, and handle requests from them. In this case the only client was going to be the console.

The console had to give a visual indication via an LED when it had confirmed a connection with the server, which itself had to reflect this. After that, the console had to tell the launcher to ‘arm’, which involves sending the two relay outputs high, for long enough to fill the bottle with pressurised air. This had to be a timer as there was no feedback on pressure. The code for the client handles this because it’s very important that should an abort event occur, the launcher won’t be

able to set off the rocket. All procedural launch operations must be handled sequentially through requests from the console (acting as the client). In other words, connecting, arming, and launching must all be initiated from the console so that the launcher doesn’t have to time its own commands. After that, the console must let the user know, via an LED, that it is ready. The same LED is used to inform the user that it is, in fact, arming the system by flashing the same LED. The launcher needs to reflect this too, so that the user knows that the server is responding and the system is actually working. That was achieved with the stack light.

Finally, when the launch button is pressed, the console needs to tell the launcher to activate the servo, then send all the outputs low. The code itself is a little more complex, but all in all it’s a fairly straightforward system. We’ve tried and tested various wireless systems over the years at DIYODE, including Bluetooth (and its low-energy variants), LoRA but we always come back to WiFi thanks to its relative universality nowadays.


#include <ESP8266WiFi.h>
#include "ESPAsyncWebServer.h"
#include <Servo.h>
#define RED_STACK D1
#define PIR_PIN D3
#define SERVO_PULL_TIME 3000
#define POST_LAUNCH_WAIT 5000
byte state = 0;
bool requestedFire = false;
const char* ssid = "DIYODE Launcher";
const char* password = "123456789";
AsyncWebServer server(80);
Servo launchServo;

The first chunk of the launcher code is fairly straightforward, and if all you’re interested in doing is building the rocket launcher, this is all of the code you may need to modify. All of the pin assignments, launch delays (e.g. for deciding how long the servo should pull) can be changed right above. We would also pick a bit more of a complex WiFi password than ‘123456789’ while you’re at it! By the way, all of the code for this project can be found in the project files if it isn’t fully listed here.

void setup(){
digitalWrite(VENT_SOLENOID, LOW);
digitalWrite(FILL_SOLENOID, LOW);

Our setup code is mainly setting up all our pin modes, starting the Serial monitor and connecting the launch servo. It’s not super complex, but this is the place to modify how the launcher boots up if you’re looking for that.

IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
server.on("/arm", HTTP_GET, [](AsyncWebServerRequest *request){
if(state != 4) {
bool success = armRocket();
state = 2;
request->send_P(200, "text/plain", success ? "SUCCESS" : "FAILURE");

This is getting into some of the more complex side of WiFi programming. We have done a number of ESP8266 projects before, but in this case we’re using an Asynchronous web server instead. Unlike other variants of synchronous web servers, the processing for the server can be done at any time whilst your main code is running. This makes setting up functionality based on a request super simple, since it doesn’t need to interfere with your main ‘loop()’ function.

We found this hugely advantageous, but besides the slightly more complex code shown above, there is another significant hurdle to using asynchronous functions. We scratched our heads for a while trying to figure out why our ESP8266 kept crashing when a rocket ‘Arm’ message was received, and then realised that we can’t use the ‘delay()’ function in asynchronous code.

//Arms and pressurises the rocket.
bool armRocket() {
//MUST be in standby mode.
if(state != 1) {
return false;
digitalWrite(VENT_SOLENOID, HIGH);
digitalWrite(FILL_SOLENOID, HIGH);
return true;

Anyhow, here is the code for arming the rocket. General safety and web request management is handled much the same as how we’re using the arming code. Most of the code responsible for real functionality, when it comes down to it, is really just turning on and off outputs. It’s nothing crazy! Remember, our rocket launcher was built to be mechanically safe, so we aren’t relying on a tiny microcontroller to safely direct 120psi of pressure. Bottom line is, our code is much simpler as the circuit handles the majority of these safety methods from a mechanical standpoint.


Since the console handles the user interface side of things, we need to ensure users can’t accidentally set the rocket off with a buggy piece of code or functionality. Again, the power can be easily cut with the physical switches, so there isn’t a lot of need to shut everything down through code should a serious need arise.

#define ARM_BUTTON D8
#define CONN_LED D5
#define FIRE_LED D6
#define ARM_WAIT_DURATION 3000
const char* ssid = "DIYODE Launcher";
const char* password = "123456789";
const char* wakeAddress =
const char* armAddress = "";
const char* abortAddress =
const char* fireAddress = "";

Like with the launcher code, there are various parameters that can be customised right from the top of the launcher code file. If you’re using different pin assignments, or want to use a different arming duration, that can all be customised here! You’ll also notice the website names, which are referring to the default gateway on the launcher’s network - i.e. the receiving ESP8266 itself. The sub-directories are able to trigger various parts of the launcher depending on which one we request. You can even do it through a smartphone just by connecting to the launcher over WiFi and visiting the website - but that’s not nearly as cool as a physical missile switch and big red button, of course.

Much of our console code is a network of ‘if’ statements, and sending web commands if we have pressed the correct sequence. Here is a portion of this code.

void loop() {
//We're connected and waiting for arm signal.
if(digitalRead(ARM_BUTTON)) {
//If we just pressed the button,
//create a new timestamp and set a new state.
if(!lastArmState) {
armTimestamp = millis();
lastArmState = true;
//This checks if we've held it for long enough.
if(millis() > armTimestamp + ARM_WAIT_DURATION)
//We've successfully armed the rocket.
// Wait for further commands.
digitalWrite(FIRE_LED, HIGH);
state = 2;
} else {
lastArmState = false;

This code is responsible for ensuring that we hold the arm button constantly for the duration we set in the code parameters. It must also be held during the pre-launch state, otherwise the rocket will disarm.

One thing we chose to implement was ‘persistent’ requests. Since the transmission of a HTTP packet is not guaranteed, we need to retransmit messages if they did not make it through to the launcher.

//Continually keeps pinging launcher until it receives a response.
void persistentRequest(const char* dest) {
while(httpGETRequest(dest) != "SUCCESS");
String httpGETRequest(const char* serverName) {
WiFiClient client;
HTTPClient http;
http.begin(client, serverName);
int httpResponseCode = http.GET();
String payload = "";
if (httpResponseCode>0) {
Serial.print("HTTP Response code: ");
payload = http.getString();
else {
Serial.print("Error code: ");
return payload;

The bulk of our WiFi transmission code is here, which mainly revolves around running the GET request, ensuring that a successful response is pinged back and then returning the resulting payload. Since we’re only trying to receive a “SUCCESS” payload, the content of the payload isn’t important, only the fact that we actually received it. We also have our “persistentRequest” function which will continually send HTTP requests until it receives a “SUCCESS” payload.

During testing of the WiFi functionality, we found that the transmission was very reliable at close range, never needing to re-transmit. However, when used at longer distances, like a safe distance from the launcher, some packets are lost. This is where the persistent request functionality comes in handy, even though it may take a few seconds for it to trigger a function.

Once everything is uploaded, and doing some testing with our foam-board testing platform to ensure components will behave as we expect, we can now use it in the real rocket system!


So far, we had performed unpressurised tests with a plain empty bottle on the launch tube just to make the limit switch in the collar activate. Now, it was time to test the system for real. However, we decided to incrementally test the system at lower pressures in the carpark of the office before going to full pressure operation. This is because at 100psi, the rockets will go high enough to leave the property in even a slight breeze. Also, at medium pressures, they go high enough to end on building roofs, where they clog gutters and cause issues even if they are not a visual litter issue from the ground. This would verify that the launcher worked smoothly and that the cable tie release functioned as expected. We also conducted a pressure test with a blanking plug screwed onto the vent solenoid, before we did anything else, with a bottle locked on and the system charged to 115psi, our compressor’s maximum. This ensured all connections, glue, and joints were sound.

We discovered through this that there needs to be at least 30psi in the bottle for it to even leave the launcher. With the heat-deformed PVC sealing system, there is quite a lot of friction. Because no water leaks during this phase (unless the rocket is left for a very long time, in which case even a tiny leak over time will drain the bottle), this isn’t an issue normally. The rocket will move slowly off the friction point, but not lose significant water until it has cleared the top of the launch tube and the water can flow freely out under its pressure, and thus make the rocket move. In the initial phase, the rocket moves because the expanding air wants to occupy the volume taken up by the tube, and so displaces it. That’s the same as if you had a pressurised bottle with a solid rod in the neck instead of the launch tube - you would have a projectile launcher at that point.

After that, it was time to move to a more open area. Every local government area has bylaws in addition to state laws. Whether or not you can launch these from your local sports field will vary between councils. Luckily for us, we are not far from a disused airstrip which is now owned partly by the local council, and partly by Forestry Corporation (correct at time of writing, formerly Forests NSW, formerly NSW Department of Forestry, formerly NSW Department of State Forests, formerly NSW Forestry Commission). This space is used by model aircraft clubs with permission from both government bodies, is far from aircraft flight paths (besides the models) and is not entirely used by the clubs, with some unused space remaining. Accordingly, we set out to find a space out of everyone’s way, where the rockets would not land in the trees or on walkers who also use the space, and where we could drag batteries and inverters to run the compressor.

We could show you the results with a photo, but that’s a bit boring. Instead, keep an eye on our social media channels and on Youtube for videos of the launches. They will do it far more justice than a photo or two does! We’re also looking for a location closer to power because, as expected, the inverter is really working hard to run the compressor!


Although the phrase ‘It’s not rocket science’ gets thrown around implying rocket science is crazy difficult, and putting humans into space was a deeply involved achievement, making a water rocket move is not anywhere near that level. The absolute basic principle is Newton’s Third Law of Motion, in which every action has an equal and opposite reaction. The pressurised air has significant potential energy stored in it, and it wants to expand. The water is in the way, and gets pushed out. The water has mass, and by throwing the mass of water one way, the rocket moves in the other. So, how much water compared to air is open to experimentation, but around one third of the capacity of the bottle is a very common figure within the water rocket community, at least for basic single-chamber bottles. Note that this is one third of the capacity, not the height. It is best to calculate the required amount of water and measure with a calibrated scientific measuring tool, known as a ‘kitchen measuring jug’.

The next point of rocket theory relevant to us is the overall geometry of the rocket. When it comes to rockets powered by sudden and short impulses rather than the long-duration thrust of a liquid- or solid-fueled rocket that can travel to space, the most ideal rocket shape is as close to an arrow as possible. An arrow is long and thin, with small guide fins at the rear. The arrow has a centre of gravity close to the middle, as the fins are the only difference along its length, which is otherwise of nearly equal mass and cross-section. The rocket needs the same shape but to have the heavier masses involved concentrated to the front, so the centre of gravity is near the nose. The centre of pressure needs to be below this, and the greater the distance between the centre of gravity and the centre of pressure, the better. There is a link in the Reading and Resources section that explains it better than we can.

A nose cone is often needed to ensure aerodynamic flight, and this will add weight as well as length, so it can be considered neutral if carefully designed. Fins will be needed to ensure stable flight. They can be cardboard glued on, or custom 3D-printed like ours. They can be big or small, although there are limits both ways. Too small, and they make no difference. Too big, and they induce too much drag and add too much weight. On the subject of paint, even that adds weight, but in a rocket like this, it is unlikely to matter much. On cardboard model rockets with small combustion engines, it does matter.

Of course, there are many variations on all of this. Make use of the links to the channels and communities we provided last month, and do lots of research. The 3D print files we used are provided, but they suit 1.25L Coca Cola bottles in particular (sadly, someone in the office still drinks way too much of that stuff, so we had many identical bottles to use). One of the best-performing rockets we have ever seen was made by a year 11 student at a youth group event, with nothing more than thin cardboard for the nose cone, and firm cardboard for the fins. He did, however, spend nearly an hour with a calculator, protractor, scales, and a physics e-textbook. Notwithstanding, the point is valid: The rocket need not be elaborate.

You will also need to think about recovery systems.
This can be a parachute, or even a bunch of streamers which deploy when a loose nose cone separates after apogee (the highest point, after which the rocket tips sideways and begins to fall) and produces enough drag to slow the rocket. If it’s just a bottle with cardboard fins and a thin card nose cone, you may not worry at all, provided there is no one else to be hit by the falling rocket and you have something to hide under!


For most people without extensive knowledge of water rockets and fibreglassing, water rockets start with a PET soft drink bottle. Do not use a water bottle! Use a bottle meant to contain pressure. Extensive tests by many model rocketeers and even some TV shows (Mythbusters in particular) have revealed that PET soft drink bottles burst at around 150psi. These bottles have a static pressure unopened of around 100psi in them, which they have to contain. The big difference is that the volume of gas held at that pressure in a new, filled, unopened bottle of soft drink is tiny, which is why all you hear is a sharp crack when you open a bottle. If a bottle with no liquid but full of air at 100psi was opened the same way, the results would be very different!

A water bottle designed for spring water under no pressure does not have to be made to these standards, and may rupture. Worse, it may work for a while and then rupture. When a bottle with such a volume of compressed gas does rupture, it usually decompresses explosively. The large volume of expanding gas is able to do much more work (in the physics sense) than the tiny amount in a bottle of drink, and so debris can be forcefully projected, even for quite some distance.

Additionally, be careful with hot melt glue. It tends to deform PET bottles easily. Epoxy adhesives, PVA, and anything else that sticks to plastics and maintains some flexibility but is not corrosive like some construction adhesives or very strong glues are, being filled with solvents that will dissolve PET. You can buy low-temperature hot-melt glue, but it needs a low-temperature hot-melt glue gun to be of any use.


There is little involved in the telescopic limit switch bracket which holds the switch for the collar on the cable tie lock, but some of it is not visible on the outside of the assembly. The telescopic section maintains its position by friction from a thumb screw arrangement. Inside the side extension at the bottom of the assembly is a hexagonal recess, sized for an M3 nut to tightly interference-fit. The nut will not go in on its own, it needs the strength of a screw to pull it into place. To do this, use the same M3 x 16mm cap screw that will be used in the final thumbscrew, and add a flat washer to the outside. Slide the screw through until it can take up the nut inside, and carefully start the thread. Do it up until the nut is sitting just at the edge of the recess with minimal pressure.

Now, you can screw down the bolt using a hex key, and the nut will be dragged into place. As you keep going the turns will get harder, but don't stop too early. The nut sits in quite a way, and the screw will stop turning altogether when the nut is fully home. Then, unscrew the bolt, remove the washer, and set aside the bolt. The knob for the thumb screw is actually an open-source gear from online, but we have modified the centre so the hole is smaller and does not go all the way through. It is sized for an M3 threaded insert, which we heated with a soldering iron before placing. Once home, the excess plastic that melts and protrudes during this process was trimmed away.

The bolt was then sent backwards through the same hole the nut had been embedded in inside, so that the head was facing the inside of the tube. Now, all that was left was to add a regular M3 nut, spin it on far enough to be out of the way, and screw on the knob with its threaded insert until it was just finger-tight. Then, the knob was held (make sure you wait until the plastic has fully cooled and reset around the insert) while the nut was done up against the insert with a small spanner, locking it in place. The tube has a hole on the opposite side so that a hex key can be used to hold the head of the bolt while this process takes place.

The tube slides over the conduit already in place, and can be used to nearly double its length.


The new 3D printed parts are the knob, telescopic limit switch bracket, fins, and nose cone.

The nose cone is sized for a 0.4mm line width, single-walled print, so that you don’t have to fiddle with vase mode or spiralised prints. It looks like a parabola at first, but it is actually an ogive. Both are cumulative in their graph, but while the parabola reaches a shallower curve until it rounds into a dome and then even flatter, the ogive maintains a point. The parabola is its own mathematical line and algebraic formula, while the ogive is formed by the intersection of two different lines, both arcs of an ellipse. The nose cone should be printed with a brim, but otherwise, settings are up to you. Infill shouldn’t matter because the shape is already hollow, and there are no supports needed (unlike the parabola) because there is never more than a 45° overhang.

The fins are similarly able to be printed with no supports, but they should be printed upside down, with the ring attached to the print bed and the wider fin bases up in the air. Some printers will need a raft or brim to print this well, but our Flashforge Guider II printed it with just a heated bed.

The knob and telescopic bracket assembly can be printed as-is. The triangular section on one end of the tube, where the thumb screw assembly is, is there to enable printing without supports. It ensures no overhang greater than 45°. The squared section where the switch glues on should be on the build plate. The knob is also directional, but if you print it upside down, the hole is so small that most printers will bridge it when printing the outer surface anyway. ■

Reading & Resources: