Interface & Application


This week’s focus is on interface and application. Building upon the progress made in the previous three weeks, which involved input ,output devices and networking, I have enhanced the design of the web interface for the smart dog bowl. As part of this update, I have incorporated two buttons—one for resetting all values and another for taring the weight scale.

The data displayed on the webpage is dynamically updated by modifying the JSON file transmitted from the XIAO device.


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

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-name";
const char* password = "wifi-psw";

// 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><!-- color of the background --><!-- <body style='background-color: #EEEEEE;'> --><body style='background-image: linear-gradient(to right,#ffffff, #000000); text-align: center;'><!-- color of the fonts --><span style='color: rgb(210, 213, 17); display: block;'> <h1 style='font-size: 80px;'>Shiba Scope</h1><p style='font-size: 30px;'>Water Remained: <span id='water'>-</span> [ml]</p><p style='font-size: 30px;'>Water Intake: <span id='sum'>-</span> [ml]</p><p><button type='button' id='BTN_RST_ALL' style='margin: auto; display: block; background-color: #FFFF00; color: #000000; padding: 10px 20px; border: none; cursor: pointer; font-size: 20px;'>Reset ALL</button></p><p><button type='button' id='BTN_SCALE' style='margin: auto; display: block; background-color: #FFFF00; color: #000000; padding: 10px 20px; border: none; cursor: pointer; font-size: 20px;'>Reset the scale</button></p></span></body><script> var Socket; document.getElementById('BTN_RST_ALL').addEventListener('click', button_reset_all); document.getElementById('BTN_SCALE').addEventListener('click', button_reset_scale); function init() { Socket = new WebSocket('ws://' + window.location.hostname + ':81/'); Socket.onmessage = function(event) { processCommand(event); }; } function button_reset_all() { var msg = {command: 1,};Socket.send(JSON.stringify(msg)); } function button_reset_scale() { var msg = {command: 0,};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.print("Received command from user: ");
        if(g_command == 0){
          Serial.println("Reset the scale ");
          // reset the weight scale
          scale.tare();
        }
        else if(g_command == 1){
          Serial.println("Reset All (the scale and water intake)");
          // 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>
<!-- color of the background -->
<!-- <body style='background-color: #EEEEEE;'> -->
<body style='background-image: linear-gradient(to right,#ffffff, #000000); text-align: center;'>

<!-- color of the fonts -->
<span style='color: rgb(210, 213, 17); display: block;'> 

<h1 style='font-size: 80px;'>Shiba Scope</h1>
<p style='font-size: 30px;'>Water Remained: <span id='water'>-</span> [ml]</p>
<p style='font-size: 30px;'>Water Intake: <span id='sum'>-</span> [ml]</p>
<p><button type='button' id='BTN_RST_ALL' style='margin: auto; display: block; background-color: #FFFF00; color: #000000; padding: 10px 20px; border: none; cursor: pointer; font-size: 20px;'>
Reset ALL
</button></p>
<p><button type='button' id='BTN_SCALE' style='margin: auto; display: block; background-color: #FFFF00; color: #000000; padding: 10px 20px; border: none; cursor: pointer; font-size: 20px;'>
Reset the scale
</button></p>

</span>

</body>
<script>
  var Socket;
  document.getElementById('BTN_RST_ALL').addEventListener('click', button_reset_all);
  document.getElementById('BTN_SCALE').addEventListener('click', button_reset_scale);

  function init() {
    Socket = new WebSocket('ws://' + window.location.hostname + ':81/');
    Socket.onmessage = function(event) {
      processCommand(event);
    };
  }
  function button_reset_all() {
    var msg = {
	command:  1,
	};
	Socket.send(JSON.stringify(msg));
  }
  function button_reset_scale() {
    var msg = {
	command:  0,
	};
	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>
Date 22 November 2023