This page contains a summary of what I have learned whilst trying to build a web-hosted temperature/humidity sensor module using the ESP8266 WiFi microcontroller. At the end of the project, you will end up with a WiFi-controllable Temperature & Humidity sensor, updatable over WiFi, with a webserver hosting your data on your local network using ChartJS. This is all wrapped up in a 3D printed housing model to it to finish it all up.

The code used in this project can be found on my Github Page here.

Hard-ware + Wiring scheme

For this project I used:

  • NodeMCU ESP8266
  • DHT-11 Temperature & Humidity sensor
  • 10kOhm resistor
  • 0.96inch OLED display (Optional)

These parts come for less than €5,- a piece these days, making the entry-barrier for these kinds of projects very low. Most of these can be bought from or Due to the rapid supplier turnover, I will refrain from adding links for the individual components.

The ESP8266

The ESP8266 is a microcontroller with built-in WiFi capabilities. You can view it as a “special Arduino”, that with the help of some existing libraries allows you to easily build and host webservers/websockets. The ESP8266 is often used for IoT applications, for instance home-automation and sensors.

An excellent resource for understanding the many features of the ESP chipset was made by tttapa, and can be found on his personal page here. A large part of my own software is based on his tutorial.

Wiring Scheme

Wiring scheme ESP8266 diagram from

For testing, I used a breadboard and some DuPont jumper cables to connect setup. The wiring is very basic: we essentially connect the power and ground of the ESP to that of the components. Both the DHT11 and the 0.96 OLED display communicate using digital signals. We can hook up the signal/read-out lines to arbitrary Digital ports of our ESP, as long as it matches our code later.


In this section I will first explain how I set-up my environment, including dependencies. I wil then go through each part of the code and show it as a stand-alone code snippet. At the end, I will show the final result.

Installing Dependencies

We start by cloning the repository:

Sadly, there is no good way to track and recreate Arduino environments, including dependencies1. As such, we will need to manually set up our Arduino environment through the Arduino IDE.

When I said the ESP8266 is a “special Arduino”, it also meant that the standard Arduino compiler does not work for the ESP chipsets. To compile your code for the ESP8266, you need the so-called board-manager for ESP2. In your Arduino IDE under File -> Preferences -> Additional Boards Manager URLs you need to add this URL to import the compiler. If you already had a URL there, you can add additional URLs by separating them with commas. Activate the ESP Board Manager by going to Tools -> Board -> Board Manager and searching for “esp8266”.

Next we need to install libraries for our other hardware. In the Tools menu, select Manage Libraries.... In the search for:

  • ADAfruit SSD1306 (for the OLED screen)
  • DHT sensor (for our DHT temperature/humiditys sensor)

We also need to the ESP8266 Filesystem uploader, which you can get from the official ESP8266 Github page: To save you the trouble, I have very lazily included it in my repository under the tools/ directory

Setting up Wifi

#include <ESP8266WiFi.h>        // Include the Wi-Fi library
#include <ESP8266WiFiMulti.h>   // Include the Wi-Fi-Multi library

const char* ssid     = "SSID";         // The SSID (name) of the Wi-Fi network you want to connect to
const char* password = "PASSWORD";     // The password of the Wi-Fi network

