Networking and communications


This week’s theme is networking and communications, a continuation of the last two weeks’ exploration of input and output devices. In my project, I took a simple dog bowl and transformed it into a smart device capable of recording the weight of food and displaying the information on a 0.96” OLED screen. To elevate the project further, I used the WiFi module on the XIAO ESP32S3. With STA mode, I set up a dedicated website for this project, allowing real-time updates of the recorded food/water weight and consumption data.x


Hardware:

  • XIAO ESP32S3 (originally XIAO RP2040, but for WiFi connectivity in the upcoming weeks, I switched to XIAO ESP32S3)
  • Load Cell Weight Sensor
  • AD HX711 Weighing Pressure Module
  • Logic Level Converter Bi-Directional 3.3V-5V Shifter Module
  • 0.96” OLED Display

Library:

  • WiFi.h
  • WebServer.h
  • WebSocketsServer.h
  • ArduinoJson.h
  • HX711.h
  • U8g2lib.h
  • Wire.h

Network Time Protocol

NTP stands for Network Time Protocol, which is a standard Internet Protocol (IP) used for synchronizing computer clocks over a network. Before I attempted to set up a website for updating weight data, I initialized the NTP client to obtain the date and time from the NTP server. This step ensured that my project had accurate time data.


Challenge 1 - WiFi Connection:

One of the initial challenges I encountered was getting the WiFi module to connect to MIT WiFi. It turned out that it required a 2.4GHz WiFi source instead of 5GHz. To work around this, I used my iPhone hotspot as a WiFi source. However, I had to make a special setup to ensure that the hotspot operated at 2.4GHz.

To switch to 2.4GHz on the iPhone:

  1. Open Settings and tap Personal Hotspot.
  2. Toggle on Maximize Compatibility.

Challenge 2 - Updating Data in Real-Time:

Initially, I sent a random number to the website, but I soon realized that this data needed to be refreshed to retrieve new values. To achieve real-time data updates, I turned to a YouTube tutorial that explained how to use WebServer.h and WebSocketsServer.h to establish a seamless flow of real-time data updates.


Challenge 3 - Multi-Tasking:

My project required multi-tasking, involving reading sensor data from a load cell, displaying the value on the OLED screen, and updating data on the web server. Initially, I attempted to use the “scheduler.h” library, but it proved incompatible with the ESP32 architecture. As an alternative, I employed the “millis()” function as a timer to perform periodic tasks.


