
Time 30-60 minutes
This tutorial builds on the last tutorial.
In this tutorial, we will build a web interface for our Laser tag gun. We will be able to change the player name and the team name via the web interface.
I was deliberating about when to put this functionality into the gun, and decided it is worth putting this in fairly early so variables can be easily changed mid-game.
So far you have already experienced using Elegant OTA with your node MCU board, so things should not be too unfamiliar as we delve into creating a web interface.
You would have installed the libaries ESPAsyncTCP and ESPAsyncWebServer when you completed the Elegant OTA tutorial. These libraries will need to be installed on your Arduino software for your program to compile.
2 main components of the web interface:
before we get started on the programming, it may be worth saving your existing program to a different file name I’ve changed my file name to “NodeMCU_OTA_LED_WEB”. It describes the parts of the program we have added so far.
1. Web Actions
Web actions are often triggered by some event. It could be a request for a web page, or after the web page is sent, it could be a response to a button press.
The web actions are usually entered in the void setup() loop. Since there are a number of instructions that need to be written, I like to move it to a subroutine and put it in its own tab.
- We will call the subroutine setupWebActions and make a call to it in the setup loop as shown below.
void setup() {
// Serial communication set up to help with debugging.
Serial.begin(74880);
Serial.println("Serial Communication is set up");
setupLEDStrip();
setupOTA();
setupWebActions();
}
Create a tab called WebActions
This tab will contain two subroutines.
- setupWebActions()
This will respond to web requests by sending webpages, and later on can be used to receive information from forms filled in on a web page. We will set up a page to respond to the root directory, and a second page that will respond to the debug directory. This will allow us to post any debug information such as errors to the website. - notFound()
This will return a 404 error as text. Notice it is sent as “text/plain” not “text/html”. The browser will expect this error message to be plain text and not HTML.
void setupWebActions(){
// Send web page with input fields to client
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html);
});
// Send web page with error messages
server.on("/debug", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/html", String(debugInfo));
});
}
void notFound(AsyncWebServerRequest *request) {
request->send(404, "text/plain", "Not found");
}
2. Web page
a. Create a tab called webPage.h
In this tab we will put all the code for the web page. The code will be put in a constant called index_html[]. Normally constants are declared at the start of the program, but to keep the program well organised we will have a complete tab dedicated to it. This is especially important as the web page can become quite large. We will use what is called a “header” to allow this to happen. Notice the extra bit of code before the constant is declared.
//use a header to store html document
#ifndef INDEXPAGE_H
#define INDEXPAGE_H
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head><title>Hello World</title></head>
<body>
Hello World
</body>
</html>
)rawliteral";
#endif
b. Call the webPage.h header from the main program & error variable
On the first tab where your main program is stored, add the following code after you include the libraries and variables for the AsyncWebServer.
//call the indexPage.h Header. (this includes the web page in html format)
#include "indexPage.h"
//debug variable
String debugInfo = "<h2>Debug Log</h2>";
The above code allows us to store the HTML code in one file uisng the #include command. It is similar to calling a library. We also create a blank string called debugInfo. We will store error codes in HTML format and then display to a webpage
c. Upload the program onto your NodeMCU board
If you don’t know how to upload the program, go back to the OTA post
d. Test the web page by typing the IP address of your NodeMCU board into the web browser
If you don’t know how to upload the program, go back to the OTA post
The web page should look like the image below. You should see Hello World twice, once for the title of the webpage, and the second one in the web page body.