void setup() {
  Serial.begin(115200);         // Start the Serial communication to send messages to the computer
  WiFi.begin(ssid, password);             // Connect to the network
  Serial.print("Connecting to ");
  Serial.print(ssid); Serial.println(" ...");

  int i = 0;
  while (WiFi.status() != WL_CONNECTED) { // Wait for the Wi-Fi to connect
    Serial.print(++i); Serial.print(' ');

  Serial.println("Connection established!");  
  Serial.print("IP address:\t");
  Serial.println(WiFi.localIP());         // Send the IP address of the ESP8266 to the computer


Fortunately, there’s another way: multicast DNS, or mDNS. mDNS uses domain names with the .local suffix, for example http://esp8266.local. If your computer needs to send a request to a domain name that ends in .local, it will send a multicast query to all other devices on the LAN that support mDNS, asking the device with that specific domain name to identify itself. The device with the right name will then respond with another multicast and send its IP address. Now that your computer knows the IP address of the device, it can send normal requests.

Luckily for us, the ESP8266 Arduino Core supports mDNS:

void setup() {
  if (!MDNS.begin("hello")) {             // Start the mDNS responder for esp8266.local
    Serial.println("Error setting up MDNS responder!");
  Serial.println("mDNS responder started");
  MDNS.addService("http", "tcp", 80); 

void loop(void){
  server.handleClient();                    // Listen for HTTP requests from clients

Then upload the webpages and scripts to SPIFFS using Tools > ESP8266 Sketch Data Upload.


#include <FS.h>   // Include the SPIFFS library
void setup () {
    SPIFFS.begin();                           // Start the SPI Flash Files System

Over-the-Air updates

void setup() {}

  ArduinoOTA.onStart([]() {
  ArduinoOTA.onEnd([]() {
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");
  Serial.println("OTA ready");

void loop() {

mDNS needs to be initialized after OTA because of hostname override

Web-based filebrowser

void setup(){
      // SERVER INIT
  //list directory
  server.on("/list", HTTP_GET, handleFileList);
  //load editor
  server.on("/edit", HTTP_GET, [](){
    if(!handleFileRead("/edit.htm")) server.send(404, "text/plain", "FileNotFound");
  //create file
  server.on("/edit", HTTP_PUT, handleFileCreate);
  //delete file
  server.on("/edit", HTTP_DELETE, handleFileDelete);
  //first callback is called after the request has ended with all parsed arguments
  //second callback handles file uploads at that location
  server.on("/edit", HTTP_POST, [](){ server.send(200, "text/plain", ""); }, handleFileUpload);

  //called when the url is not defined here
  //use it to load content from SPIFFS
      server.send(404, "text/plain", "FileNotFound");

  //get heap status, analog input value and all GPIO statuses in one json call
  server.on("/all", HTTP_GET, [](){
    String json = "{";
    json += "\"heap\":"+String(ESP.getFreeHeap());
    json += ", \"analog\":"+String(analogRead(A0));
    json += ", \"gpio\":"+String((uint32_t)(((GPI | GPO) & 0xFFFF) | ((GP16I & 0x01) << 16)));
    json += "}";
    server.send(200, "text/json", json);
    json = String();
  Serial.println("HTTP server started");

void loop() {
      server.handleClient();                    // Listen for HTTP requests from clients

String getContentType(String filename){
  if(filename.endsWith(".htm")) return "text/html";
  else if(filename.endsWith(".html")) return "text/html";
  else if(filename.endsWith(".css")) return "text/css";
  else if(filename.endsWith(".js")) return "application/javascript";
  else if(filename.endsWith(".png")) return "image/png";
  else if(filename.endsWith(".gif")) return "image/gif";
  else if(filename.endsWith(".jpg")) return "image/jpeg";
  else if(filename.endsWith(".ico")) return "image/x-icon";
  else if(filename.endsWith(".xml")) return "text/xml";
  else if(filename.endsWith(".pdf")) return "application/x-pdf";
  else if(filename.endsWith(".zip")) return "application/x-zip";
  else if(filename.endsWith(".gz")) return "application/x-gzip";
  return "text/plain";

bool handleFileRead(String path){  // send the right file to the client (if it exists)
  // Print the filename request in serial monitor
  Serial.println("handleFileRead: " + path);
  // If a folder is requested, send the index.html file instead as is standard for websites
  if(path.endsWith("/")) path += "index.html";           // If a folder is requested, send the index file

  // Get the MIME type from the path name
  String contentType = getContentType(path);             // Get the MIME type
  String pathWithGz = path + ".gz";
  if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){  // If the file exists, either as a compressed archive, or normal
    if(SPIFFS.exists(pathWithGz))                          // If there's a compressed version available
      path += ".gz";                                         // Use the compressed version
    File file =, "r");                    // Open the file
    size_t sent = server.streamFile(file, contentType);    // Send it to the client
    file.close();                                          // Close the file again
    Serial.println(String("\tSent file: ") + path);
    return true;
  Serial.println(String("\tFile Not Found: ") + path);
  return false;                                          // If the file doesn't exist, return false

void handleFileUpload(){
  if(server.uri() != "/edit") return;
  HTTPUpload& upload = server.upload();
  if(upload.status == UPLOAD_FILE_START){
    String filename = upload.filename;
    if(!filename.startsWith("/")) filename = "/"+filename;
    Serial.print("handleFileUpload Name: "); Serial.println(filename);
    fsUploadFile =, "w");
    filename = String();
  } else if(upload.status == UPLOAD_FILE_WRITE){
    //Serial.print("handleFileUpload Data: "); Serial.println(upload.currentSize);
      fsUploadFile.write(upload.buf, upload.currentSize);
  } else if(upload.status == UPLOAD_FILE_END){
    Serial.print("handleFileUpload Size: "); Serial.println(upload.totalSize);

void handleFileDelete(){
  if(server.args() == 0) return server.send(500, "text/plain", "BAD ARGS");
  String path = server.arg(0);
  Serial.println("handleFileDelete: " + path);
  if(path == "/")
    return server.send(500, "text/plain", "BAD PATH");
    return server.send(404, "text/plain", "FileNotFound");
  server.send(200, "text/plain", "");
  path = String();

void handleFileCreate(){
  if(server.args() == 0)
    return server.send(500, "text/plain", "BAD ARGS");
  String path = server.arg(0);
  Serial.println("handleFileCreate: " + path);
  if(path == "/")
    return server.send(500, "text/plain", "BAD PATH");
    return server.send(500, "text/plain", "FILE EXISTS");
  File file =, "w");
    return server.send(500, "text/plain", "CREATE FAILED");
  server.send(200, "text/plain", "");
  path = String();

void handleFileList() {
  if(!server.hasArg("dir")) {server.send(500, "text/plain", "BAD ARGS"); return;}
  String path = server.arg("dir");
  Serial.println("handleFileList: " + path);
  Dir dir = SPIFFS.openDir(path);
  path = String();

  String output = "[";
    File entry = dir.openFile("r");
    if (output != "[") output += ',';
    bool isDir = false;
    output += "{\"type\":\"";
    output += (isDir)?"dir":"file";
    output += "\",\"name\":\"";
    output += String(;
    output += "\"}";
  output += "]";
  server.send(200, "text/json", output);

//format bytes
String formatBytes(size_t bytes){
  if (bytes < 1024){
    return String(bytes)+"B";
  } else if(bytes < (1024 * 1024)){
    return String(bytes/1024.0)+"KB";
  } else if(bytes < (1024 * 1024 * 1024)){
    return String(bytes/1024.0/1024.0)+"MB";
  } else {
    return String(bytes/1024.0/1024.0/1024.0)+"GB";

Authentication for filebrowser

in index.html

function logoutButton() {
  var xhr = new XMLHttpRequest();"GET", "/logout", true);
  setTimeout(function(){"/logged-out","_self"); }, 1000);

In each function we need:

  if (!httpServer.authenticate(wwwUsername, wwwPassword)) {


// For Network Time Protocol (NTP)
IPAddress timeServerIP;          // NTP server address
const char* NTPServerName = "";

const int NTP_PACKET_SIZE = 48;  // NTP time stamp is in the first 48 bytes of the message

byte NTPBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets

Chart JS


Housing 3D model