Code (for XIAO ESP32S3):

  //--- WiFi and Webserver
  #include <WiFi.h>                                     // needed to connect to WiFi
  #include <WebServer.h>                                // needed to create a simple webserver (make sure tools -> board is set to ESP32, otherwise you will get a "WebServer.h: No such file or directory" error)
  #include <WebSocketsServer.h>                         // needed for instant communication between client and server through Websockets
  #include <ArduinoJson.h>                              // needed for JSON encapsulation (send multiple variables with one string)

  // SSID and password of Wifi connection:
  const char* ssid = "wifi-ssid";
  const char* password = "password";

  // The String below "webpage" contains the complete HTML code that is sent to the client whenever someone connects to the webserver
  //String webpage = "<!DOCTYPE html><html><head><title>Shiba Scope</title></head><body style='background-color: #EEEEEE;'><span style='color: #003366;'><h1>Shiba Scope</h1><p>Water: <span id='rand1'>-</span> [ml]</p><p>Kibble: <span id='rand2'>-</span> [g]</p><p><button type='button' id='BTN_SEND_BACK'>Send info to ESP32</button></p></span></body><script> var Socket; document.getElementById('BTN_SEND_BACK').addEventListener('click', button_send_back); function init() { Socket = new WebSocket('ws://' + window.location.hostname + ':81/'); Socket.onmessage = function(event) { processCommand(event); }; } function button_send_back() { var msg = {brand: 'Gibson',type: 'Les Paul Studio',year: 2010,color: 'white'};Socket.send(JSON.stringify(msg)); } function processCommand(event) {var obj = JSON.parse(event.data);document.getElementById('rand1').innerHTML = obj.rand1;document.getElementById('rand2').innerHTML = obj.rand2; console.log(obj.rand1);console.log(obj.rand2); } window.onload = function(event) { init(); }</script></html>";
  String webpage = "<!DOCTYPE html><html><head><title>Shiba Scope</title></head><body style='background-color: #EEEEEE;'><span style='color: #003366;'><h1>Shiba Scope</h1><p>Water Remained: <span id='water'>-</span> [ml]</p><p>Water Intake: <span id='sum'>-</span> [ml]</p><p><button type='button' id='BTN_SEND_BACK'>Reset the bowl</button></p></span></body><script> var Socket; document.getElementById('BTN_SEND_BACK').addEventListener('click', button_send_back); function init() { Socket = new WebSocket('ws://' + window.location.hostname + ':81/'); Socket.onmessage = function(event) { processCommand(event); }; } function button_send_back() { var msg = {command: 1,};Socket.send(JSON.stringify(msg)); } function processCommand(event) {var obj = JSON.parse(event.data);document.getElementById('water').innerHTML = obj.water;document.getElementById('sum').innerHTML = obj.sum; console.log(obj.water);console.log(obj.sum); } window.onload = function(event) { init(); }</script></html>";

  // The JSON library uses static memory, so this will need to be allocated:
  // -> in the video I used global variables for "doc_tx" and "doc_rx", however, I now changed this in the code to local variables instead "doc" -> Arduino documentation recomends to use local containers instead of global to prevent data corruption

  // We want to periodically send values to the clients, so we need to define an "interval" and remember the last time we sent data to the client (with "previousMillis")
  int interval = 1000;                                  // send data to the client every 1000ms -> 1s
  unsigned long previousMillis = 0;                     // we use the "millis()" command for time reference and this will output an unsigned long

  // Initialization of webserver and websocket
  WebServer server(80);                                 // the server uses port 80 (standard port for websites
  WebSocketsServer webSocket = WebSocketsServer(81);    // the websocket uses port 81 (standard port for websockets

  //---- button
  uint8_t buttonPin = D6;
  int buttonState = 0;

  //---- sensor
  #include "HX711.h"
  HX711 scale;
  uint8_t dataPin = D3;
  uint8_t clockPin = D7;
  float w1, w2, previous = 0;
  float tolerance = -1.5;
  float delta = 0.0;
  float sum = 0.0;

  //---- OLED
  #include <U8g2lib.h>
  #ifdef U8X8_HAVE_HW_SPI
  #include <SPI.h>
  #endif
  #ifdef U8X8_HAVE_HW_I2C
  #include <Wire.h>
  #endif
  U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /* clock=*/ SCL, /* data=*/ SDA, /* reset=*/ U8X8_PIN_NONE);

  void setup() {
    Serial.begin(115200);                               // init serial port for debugging
  
    //---- Button
    pinMode(buttonPin, INPUT);

    //---- scale senser set-up
    Serial.println(__FILE__);
    Serial.print("LIBRARY VERSION: ");
    Serial.println(HX711_LIB_VERSION);
    Serial.println();

    scale.begin(dataPin, clockPin);

    Serial.print("UNITS: ");
    Serial.println(scale.get_units(10));

    // load cell factor 20 KG
    // scale.set_scale(127.15);
    // load cell factor 5 KG
    // scale.set_scale(429.4845);       // TODO you need to calibrate this yourself.
    scale.set_scale(438.51);       // TODO you need to calibrate this yourself.
    scale.tare();

    Serial.print("UNITS: ");
    Serial.println(scale.get_units(10));     

    // OLED
    u8g2.begin();

    //---- WiFi
    WiFi.begin(ssid, password);                         // start WiFi interface
    Serial.println("Establishing connection to WiFi with SSID: " + String(ssid));     // print SSID to the serial interface for debugging
  
    while (WiFi.status() != WL_CONNECTED) {             // wait until WiFi is connected
      delay(500);
      Serial.print(".");
    }
    Serial.print("Connected to network with IP address: ");
    Serial.println(WiFi.localIP());                     // show IP address that the ESP32 has received from router
    
    server.on("/", []() {                               // define here wat the webserver needs to do
      server.send(200, "text/html", webpage);           //    -> it needs to send out the HTML string "webpage" to the client
    });
    server.begin();                                     // start server
    
    webSocket.begin();                                  // start websocket
    webSocket.onEvent(webSocketEvent);                  // define a callback function -> what does the ESP32 need to do when an event from the websocket is received? -> run function "webSocketEvent()"
  }

  void loop() {
    server.handleClient();                              // Needed for the webserver to handle all clients
    webSocket.loop();                                   // Update function for the webSockets 

    buttonState = digitalRead(buttonPin);
    buttonUpdater();
    weigh_function();
    oledUpdater();

    unsigned long now = millis();                       // read out the current "time" ("millis()" gives the time in ms since the Arduino started)
    if ((unsigned long)(now - previousMillis) > interval) { // check if "interval" ms has passed since last time the clients were updated
      
      String jsonString = "";                           // create a JSON string for sending data to the client
      StaticJsonDocument<200> doc;                      // create a JSON container
      JsonObject object = doc.to<JsonObject>();         // create a JSON Object
      object["water"] = int(w1);                            // write data into the JSON object -> I used "rand1" and "rand2" here, but you can use anything else
      object["sum"] = abs(int(sum));
      serializeJson(doc, jsonString);                   // convert JSON object to string
      Serial.println(jsonString);                       // print JSON string to console for debug purposes (you can comment this out)
      webSocket.broadcastTXT(jsonString);               // send JSON string to clients
      
      previousMillis = now;                             // reset previousMillis
    }
  }

  void webSocketEvent(byte num, WStype_t type, uint8_t * payload, size_t length) {      // the parameters of this callback function are always the same -> num: id of the client who send the event, type: type of message, payload: actual data sent and length: length of payload
    switch (type) {                                     // switch on the type of information sent
      case WStype_DISCONNECTED:                         // if a client is disconnected, then type == WStype_DISCONNECTED
        Serial.println("Client " + String(num) + " disconnected");
        break;
      case WStype_CONNECTED:                            // if a client is connected, then type == WStype_CONNECTED
        Serial.println("Client " + String(num) + " connected");
        // optionally you can add code here what to do when connected
        break;
      case WStype_TEXT:                                 // if a client has sent data, then type == WStype_TEXT
        // try to decipher the JSON string received
        StaticJsonDocument<200> doc;                    // create a JSON container
        DeserializationError error = deserializeJson(doc, payload);
        if (error) {
          Serial.print(F("deserializeJson() failed: "));
          Serial.println(error.f_str());
          return;
        }
        else {
          // JSON string was received correctly, so information can be retrieved:
          const int g_command = doc["command"];
          Serial.println("Received command from user: " + String(num));
          Serial.println("Command: " + String(g_command));
          if(g_command == 1){
            // reset the weight scale
            scale.tare();
            // reset the value of water consumption
            sum = 0;
          }
        }
        Serial.println("");
        break;
    }
  }

  void weigh_function(){
    // read until stable
    w1 = scale.get_units(10);
    //delay(100);
    w2 = scale.get_units(10);
    while (abs(w1 - w2) > 10)
    {
      w1 = w2;
      w2 = scale.get_units();
      delay(100);
    }

    Serial.print("UNITS: ");
    Serial.print(w1);
    if (w1 == 0)
    {
      Serial.println();
    }
    else
    {
      delta = w1 - previous;
      Serial.print("\t\tDELTA: ");
      Serial.print(delta);

      calculate_water();
      previous = w1;
    }
    //delay(100);
  }

  void calculate_water(){
    if (delta <= tolerance){
      sum += (w1 - previous);
    }
    else{}
    Serial.print("\t\tSum: ");
    Serial.println(sum);

  }

  void oledUpdater(){
    u8g2.clearBuffer();                   // clear the internal memory
    u8g2.setFont(u8g2_font_ncenB08_tr);   // choose a suitable font
    u8g2.drawStr(0,10,"HTM(A)A 2023");    // write something to the internal memory
  
    u8g2.drawStr(0,30,"Weight: "); 
    u8g2.setCursor(60, 30);
    u8g2.print(w1);

    u8g2.drawStr(0,40,"Sum: "); 
    u8g2.setCursor(60, 40);
    u8g2.print(sum);

    u8g2.drawStr(0,60,"IP: "); 
    u8g2.setCursor(40, 60);
    u8g2.print(WiFi.localIP());

    u8g2.sendBuffer();                    // transfer internal memory to the display
  }

  void buttonUpdater(){
    // check if the pushbutton is pressed. If it is, the buttonState is HIGH:
    if (buttonState == HIGH) {
      // reset the weight scale
      scale.tare();
    } else {
      // do nothing
    }
  }
  // -- END OF FILE --