Change Player ID and Team ID
Each Laser tag gun needs its unique Player ID, and be put in a team. This next section will show us how to change playerID and TeamID.
When we change the TeamID, the LED light strip should change colour.
Set up string processing for HTML file
String processing allows a variable to be inserted into the HTML file. For this exercise we will want to display the existing playerID and teamID on the webpage.
Add the following code to the main program. You should already have the teamID variable
// game variables
int teamID = 4; // 0-4 0 = red, 1 = blue, 2 = yellow, 3 = green, 4 = unspecified
int playerID = 99; // 0-99 Each gun needs a unique player ID
In the “WebActions” tab, add the following string processing command.
String processor(const String& var){
if(var == "PLACEHOLDER_PlayerID"){
return String(playerID);
}
else if(var == "PLACEHOLDER_TeamID"){
return String(teamID);
}
return String();
}
In the “WebActions” tab replace the code:
request->send_P(200, "text/html", index_html);
with
request->send_P(200, "text/html", index_html, processor);
Adding processor to this code allows the string processor to operate, swapping any placeholders declared.
In the code above:
- “text/html” tells the browser it is html
- index_html links to the variable that holds the html data
- Processor replaces the wildcards in the html
Modify the HTML file
Add the form into indexPage.h
You will notice the placeholders %PLACEHOLDER_PlayerID% and %PLACEHOLDER_TeamID% have been added as well as forms.
The forms use the /get command to get data back to the NodeMCU board.
The code onsubmit=”setTimeout(function(){window.location.reload();},10);” use used to refresh the page after a small delay.
Below the code contains one form, two number inputs and one button input.
//use a header to store html document
#ifndef INDEXPAGE_H
#define INDEXPAGE_H
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head><title>LaserTag Game Settings</title></head>
<body>
<H1> Player: %PLACEHOLDER_PlayerID% Team: %PLACEHOLDER_TeamID% </H1>
<form action="/get" target="hidden-form" onsubmit="setTimeout(function(){window.location.reload();},10);">
Player ID | currently: %PLACEHOLDER_PlayerID% | 0-99 : <input type="number" name="inputPlayerID" value=%PLACEHOLDER_PlayerID% min="0" max="99" step="1" pattern="\d+"><br>
Team ID | currently: %PLACEHOLDER_inputTeamID% | 0-3 : <input type="number" name="inputTeamID" value=%PLACEHOLDER_TeamID% min="0" max="3" step="1" pattern="\d+">
<br>
<input type="submit" value="Submit" onclick="submitMessage()">
</form><br>
<iframe style="display:none" name="hidden-form"></iframe>
</body>
</html>
)rawliteral";
#endif
Add a web action to receive data from the “/get” action
When the submit button is pressed, any data in the fields will be added to a variable. The IF statements are used to ensure the values exist, without the if statements, an error might be thrown if the values did not exist.
The setupWebActions() subroutine should now look like:
void setupWebActions(){
// Send web page with input fields to client
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, processor);
});
// Send a GET request for the form
server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
String inputMessage;
// GET inputPlayerID from web form
if (request->hasParam("inputPlayerID")) {
inputMessage = request->getParam("inputPlayerID")->value();
playerID = inputMessage.toInt();
}
// GET inputPlayerTeam
if (request->hasParam("inputTeamID")) {
inputMessage = request->getParam("inputTeamID")->value();
teamID = inputMessage.toInt();
teamColour(); // make sure team colour is updated
}
});
}
Test your code
The website should now look like:

When you change the Team ID, the lights on the gun should change colour to match the team colour.
The player number and team ID will change, but will not be saved to the gun. The gun will return to its original state. NodeMCU has the ability to save data so it is retained between reboots and upgrading your firmware.
Debugging
We want to be able to see if certain code is run. To ensure the setup is complete, we will add a line at the end of the setup loop that will write to the debugInfo variable so we know the setup is complete.
Add the following code at the end of the startup loop:
debugInfo += "Setup Complete <br>";
Once you compile and upload the firmware, you should be able to access a debug page as follows:

Christian Content
At the moment we can change the player ID and the Team ID, but as soon as the guns are reset or turned off, they forget who they are. They forget the instructions we have given, and they return to their original state.
This original state is not where we want our guns to be, they are all very similar clones of each other, and will not function correctly.
It would be increasingly frustrating if you needed to keep on reminding your gun what it’s player ID and team ID should be.
The Bible talks about people in the Bible who are forgetful. And it’s not a good thing.
Isaiah speaking on Behalf of God said this to his people of Israel
Isaiah 1:2-3 (HCSB)
Listen, heavens, and pay attention, earth, for the Lord has spoken:
“I have raised children and brought them up,
but they have rebelled against Me.
3 The ox knows its owner,
and the donkey its master’s feeding trough,
but Israel does not know;
My people do not understand.”
It isn’t very flattering when you are compared to a donkey, and the donkey comes up on top.
But while God addressing Israel over 2500 years ago might seem a little distant. The same insult could be given to us. That we don’t know our master, we have somehow forgotten
In the book of James, it is written:
James 1:22-25 (HCSB) But be doers of the word and not hearers only, deceiving yourselves. 23 Because if anyone is a hearer of the word and not a doer, he is like a man looking at his own face c in a mirror. 24 For he looks at himself, goes away, and immediately forgets what kind of man he was. 25 But the one who looks intently into the perfect law of freedom and perseveres in it, and is not a forgetful hearer but one who does good works—this person will be blessed in what he does.
When we look at the word, that is the Bible, there is an expectation that we will not just hear, that it doesn’t just go in one ear and out the other, but it results in long-term action.
You might go to church, you might read your Bible, but if it doesn’t result in action, then it is of no worth. It’s just like looking into a mirror and immediately forgetting what you look like.
When you read the Bible or hear the Bible taught, it should result in action, in obedience to the master.
So remember who your master is, and remember your master’s words.
I want to finish this devotion by looking at the following verses
2 Peter 1: 3-11 (HCSB) His divine power has given us everything required for life and godliness through the knowledge of Him who called us by l His own glory and goodness. 4 By these He has given us very great and precious promises, so that through them you may share in the divine nature, escaping the corruption that is in the world because of evil desires. 5 For this very reason, make every effort to supplement your faith with goodness, goodness with knowledge, 6 knowledge with self-control, self-control with endurance, endurance with godliness, 7 godliness with brotherly affection, and brotherly affection with love. 8 For if these qualities are yours and are increasing, they will keep you from being useless or unfruitful in the knowledge of our Lord Jesus Christ. 9 The person who lacks these things is blind and shortsighted and has forgotten the cleansing from his past sins. 10 Therefore, brothers, make every effort to confirm your calling and election, because if you do these things you will never stumble. 11 For in this way, entry into the eternal kingdom of our Lord and Savior Jesus Christ will be richly supplied to you.