Setting Up a Traffic Light IoT Device in Arduino / NodeMCU

Mar 5 2021 · 11 min read

After being able to dynamically change the states by command, we also want to manage things remotely! In this post, we will learn how to convert our device into an internet connected "thing".

Concept

In the previous project, we have successfully made a Traffic Light that we can freely change on the go by communicating through the serial communication line. But what if we want to control multiple traffic lights remotely (a traffic usually use more than 1 traffic light)? How about adding or removing traffic lights on the go? Usually we have to upload a new code to the Arduino telling where the pins are. We can solve these problems by using the internet to communicate through our device!

By connecting our devices through the internet to communicate with it, we will have successfully created an IoT device.

By doing so, we can easily scale our traffic system by adding more traffic lights and process them with the data provided by each traffic light (which ones are turning green/red). We can add external data like crowded lane detection using induction loops or object detection implemented CCTV cameras to automatically determine which traffic light should turn on/off, ultimately creating a very efficient system.

What You Will Need

  • 1 ˣ NodeMCU
  • 1 ˣ MicroUSB (for connecting the NodeMCU to your PC)
  • 6 ˣ LEDs or 2 Traffic Light Module
  • 6 ˣ 220 Ohm Resistors
  • Jumper Wires

Make sure that you have implemented the Traffic Light class so it is easier to call the traffic light object.

Build The Circuit

From the image provided above, connect:

  • All Red, Yellow, and Green LED Cathodes to GND
  • Red Anode 1 -> 220Ω -> GPIO4
  • Yellow Anode 1 -> 220Ω -> GPIO0
  • Green Anode 1 -> 220Ω -> GPIO2
  • Red Anode 2 -> 220Ω -> GPIO13
  • Yellow Anode 2 -> 220Ω -> GPIO12
  • Green Anode 2 -> 220Ω -> GPIO14

Follow this diagram for GPIO pins in NodeMCU:

Nodemcu Nodemcu Gpio With Arduino Ide | Nodemcu

Get on Coding!

Now let’s get into the code! I’ll be presenting the full code head on first, and then I’ll explain the code by segments:

include "TrafficLight.h"
include <ESP8266WiFi.h>
include <ESP8266WebServer.h>

char ssid[] = "<your-wifi-ssid>";
char pass[] = "<your-wifi-password>";

// create a server with port 80 (default web port)
ESP8266WebServer  server(80);
// this will hold the current traffic light max
int ct = 0;
// set a maximum of 3 because of memory & pin limitations
TrafficLight *trafficLights[3];

