Tuesday, 2 July 2019

Easier ESP8266 Development

Or How to Learn from My 4 Years of Frustration

(This is a post in my series about my Garage Door Opener.  Yes I wrote a series. About a Garage Door Opener.  Check it out:

Okay, so that took a little longer than expected.....



Back in 2013 I built a light for my Garage Door and my daughter said it would be cool if I could control it from my phone.

My original plan was to simply use a Bluetooth to serial converter, type 'open' in an iPhone app and job's done.

However, my iPhone 4 didn't support Blue Tooth Low Energy, and I was hearing good stuff about this $5 WiFi chip, the ESP8266.

So, I thought I'd give it a go.  But, no, this post isn't about the widget I built, but rather a collection of tip's for anyone else who wants to try developing  with an ESP8266 from scratch.  There was a lot to learn, and if you're looking to get started with the ESP866, please read on and learn from my mistakes!


Where to Start?

Learn from my mistakes.

Not with an ESP-01!  I did start with a few of these, but they suck.

Firstly, there's limited pins, and you need to bias the pins in the right states to either boot the unit or put it in a programming mode.  Also, when you do boot an ESP-01 some of the IO pins toggle as part of the boot process, and you need to develop a method to ignore these pins for a short period of time when booting.  Too much hassle.

If you insist on using one of these modules, there are many tutorials out there that can help you. 

But trust me, you are better off starting with a module like the Wemos D1 mini or Node MCU.  They are still pretty damn cheap, and you only need to connect them to your PC via a USB cable and you're ready to go.  This may sound silly, but with the ESP-01 you need to hold GPIO_0 low, hit the reset button, release GPIO_0 and then start the programming process, which gets old, real fast.  With the modules, you just hit upload and go.

Programming.

I used to have a love / hate relationship with the Ardino IDE, but if you are starting with ESP development, in my opinion you are nuts not to start here.  Again, there's a heap of sites that explain how to get up and running better than I can.  Don't forget to refer to the excellent ESP8266/Arduino resource on GitHub.

Also, this tutorial was a great breakthrough for my understanding about how hosting the webserver on the ESP8266 works, and pointed me to W3Schools which is a great resource for learning about how to build a HTML page. 

Ok, so far all I've done so far is summarise some links and save you some time googling stuff for yourself.  From now on, I'd like to point out how I think you can minimise your development time going forward.

But first some Do Nots:

Do not:

  • Try to fit the html code inline with your arduino code (as an include file).  Escaping quotes will do your head in.  
    • Okay, that's a little judgmental and I did get a great sense of achievement when I got this demo up and running.  So a good start to check out that everything is working but for the Love of God please don't think this is the best way to move forward. 
  • Don't edit html pages in Notepad
    • Yeah, this is what I did to 'just quickly make a small change'.  Don't be like me. Use a good editor.  See below!
  • Don't used online WYSIWYG editor
    • They all suck and will waste your time
  • Don't think you can create a good looking style sheet yourself.  
    • Okay, that might be just my lack of artistic talent manifesting itself, but there are so many free style-sheets out there you're just wasting your time if your starting out and not using them

ESP Tips

Start with Examples



This is a big tip that I wish someone had pointed out to me when I was starting.  

I managed to get bogged down in trying to write simple HTML files and adding them as an include files (with all the grief associated with wrapping all lines in quotes and dealing with strings and escape characters....) when you can simplify issues with using the SPIFFS file system.  

Simply start the FSBrowser example that comes with the Arduino IDE and get cracking.

If you place a HTML file in the data folder in your Arduino project, the FSBrowser code handleFileRead routine will simply search the SPIFFS folder for a matching file, and if found, get the server to, erm, serve it.  Otherwise it returns a 404 error.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
bool handleFileRead(String path){
  DBG_OUTPUT_PORT.println("handleFileRead: " + path);
  if(path.endsWith("/")) path += "index.htm";
  String contentType = getContentType(path);
  String pathWithGz = path + ".gz";
  if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){
    if(SPIFFS.exists(pathWithGz))
      path += ".gz";
    File file = SPIFFS.open(path, "r");
    server.streamFile(file, contentType);
    file.close();
    return true;
  }
  return false;
}

It also redirects you to index.htm (which is included in the examples data folder) if you try to direct the server to the root, and also manages unpacking .gz files too.

