Laser Tag S03 – Web interface

Love Jesus, Love Code

30-60 minutes

This tutorial builds upon the previous one.

In this tutorial, we will develop a web interface for our Laser tag gun. Through the web interface, we will be able to modify the player name and team name.

I debated when to implement this functionality into the gun, and ultimately decided that it is beneficial to do so early on. This way, variables can be easily adjusted during gameplay.

By now, you should already be familiar with using Elegant OTA with your node MCU board, so delving into the creation of a web interface should not feel too unfamiliar.

When you completed the Elegant OTA tutorial, you would have installed the ESPAsyncTCP and ESPAsyncWebServer libraries. These libraries must be installed in your Arduino software for your program to compile.

2 main components of the web interface:

Before we begin programming, it might be a good idea to save your current program with a different file name. I have already renamed my file to “NodeMCU_LaserTag_S03,” which reflects the tutorial name.

1. Web Actions

Web actions are typically triggered by events, such as a request for a web page or a web page button press response.

These web actions are commonly placed within the void setup() loop. To improve organisation and readability, I prefer to move them into a separate subroutine and place it in its own tab.

Let’s name this subroutine “setupWebActions” and call it within the setup loop as demonstrated below.

void setup() {
  // Serial communication set up to help with debugging.
  Serial.println("Serial Communication is set up");

Create a tab called WebActions

This tab will contain two subroutines.

  1. setupWebActions()
    This subroutine will respond to web requests by sending webpages. Later on, it can also 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.
  2. notFound()
    This subroutine 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 in plain text format, 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 indexPage.h

In this tab, we will place all the code for the web page. The code will be stored in a constant called index_html[]. Normally, constants are declared at the beginning of the program. However, to maintain a well-organised program structure, we will dedicate a separate tab to it. This is particularly important considering that the web page may become quite large. To facilitate this, we will utilize a “header”. Please take note of the additional piece of code preceding the declaration of the constant.

//use a header to store html document

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head><title>Hello World</title></head>
Hello World



b. Call the indexPage.h header from the main program & error variable

On the first tab where your main program is stored, add the following code after you have included 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 code above enables us to store the HTML code in one file using the #include command, similar to calling a library. Additionally, we create a blank string called debugInfo, where we will store error codes in HTML format and display them on a webpage.

c. Upload the program onto your NodeMCU board

If you are unsure about how to upload the program, please refer back to the OTA post.

d. Test the web page by typing the IP address of your NodeMCU board into the web browser

The web page should resemble the image provided below. You will find the phrase “Hello World” displayed twice: once as the title of the webpage, and again within the body of the web page.

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

Each laser tag gun needs its own unique Player ID and needs to be assigned to a team. The following section will demonstrate how to change the player ID and Team ID.

When we modify the Team ID, the LED light strip should change colour.

// 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);


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 to indexPage.h.
You will notice that the placeholders %PLACEHOLDER_PlayerID% and %PLACEHOLDER_TeamID% have been added, as well as the forms.

The forms use the /get command to retrieve data back to the NodeMCU board.

The code onsubmit=”setTimeout(function(){window.location.reload();},10);” is used to refresh the page after a slight delay.

Below the code, there is one form with two number inputs and one button input.

// Use a header to store the HTML document

const char index_html[] PROGMEM = R"rawliteral(
<title>LaserTag Game Settings</title>
 <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()">
 <iframe style="display:none" name="hidden-form"></iframe>


Add a web action to receive data from the “/get” action

When the submit button is pressed, the data entered in the fields will be added to a variable. The IF statements are used to check if the values exist. Without these IF statements, an error could occur if the values are not present.

The setupWebActions() subroutine should now be updated as follows:

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 that it is retained between reboots and when upgrading your firmware.


We want to be able to determine if a specific code is executed. To ensure that the setup is fully completed, we will insert a line of code at the end of the setup loop. This line of code will write to the debugInfo variable, providing us with confirmation that the setup has been completed.

Please add the following code snippet 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:

Video of the coding. I did not put in the TeamID like in the instructions above and it causes an error. But you should be able to follow along and get your code working.

Christian Content

At present, we are able to change the player ID and Team ID of the guns. However, once the guns are reset or turned off, they forget who they are. They forget the instructions we have given them and revert back 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 do not function properly.

It would be increasingly frustrating to constantly remind your gun of its player ID and team ID.

The Bible discusses the issue of forgetfulness among people. And it is not a good thing.

Isaiah, speaking on behalf of God, said this to the 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.”

Being compared to a donkey and being found lacking is not flattering.

But while God’s address to Israel over 2500 years ago may seem distant, the same criticism could apply to us. We may have forgotten our master.

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 engage with the word, which refers to the Bible, we should not simply listen and let it go in one ear and out the other. There is an expectation that it will lead to lasting action.

You may attend church or read your Bible, but if it does not result in action, it holds no value. It is like looking in a mirror and instantly forgetting your own reflection.

When you read or hear the Bible, it should inspire action and obedience to the master.

So remember who your master is and remember His words.

I would like to conclude 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.

Leave a Reply