Code (for the website):

  <!DOCTYPE html>
  <html>
  <head>
  <title>Shiba Scope</title>
  </head>
  <body style='background-color: #EEEEEE;'>

  <span style='color: #003366;'>

  <h1>Shiba Scope</h1>
  <p>Water Remained: <span id='water'>-</span> [ml]</p>
  <p>Water Intake: <span id='sum'>-</span> [ml]</p>
  <p><button type='button' id='BTN_SEND_BACK'>
  Reset the bowl
  </button></p>

  </span>

  </body>
  <script>
    var Socket;
    document.getElementById('BTN_SEND_BACK').addEventListener('click', button_send_back);
    function init() {
      Socket = new WebSocket('ws://' + window.location.hostname + ':81/');
      Socket.onmessage = function(event) {
        processCommand(event);
      };
    }
    function button_send_back() {
      var msg = {
    command:  1,
    };
    Socket.send(JSON.stringify(msg));
    }
    function processCommand(event) {
    var obj = JSON.parse(event.data);
    document.getElementById('water').innerHTML = obj.water;
    document.getElementById('sum').innerHTML = obj.sum;
      console.log(obj.water);
    console.log(obj.sum);
    }
    window.onload = function(event) {
      init();
    }
  </script>
  </html>

How to put .html into arduino.ino file

  1. Copy and paste the full html file to the HTML Compression Tool
  2. Press the “Compress HTML” button
  3. Copy your new smaller html code from the box below
  4. Insert the smaller html code into the arduino code

Conclusion:

This week’s exploration of networking and communications has allowed me to transform a simple dog bowl into a smart, connected device. Overcoming challenges related to WiFi connectivity, real-time data updates, and multitasking has been a valuable learning experience. As I continue to refine this project, I look forward to applying these newfound skills and knowledge to future endeavors. Stay tuned for more exciting developments on the “How To Make Almost Anything” website as I delve deeper into the world of making and innovation!

Date 15 November 2023