You have to upload the contents of the data folder after programming the target by using the "ESP8266 Sketch Data Upload" tool.  Pro-tip:  The tool won't work if you are using the serial monitor at the time you try to upload.

I cannot stress how helpful this example is.  It also includes an mDNS server that lets you connect to the ESP by name, not ip address, (unless you are using an Android device...).  You need to install Bonjour Print Services for Windows for this to work (comes packaged with iTunes btw) in Windows, Avhai to work in Lunix, or nothing for an iPhone as it's standard with iOS.

So, following the above, from scratch within a hour or so you can load your HTML files from a SPIFFS file server.  Impressive.  Took me a year to get here.

Don't Develop on the ESP8266

Wait.. what?

Because I did not know better, I tried to develop my HTML pages buy uploading them to SPIFFS, testing, editing and re-uploading.  As handy as SPIFFS is, this is a 5 minute code and text cycle that also gets old, real fast. 

Also, you tend to have no idea why your formatting is broken, or where you broke your HTML page with your last series of edits.

Thankfully, the solution is real simple.

Develop HTML on Your PC

Open the file locally.  Simply double click your HTML file you are developing and open it in Chrome (you are using Chrome, right?) and then hit F12.  

This opens the Chrome Developers Tools panel which shows you a wealth of data about your page.

What else it means is that you can edit, save and reload a page in seconds.  Much speedier than uploading to your ESP board.

Use a decent Text Editor

While, in theory you can edit a HTML page with notepad, in practice it's nothing but a massive ball ache.  

Options include:

Notepad++

Notepad++ is lightweight, loads fast, has more than 1 level of Ctrl-Z.  It also uses colour to stylise your text, making your HTML much more readable.


And that's about it.  Light years in front of Notepad and surprisingly  very useful.  But it by default will not point out any errors in your HTML.

There are plugins available but they seem to be in development so I moved on from here.

Sublime Text


I'll be completely honest here - I started using Sublime Text because I liked the dark theme :)
It also colour codes your, um, code, and has linter plug-ins to help style your code. Also lite and quick to load.  

Atom Editor

But forget all those - go for the Atom Editor (with Program IO plug in). 


With a large community developing many plug-ins there is just massive levels of support. 
  • Atom Beautify
    • Will (on save) fix any indenting and formatting issues in your file
  • Pigments
    • Will display HTML colours in line while you are editing 
  • Linter
    • Will use the W3Schools *validator* and point out errors in your code (more on this later)

Chrome Developers Tools

The Chrome Developers Tools are a Godsend when it comes to debugging your HTML pages.

Take for example this 'settings' screen I was developing, and try to ignore it's ugliness for now.  Whilst it loads fine, the save button didn't work (you have to scroll down to see it) and I had no idea why.  It also isn't a great representation as to how it will look on a mobile device.


Pressing 'F12' brings up the developers view, and the Console tab, for now it's blank - so there were no errors loading this page.  So far, so good.


You'll also notice that it's been reformatted to show how it would look on a mobile device.  


In case it hasn't you can click the 'Toggle Device' toolbar icon circled in red above to change the view. I'll come back to the Console tab in a bit.

If you select the Elements tab, you can poke around at your HTML and see the code that each block effects. 


You can then highlight sections of your displayed page and work out what's going wrong there.  It's handy when you drop a </div> statement.

Back to the console.  In this example I wanted to save the settings but the SAVE button wasn't working.  The Console tab is useful here - looking at the error message I can see that I'm calling buttonReset in my JavaScript but it's looking like I've done something wrong there, so I can focus my attention there. 


But the Console tab is much more useful than that.  If you want to trace progress in your script, in a similar manner to using Serial.printf statements in your Arduino code, you can use  console.log('text to show') statements to output text to the Console.  Very handy.

Anyone reading this who has any experience would probably already know about this, but I didn't and I hope by sharing this, someone out there can benefit from this info quickly.

HTML (and Javascript) Validation

I'd love someone to show my there's a better way to develop HMTL, but for me I'm simply using the ATOM IDE (heck, my helloworld.html was written in notepad!) and then reloading the page in Chrome to test it.

But, thankfully, Chrome (and other browsers) are pretty tolerant of mal-formed HTML.  This partly explains why some browsers display the same page differently!