void connectWIFI() {
  // for decoration purposes
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  // try to connect to the wifi
  WiFi.begin(ssid, pass);
  while (WiFi.status() != WL_CONNECTED) {
    // retry attempt every 500ms
    // we won't start our code if wifi hasn't connected
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected!");
  // shows the IP address that our device uses
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

// returnInvalid will return a response with 400 Invalid Request
void returnInvalid() {
  server.send(400, "text/plain", "Invalid Request");
}

// returnOK will return a response with 200 OK
void returnOK() {
  server.send(200, "text/plain", "OK");
}

// handleTraffic will handle requests to change a specific traffic light object's state
void handleTraffic() {
  if(!server.hasArg("state") || !server.hasArg("id")) {
     returnInvalid();
  }
  int id = server.arg("id").toInt();
  if(trafficLights[id]==NULL) {
    returnInvalid();
  }
  switch(server.arg("state").toInt()) {
    case RED:
      trafficLights[id]->Stop();
      break;
    case GREEN:
      trafficLights[id]->Go();
      break;
    case YELLOW:
      trafficLights[id]->Careful();
      break;
    default:
      returnInvalid();
  }
  returnOK();
}

// handleRegister handles requests to register a new traffic light object provided the LED pins
void handleRegister() {
  if(!server.hasArg("redPin") || !server.hasArg("yellowPin") || !server.hasArg("greenPin")) {
    returnInvalid();
  }
  if(trafficLights[ct]!=NULL) {
    free(trafficLights[ct]);
  }
  TrafficLight* tl = new TrafficLight(server.arg("redPin").toInt(), server.arg("yellowPin").toInt(), server.arg("greenPin").toInt());

  if(server.hasArg("id")){
    int id = server.arg("id").toInt();
    if(id<3 && id>=0) {
       ct = id;
     } else {
       returnInvalid();
     }
  }
  trafficLights[ct] = tl;
  server.send(200, "text/plain", String(ct));
  ct = (ct + 1)%3;
}

// startServer will register the handlers and start the server
void startServer() {
  server.on("/traffic", handleTraffic);
  server.on("/register", handleRegister);
  server.begin(); //Start the server
  Serial.println("Server listening");
}

// we will call this function first
void setup() {
  Serial.begin(115200);
  delay(10);
  // this section is optional, but I find it being more consistent
  WiFi.mode(WIFI_OFF);
  delay(1000);
  WiFi.mode(WIFI_STA);
  while(!Serial);
  connectWIFI();
  startServer();
}

void loop() {
  //Handling of incoming client requests
  server.handleClient(); 
}

Basically what this code does is that it runs a server that has 2 request handlers:

  • /register, for registering new traffic light objects, and;
  • /traffic, for managing the traffic lights.

Since we are serving on port 80 with a simple GET request, we can just call these handlers by accessing the URL on our browser.

Let’s now dissect this big code by segments.

The first segment contains headers & variable initializations:

include "TrafficLight.h"
include <ESP8266WiFi.h>
include <ESP8266WebServer.h>

char ssid[] = "<your-wifi-ssid>";
char pass[] = "<your-wifi-password>";

// create a server with port 80 (default web port)
ESP8266WebServer  server(80);
// this will hold the current traffic light max
int ct = 0;
// set a maximum of 3 because of memory & pin limitations
TrafficLight *trafficLights[3];

We are importing the TrafficLight.h library from our previous projects. In NodeMCU, we are using ESP8266 as the WiFi module, so we will be using that library for our code. For more information regarding installing these 2 libraries, check out the official documentation.

ssid and pass will contain your WiFi SSID and password respectively.

we are also declaring ESP8266WebServer server(80); to say that we will have a server listening to port 80.

To hold the traffic light objects, we can use an array for a simple implementation, using ct as a simple index pointer to the objects.

The next segment will be discussing more about the server functions:

void connectWIFI() {
  // for decoration purposes
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  // try to connect to the wifi
  WiFi.begin(ssid, pass);
  while (WiFi.status() != WL_CONNECTED) {
    // retry attempt every 500ms
    // we won't start our code if wifi hasn't connected
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected!");
  // shows the IP address that our device uses
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

// returnInvalid will return a response with 400 Invalid Request
void returnInvalid() {
  server.send(400, "text/plain", "Invalid Request");
}

// returnOK will return a response with 200 OK
void returnOK() {
  server.send(200, "text/plain", "OK");
}

connectWIFI() function contains the WiFi connect procedures the device will take. It will attempt to connect to the WiFi using WiFi.begin(ssid, pass), using the ssid and password that we have provided. Then, it will be in an endless loop while checking whether the WiFi.status() have returned a connected state. Once a connection is successfully established, the function will output the local IP address that it is using to connect to the WiFi.

returnInvalid() and returnOK() are helper functions that we use to simplify response returning to the request. We will look at these two being used in the next segment, request handler functions:

// handleTraffic will handle requests to change a specific traffic light object's state
void handleTraffic() {
  if(!server.hasArg("state") || !server.hasArg("id")) {
     returnInvalid();
  }
  int id = server.arg("id").toInt();
  if(trafficLights[id]==NULL) {
    returnInvalid();
  }
  switch(server.arg("state").toInt()) {
    case RED:
      trafficLights[id]->Stop();
      break;
    case GREEN:
      trafficLights[id]->Go();
      break;
    case YELLOW:
      trafficLights[id]->Careful();
      break;
    default:
      returnInvalid();
  }
  returnOK();
}

// handleRegister handles requests to register a new traffic light object provided the LED pins
void handleRegister() {
  if(!server.hasArg("redPin") || !server.hasArg("yellowPin") || !server.hasArg("greenPin")) {
    returnInvalid();
  }
  if(trafficLights[ct]!=NULL) {
    free(trafficLights[ct]);
  }
  TrafficLight* tl = new TrafficLight(server.arg("redPin").toInt(), server.arg("yellowPin").toInt(), server.arg("greenPin").toInt());
  if(server.hasArg("id")){
    int id = server.arg("id").toInt();
    if(id<3 && id>=0) {
       ct = id;
     } else {
       returnInvalid();
     }
  }
  trafficLights[ct] = tl;
  server.send(200, "text/plain", String(ct));
  ct = (ct + 1)%3;
}

Whenever a request comes into the server, the server has to decide how to handle the request. For now, we have 2 different request handlers, handleTraffic and handleRegister.

handleTraffic() will receive 2 parameters, state and id. This is so we can tell the server to change the TrafficLight assigned with the specific id to change to the desired state. Once we have ensured that both of them are present with hasArg, we check whether the id is a valid id. Afterwards we switch the traffic light state and send an OK message to the request, indicating that it was a successful.

handleRegister() on the other hand receives 3 mandatory parameters and an optional parameter id. We have to provide the LED pins that we are using for the new Traffic Light object. The id parameter will be used for allocating the traffic light to the specified id (with the condition it’s a valid id allocation).

The ct pointer works on a very simple basis; it will register the current trafficLight then move on to the next index in a circular loop, replacing anything that it is registering. Once we have successfully registered a new Traffic Light, the ct will return the associated ID where it placed it.

Now that the handlers are all set up, we can register these handlers and start the server:

// startServer will register the handlers and start the server
void startServer() {
  server.on("/traffic", handleTraffic);
  server.on("/register", handleRegister);
  server.begin(); //Start the server
  Serial.println("Server listening");
}

In the startServer function, we call server.on to register the handlers with the associated url. After registering all of the required handlers, we can safely start the server by calling server.begin()

Now to execute all of those things, we implement them in our setup and loop function:

// we will call this function first
void setup() {
  Serial.begin(115200);
  delay(10);
  // this section is optional, but I find it being more consistent
  WiFi.mode(WIFI_OFF);
  delay(1000);
  WiFi.mode(WIFI_STA);
  while(!Serial);
  connectWIFI();
  startServer();
}

void loop() {
  //Handling of incoming client requests
  server.handleClient(); 
}

In the setup function, we initialize the serial baudrate to 115200, and set the WiFi to station mode. You can read more about ESP8266 WiFi modes in the official documentation. This part is optional. It can still run without setting the WiFi mode because it will fall to the default mode (WIFI_AP_STA).

Once our serial communication is ready, we can call connectWifi and startServer and run our server on the provided WiFi.

Next we go to the loop function, where it will constantly handle client requests by calling server.handleClient().

Running the Code

You can first debug your device by reading the serial monitor. First, it will connect to the provided WiFi:

Once you have seen this message, you can access the provided IP address in your browser and call the handlers that we have initialized. Let’s say we want to set up the first traffic lights.

The device returned us a 0, meaning that the traffic light got assigned with an ID of 0! Let’s try managing that new traffic light:

Now, if we look at our traffic light, we can see that it’s now turning on red. Now try registering the other traffic light and play around with other states!

Getting Status

We have completed the steps to manage multiple traffic lights remotely! But now what if we want to get the status of a traffic light? We can add another handler that returns the state of the traffic light with the specified ID.

To accomplish this, we need to add another function in the TrafficLight library class. First, we add another function declaration in the class header file, called String GetStatus():

class TrafficLight
{
  public:
    TrafficLight(byte redPin, byte yellowPin, byte greenPin);
    void Toggle(int color);
    void TurnOff(int color);
    void TurnOn(int color);
    void Go();
    void Careful();
    void Stop();
    bool GetState(int color);
    bool* GetStates();
    String GetStatus();
  private:
    byte pins[3];
bool states[3]={0}; void init();
};

Then we add the implementation in TrafficLight.cpp

String TrafficLight::GetStatus() {
  int selected = 0;
  for(selected=0; i<3; selected++) {
    if(this->states[selected]) {
      break;
    }
  }
  switch(selected) {
    case RED:
      return "STOP";
    case GREEN:
      return "GO";
    case YELLOW:
      return "CAREFUL";
  }
  return "OFF";
}

After that is done, we can add another function in our main code:

void handleStatus() {
  if(!server.hasArg("id")) {
    returnInvalid();
  }
  int id = server.arg("id").toInt();
  if(trafficLights[id]==NULL) {
    returnInvalid();
  }
  server.send(200, "text/plain", trafficLights[id]->GetStatus());
}

Here, we are telling that the handleStatus will require one parameter id and returns the state of the selected traffic light, given if the selected traffic light does exist.

Register that in our startServer function!

void startServer() {
  server.on("/traffic", handleTraffic);
  server.on("/", handleStatus);
  server.on("/register", handleRegister);
  server.begin(); //Start the server
  Serial.println("Server listening");
}

Let’s try calling the new endpoint that we have created:

Alright! It’s telling us that the current traffic light is in Green!

Summary

Congratulations on finishing this project! You now have learned about:

  • How to manage objects through the internet using NodeMCU/Arduino.
  • How to create endpoints and servers inside NodeMCU/Arduino.
  • Creating and registering handlers.

There are many improvements that can be done about this project, first is with implementing the right HTTP Methods for the endpoints. We are currently only using GET methods on all of our endpoints. Another improvement around the HTTP Method is with the payload, we can send our responses using json instead of plaintext, so we can send more complex data through the response while maintaining readability.

Thank you for reading!

Tags

Share