The way forward I found was to use the W3C Markup Validator.  It's online and you can simply upload your html file and get a report on what's wrong.

For example, this 'Home Page' of mine works and looks fine, with no errors reported in the console log (ignore the comments around parsing data - I'll get onto websockets later):



However, when I run it through the validator I get the following report:


Some minor issues to clean up - but otherwise completely hidden to me, and who knows how they will bite you later!

Coming back full circle when talking about text editors prior, what's even better is that there's a plug-in for the Atom Editor - W3C-Validation that makes the process easier.  I used the 'Check on Save (default) option'.  For the example above, the results are as follows:



Looking at the errors, there's an unclosed <div> statement on line 192.  By placing the cursor on the next <div> statement, Atom's highlighting shows this statement is closed on line 210 (by the </div> statement) so I simply need an extra </div> on line 211.



So as you write your code, both HTML and Javascript, the Linter plug in will show you errors.  Fantastic.  I'm embarrassed to admit I've only started with Linter in the last few months, and do think that I'd have completed my project sooner if I started using it earlier!

Managing Data

GET Vs Ajax

Just about every introductory tutorial out there teaches you to type in commands through the address bar (when testing).

This is fine to get things up and running quickly, and the next step is typically making HTML calls when pressing a button on a webpage.

This seems fine, but has an annoying drawback:

Cache.  When you next go to type an address into your address bar to load your application, the auto-complete feature will populate the whole address.

You attempt to load the home page and end up toggling an IO.

For example, you type "192.168.1.1" and miss that the autocomplete has suggested "192.168.1.1/GPIO0_ON" and you hit enter.  Annoying.

This is known as the GET Method and was fine in the early days of HTML.

The issue here is that when you assign a link to a button on a form, you send a GET message and it's also cached in the address bar.  Back to square one.

However, AJAX handles this in the background for you!

Using Ajax

AJAX allows web pages to be updated asynchronously, effectively by sending GET messages in the background. 

This post was where I learnt how to format a request in Javascript.

Firstly - declare a function in your script, similar to this:

1
2
3
4
5
6
function requestIO(selectButton) { 
  request = new XMLHttpRequest();
  requestData = "output_" + selectButton;
  request.open("GET", requestData, true);
  request.send(null);
 }

Then, call the code from your button:

1
<button id="b1" type="button" class="btn btn-default btn-lg btn-block" onclick="requestIO(1);">BUTTON 1</button>

When your button is pressed, your code is called and your request is passed using AJAX, and never appears in the address bar. Win!  Also, if you do insist on manually entering an address, it will still work.  Nice.

This will let you toggle IO on your device relatively easily, even send form data.  In short, once you are at this point, you can basically control IO on your ESP at will.  However, the following makes things much more manageable and dynamic!

JSON - Learn to Love It!

If, like me, once you have a hammer and everything looks like a nail, you might be tempted to forge ahead with implementing your own solutions for more advanced functions in your devices. For example, you may add a temperature sensor and have an options page where you can choose between displaying degrees in Celcius or Farenheit.

The temptation might be to set up a _very long_ GET message and then use Arduinos' built in string compare tools to pick out the data you need.

Perhaps something like this:


1
2
3
4
5
6
function sendOptions() {
  request = new XMLHttpRequest();
  requestData = "options_temperatureChoice_C";
  request.open("GET", requestData, true);
  request.send(null);
 }

Then search for it with something like this:


1
2
3
4
5
6
7
8
index_start = request.indexOf("temperatureChoice");
    if (index_start == -1 ) {
      Serial.println("Temperature Choice data not found");
      return 95; // change to page type that says Browser Data save error, retry
    }
    String newTempChoice = request.substring(index_start + 11, index_start + 12);
    Serial.print("NewTempChoice: ");
    Serial.println(newTempChoice);


Like I did.  Dammit.


This takes many lines of code and while effective it's a pain in the butt to manage.

Then, some times in life, something magical happens.  Your mechanic calls you and tells you it's just a blocked oil filter and you don't need your engine rebuilt.  Your son or daughter is born.  You discover JSON (and it's Arudino implementation with working examples).

What this means for you, is that you simply need to pass a JSON formatted text string and the JSON libraries deal with the extraction of the data.  At a machine code level, it's probably no more (or less) efficient than manually parsing strings as above, but it is a lot less headaches to manage.

Taking our temperature choice example, to send the data using the JSON format you'd construct a string as follows:


1
2
3
{
 "temperatureChoice": "C"
}

and to read it you'd parse the JSON object:


1
JsonObject& root = jsonBuffer.parseObject(json);

then to read the value (in your Arduino sketch) you need:


1
temperatureChoice = root["temperatureChoice"];

The above is a very simplified overview, and in practice takes a little setup as per the examples here:
If you want to see how I did it you can view my source code here.

In summary the following is all I needed to read all my options from the settings file:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
bool loadConfig() {
  File configFile = SPIFFS.open("/config.json", "r");
  if (!configFile) {
    Serial.println("Failed to open config file");
    return false;
  }
  Serial.println("Opened config file successfully");
  size_t size = configFile.size();
  if (size > 1024) {
    DEBUG_PRINTLN("Config file size is too large");
    return false;
  }
  DEBUG_PRINT("Congif file size is: "); DEBUG_PRINTLN(size);
  // Allocate a buffer to store contents of the file.
  std::unique_ptr<char[]> buf(new char[size]);              // buffer is named 'buf' and is set to the size of the opened json file
  configFile.readBytes(buf.get(), size);                    // read bytes from congfigFile (read from SPIFFs) into the file buffer named 'buf'
  //  DynamicJsonBuffer jsonBuffer;
  DynamicJsonBuffer jsonBuffer;
  JsonObject& json = jsonBuffer.parseObject(buf.get());     // make a JasonObject of name "json" to store the file contents read from
  if (!json.success()) {
   Serial.println("Failed to parse config file");
    return false;
  }
  uint8_t modeArraySize =     json["buttonMode"].size();
  uint8_t pwmArraySize =      json["buttonPWM"].size();
  json["Version"].as<String>().toCharArray(gdiVersion, 30);
  json["SSID"].as<String>().toCharArray(ssid, 30);
  json["Password"].as<String>().toCharArray(password, 30);
  json["hostName"].as<String>().toCharArray(hostName, 30);
  json["temperatureChoice"].as<String>().toCharArray(temperatureChoice, 30);
  json["APName"].as<String>().toCharArray(apSSID, 30);
  json["APPass"].as<String>().toCharArray(apPASS, 30);
  const char* tempName = "x";
  for (int i = 0; i < modeArraySize; i++) {
    buttons[i].mode       = json["buttonMode"][i];  // choose mode of input buttons,   0 = door, 1 = light
    inputs[i].mode        = json["inputMode"][i];   // choose mode of input terminals, 0 = door, 1 = light
    tempName              = json["buttonNames"][i];
    htmlButtonNames[i]    = tempName;
    buttons[i].show       = json["show"][i];
    buttons[i].onTime     = json["onTime"][i];

    inputs[i].onTime      = json["onTime"][i];
  }
  for (int i = 0; i < modeArraySize; i++) {
    DEBUG_PRINT("IO Config button "); DEBUG_PRINT(i + 1); DEBUG_PRINT(" is "); DEBUG_PRINTLN(buttons[i].mode);
  }
  Serial.println("IOConfig data read");
  for (int i = 0; i < modeArraySize; i++) {
    DEBUG_PRINT("IO Input ");         DEBUG_PRINT(i + 1); DEBUG_PRINT(" is "); DEBUG_PRINTLN(inputs[i].mode);
  }
  for (int i = 0; i < modeArraySize; i++) {
    DEBUG_PRINT("Button name "); DEBUG_PRINT(i + 1); DEBUG_PRINT(" is "); DEBUG_PRINTLN(htmlButtonNames[i]);
  }
  for (int i = 0; i < modeArraySize; i++) {
    DEBUG_PRINT("Button Show "); DEBUG_PRINT(i + 1); DEBUG_PRINT(" is "); DEBUG_PRINTLN(buttons[i].show);
  }
  for (int i = 0; i < modeArraySize; i++) {
    DEBUG_PRINT("Button onTime "); DEBUG_PRINT(i + 1); DEBUG_PRINT(" is "); DEBUG_PRINTLN(buttons[i].onTime);
  }
  DEBUG_PRINTLN("Reading PWM Values from JSON File");
  DEBUG_PRINT("PWM Array size is "); DEBUG_PRINTLN(pwmArraySize);
  for (int i = 0; i < pwmArraySize; i++) {
    panelButtonPWM[i] = json["buttonPWM"][i];  // Read PWM Value for button illumination, 0 - 1024
    DEBUG_PRINT("Button PWM_"); DEBUG_PRINT(i); DEBUG_PRINT(" is "); DEBUG_PRINTLN(panelButtonPWM[i]);
  }
  tempSlope     = json["tempSlope"];
  DEBUG_PRINT("Temperature Slope Calibration is: "); DEBUG_PRINTLN(tempSlope);
  tempIntercept = json["tempIntercept"];
  DEBUG_PRINT("Temperature Slope Intercept is: "); DEBUG_PRINTLN(tempIntercept);
  return true;
}

And there's a lot there - but it's much easier to manage than trying to search longs strings of data!

Same for saving data:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//******************************************************************************************************************************
// Save settings to config file
//******************************************************************************************************************************
bool saveConfig() {
  DynamicJsonBuffer jsonBuffer;
  JsonObject& rootSave = jsonBuffer.createObject();
  String tempString = "";
  rootSave["Version"]   = gdiVersion;
  rootSave["SSID"]      = ssid;
  rootSave["Password"]  = password;
  rootSave["hostName"]  = hostName;
  rootSave["temperatureChoice"] = temperatureChoice;
  rootSave["APName"]    = apSSID;
  rootSave["APPass"]    = apPASS;
  Serial.println("Save Config creating nested array's");
  JsonArray& buttonMode   = rootSave.createNestedArray("buttonMode");
  JsonArray& inputMode    = rootSave.createNestedArray("inputMode");
  JsonArray& buttonNames  = rootSave.createNestedArray("buttonNames");
  JsonArray& show         = rootSave.createNestedArray("show");
  JsonArray& onTime       = rootSave.createNestedArray("onTime");
  JsonArray& buttonPWM    = rootSave.createNestedArray("buttonPWM");
  Serial.println("Save Config saving temp cal numbers");
  rootSave["tempSlope"]       = tempSlope;
  rootSave["tempIntercept"]   = tempIntercept;
  Serial.println("Save Config adding button modes.");
  for (int i = 0; i < 4; i++) {
    buttonMode.add(buttons[i].mode);
    inputMode.add(inputs[i].mode);
    buttonNames.add(htmlButtonNames[i]);
    show.add(buttons[i].show);
    onTime.add(buttons[i].onTime);
  }
  Serial.println("Save Config adding PWM data");
  for (int i = 0; i < 2; i++) {
    buttonPWM.add(panelButtonPWM[i]);
  }
  File configFile   = SPIFFS.open("/config.json", "w");
  Serial.println("Save Config opening SPIFFS File");
  //  File configFile   = SPIFFS.open("/test.json", "w"); // this is for testing
  if (!configFile) {
    Serial.println("Failed to open config file for writing");
    return false;
  }
  rootSave.printTo(configFile);
  Serial.println("Config Saved.");
  return true;
}

So far, this description has focussed on using files from spiffs.  But what about transferring data between the Server and the Client?

Run an Websocket Server on your Desktop

Wait, what?

Websockets, for transferring data between your webpage and your server are excellent and easy.

As the name suggests, you can open a socket between the server and client and share data.

Following this chat-sever tutorial I was able to set up a  Node.js server on my desktop (yes you have to install Node.js first).

My version can be found here and I simplified it a bit to help with my development.

How it Works:  Sever Side

In short, my version:

  • Receives a websocket packet, and checks to see if it's of type 'request'
  • If a request, it checks what the 'data' value is and then
  • Opens a .json file that has the same name as the data value and then
  • Sends the data in the file back to the requester
  • The requester in this case is typically the client (webpage) you are developing on your desktop. 
The end goal will be to have the ESP8266 host your web socket server and respond to data requests, but while developing the compile / upload cycle in the Arduino IDE can suck up a lot of time.  This way you simply add a .json file to your working folder and you can test changes in your page.
Yes, I've not yet explained how to use websockets in your html pages - bear with me I'll get here.

Working again with our temperature example, the client (webpage) sends a request using the following .json string:


1
2
3
{ "type": "request",
 "data": "configTemperature"
}

The socketserver.js file I'm running will first check if the 'type' is 'request' and if it is, it will then read what the 'data' is, and in this case check if the file 'configTemperature.json' is present. Then send the contents of that file back to the client.  Nice.

How It Works: Client Side

Ok, thanks for sticking with me this far.  Now it's time to discuss how to use Websockets in HTML pages. 

Setting Up the Websocket, and Choosing Where to Connect

1. You need to use Javascript.  I won't go into it here, but if you need to learn how to use it there are many great resources out there.

2. Set up the Websocket in your page with the following script:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<script language="javascript" type="text/javascript">
    //**************************************************************************
    // setup Websockets
    //**************************************************************************
    // use below when hosted on ESP8266
    var wsUri = 'ws://' + location.hostname + ':81/';
    // use below for connecting to ESP server via IP address (when testing code on PC)
    // var wsUri = 'ws://192.168.1.111:81/';
    // use below for connecting to Nodejs server (when testing code on PC)
    // var wsUri = 'ws://localhost:80/';
    var output;
    var echo = "";
    var requestMsg = []; // array of messages to be sent out the websocekt.

Note that I've got a few options in this setup:

  • The uncommented line is used when connecting to a Websocket server on the ESP8266 itself, and the webpage is hosted on there also (file in SPIFFS).  Good for final product, but makes for time consuming testing.
  • The commented out line with the hard coded IP address is for connecting to a Websocket server on the ESP8266 a you're testing your webpage on your PC.  Quite nifty, if your ESP8266 code is mature and you're developing your webpage (good is you're reading and displaying live sensor data for example).
  • Lastly, if you're running a Websocket server on your desktop, and testing your webpage on your PC, the last line uses the localhost reference to connect to your websocket server.
You'll also see that in two case I use port 81, and in one case I use port 80.  Why?  Because that's the way it was shown to me, and I don't know if there are any advantages using other ports.  But hey, it works!

Now Open It!

After setting up the Websocket, you need to open it, like this:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function init() {
      console.log("Initiating Home page");
      openWebsocket();
    }
    //**************************************************************************
    // Open Websockets
    // As Websocekts are asynchronous, when first opening messages may be sent
    // to a que to be processed once the readyState is = 1 (opened)
    // this will send all queued messages once the socket is opened.
    //**************************************************************************
    function openWebsocket() {
      var message = "";
      websocket = new WebSocket(wsUri);
      websocket.onopen = function() {
        console.log("Websocket opened.");
        while (requestMsg.length > 0) {
          message = requestMsg.pop();
          console.log("Popping message ", message, " from queue.");
          websocket.send(message);
        }
        console.log("Message queue now empty.");
      }
    }

Once open, as per the comments, this code will check for queued messages and send them out if needed.

Query for Messages

The following code processes Websockets when received


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    //**************************************************************************
    // Query Websockets
    // Take action as required on webSocket
    // Either queues or sends messages based on readyState of the socket
    //**************************************************************************
    function queryWebSocket(request) {
      if (websocket.readyState == 3) {
        console.log("Websocket not open, opening.")
        openWebsocket();
      }
      console.log("Requesting: ", request);
      if (websocket.readyState !== 1) {
        requestMsg.push(request);
        console.log("request pushed to array");
      } else {
        websocket.send(request);
        console.log("request sent to websocket");
      }
      websocket.onclose = function(evt) {
        console.log("Websocket disconnected");
      };
      websocket.onmessage = function(evt) {
        console.log("Websocket message received.")
        onMessage(evt);
      };
      websocket.onerror = function(evt) {
        console.log("Websocket Error! " + evt.data);
      };
    }

So how does this actually work?    WebSocket is a built-in Javascript class, and websocket is a variable of that class.  The .readyState, .send, .onClose and .onMessage are functions called when required.   For example, when a websocket message comes in, websocket.onmessage is active data (evt) is passed in, and is the passed to the function onMessage.

Processing Received Messages

In the example below, as I'm only expecting JSON formatted messages now, I parse it and if the 'echo' (jsonMessage.echo) is 'getEnvironmentals' I get on with business, otherwise throw an error  message.  And yes, I need to learn how to spell 'received'....


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    function onMessage(evt) {
      var messageRecieved = evt.data;
      var jsonMessage = JSON.parse(messageRecieved);
      console.log("Message Recieved is: " + messageRecieved);
      if (jsonMessage.echo == "getEnvironmentals") {
        console.log("getting environmentals");
        displayEnvironmentals(jsonMessage);
      } else {
        console.log("ERROR: Unknown message received.")
      }
    }

In the above example I look for 'echo' as it's my reply to any 'requests' I send.  You can make up your own rules.

How It Works: Server Side

On the server (ESP8266) side there's many tutorials on how to set up your websocket server so I'll skip over those details, and jump into my example directly.

Start with an Example

This example is a good place to start:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/*
 * WebSocketServer.ino
 *
 *  Created on: 22.05.2015
 *
 */

#include <Arduino.h>

#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <WebSocketsServer.h>
#include <Hash.h>

ESP8266WiFiMulti WiFiMulti;

WebSocketsServer webSocket = WebSocketsServer(81);

#define USE_SERIAL Serial1

void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {

    switch(type) {
        case WStype_DISCONNECTED:
            USE_SERIAL.printf("[%u] Disconnected!\n", num);
            break;
        case WStype_CONNECTED:
            {
                IPAddress ip = webSocket.remoteIP(num);
                USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload);
    
    // send message to client
    webSocket.sendTXT(num, "Connected");
            }
            break;
        case WStype_TEXT:
            USE_SERIAL.printf("[%u] get Text: %s\n", num, payload);

            // send message to client
            // webSocket.sendTXT(num, "message here");

            // send data to all connected clients
            // webSocket.broadcastTXT("message here");
            break;
        case WStype_BIN:
            USE_SERIAL.printf("[%u] get binary length: %u\n", num, length);
            hexdump(payload, length);

            // send message to client
            // webSocket.sendBIN(num, payload, length);
            break;
    }

}

void setup() {
    // USE_SERIAL.begin(921600);
    USE_SERIAL.begin(115200);

    //Serial.setDebugOutput(true);
    USE_SERIAL.setDebugOutput(true);

    USE_SERIAL.println();
    USE_SERIAL.println();
    USE_SERIAL.println();

    for(uint8_t t = 4; t > 0; t--) {
        USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t);
        USE_SERIAL.flush();
        delay(1000);
    }

    WiFiMulti.addAP("SSID", "passpasspass");

    while(WiFiMulti.run() != WL_CONNECTED) {
        delay(100);
    }

    webSocket.begin();
    webSocket.onEvent(webSocketEvent);
}

void loop() {
    webSocket.loop();
}


In the above, once set up, the webSocket.loop(); call processes websocket actions when needed.

On a websocket event - the following code runs:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//******************************************************************************************************************************
// Websockets Events
//******************************************************************************************************************************
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {

  switch (type) {
    case WStype_DISCONNECTED:                     // Add code here to execute if disconnected
      DEBUG_PRINTLN("Websocket disconnected");
      break;
    case WStype_CONNECTED:                        // Add code here to execute when connected
      {
        //getConfig();
        IPAddress ip = webSocket.remoteIP(num);   // Get ip address of connection
        Serial.print("Websocket IP Address is: "); Serial.println(ip);
      }
      break;
    case WStype_TEXT:                             // Execute code here to match text etc
      {
        String text = String((char *) &payload[0]);
        Serial.print("WStype_TEXT recieved was: ");
        Serial.println(text);
        parseWebsocketText(text, num);
      }
      break;
    case WStype_BIN:
      DEBUG_PRINT("WStype_BIN recieved.");
      hexdump(payload, length);                 // echo data back to browser
      webSocket.sendBIN(num, payload, length);
      break;
  }
}

As I'm passing data using JOSN files I look to see if text was recieved, and if so process it in the following manner:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//******************************************************************************************************************************
//  Parse rx's websocket text and check if it's a valid request
//******************************************************************************************************************************
void parseWebsocketText(String text, uint8_t num) {
  Serial.println("Parseing Web Socket.");
  String message = "{\"echo\":\"Invalid Message Received.\"}";
  DynamicJsonBuffer jsonBuffer;
  JsonObject& jsonData = jsonBuffer.parseObject(text);
  const char* type = jsonData["type"];
  if ((jsonData["type"]) == "request") {
    if ((jsonData["data"]) == "configTemperature") {
      message = sendTemperature();
    } else if ((jsonData["data"]) == "configButtons") {
      message = sendButtonConfig();
    } else if ((jsonData["data"]) == "configButtonPWM") {
      message = sendButtonPWM();
    } else if ((jsonData["data"]) == "localName") {
      message = sendLocalName();
    } else if ((jsonData["data"]) == "listSSIDs") {
      message = sendSSID();
    } else {
      Serial.println("Unknown JSON Type");
    }
  } else if ((jsonData["type"]) == "saveNetworkData") {
    message = saveNetworkConfig(jsonData);
  } else if ((jsonData["type"]) == "saveButtons") {
    message = saveButtonConfig(jsonData);
  } else if ((jsonData["type"]) == "saveTemperature") {
    message = saveTemperatureConfig(jsonData);
  } else if ((jsonData["type"]) == "saveButtonPWM") {
    message = saveButtonPWMConfig(jsonData);
  }
  Serial.println("Sending Websocket Message:");
  Serial.println(message);
  webSocket.sendTXT(num, message.c_str(), message.length());  // Send update data
}

You can of course tailor this to your own requirements.

Wrapping Up

Okay, if you've made it hits far, thanks!

Granting a Wish

So, me from 4 years ago was trying to get started with a  practical working example to control GPIO from a webpage, so here it is!




This example:
  1. Uses Twitter Bootstrap theme to make it look half decent from the get go
  2. Sends AJAX requests to control GPIO
  3. Uses websockets to determine the current state of a GPIO with json formatted requests
  4. Also shows an error message if the websocket cannot connect
  5. Throws up a 404 not found page

If you start with a WeMos D1 Mini, you can be up and running in just a few minutes. You're welcome, past me!

(That said, there's a lot in there - I will go into details about this later!)

And, yes, I can now open my garage door from my phone :)

More on that here. Enjoy!


15 comments:

  1. Excellent! Thanks for all the tips this'll definitely make the learning curve much faster. Thanks!

    ReplyDelete
  2. Thanks a lot for the share!.. Wonderful source of ESP8266 Knowledge

    ReplyDelete
  3. Thank you so much for sharing of your experiences and knowledge!!

    ReplyDelete
  4. Thanks for the great write up! Particularly enjoyed the websocket portion.
    Re your don't fit your html inline in your code. If you really want to do it you can use C++ string literals using R"()" or escape it a bit more like this
    const char *htmlContent = R""""()"""";

    ReplyDelete
  5. Thanks for tutorial.
    Install Atom Live Server to automatically reload your webpages when developing. You'll wonder how you ever lived without it.

    ReplyDelete
  6. Thanks! Learning a lot here!!

    ReplyDelete
  7. Well, this is just a cute web development for starters guide. I'm sure that after 4 years of development with ESP you have much more experience with the device and can talk a bit more about it.
    I came here for that and learned practically nothing \=

    ReplyDelete
    Replies
    1. How rude. I'm sure that Ludzinc is sorry for wasting your time.

      Delete
    2. Got any particular questions? Happy to address if I know. Did you look at the Software example I linked above?

      Delete
    3. Didn't try to be rude. it's just that every web developer would learn very little from this, so it's not really an ESP guide, it's more of a web development introduction.

      Things I would happy to hear about your experience with ESP:
      Power supply and battery usage, sleep modes, network reconnect methods, great libraries, OTA, easy http logging, differences from arduino, debugging, limitations etc.
      I'm sure you have a lot more knowledge in the H/W side, this is why I'm saying that.

      Delete
    4. I am sure there are people who know everything about the subjects you mention such as OTA and sleep modes but still learned here about web sockets and AJAX. It's kinda like following a car mechanics course and complain you didnt learn about upholstery

      Delete
  8. Thanks for this. I'm checking out the Atom editor.

    Another route for working with ESP8266 is a lot quicker and allows you to integrate with many other devices. Check out home-assistant.io along with ESPhome.io Both are open source and widely supported.

    ReplyDelete
  9. This is not an "ESP8266" development post as the title claims, but rather a Node MCU post.

    It's like you would write a post titled "ATmega Development" and inside recommend to use an Arduino.

    ReplyDelete
  10. Great content, thanks for sharing your experience. Very helpful.

    ReplyDelete