ESP32 Communication Protocol – Websocket

WebSocket adalah full-duplex messaging. Setelah proses HTTP handshake, node dapat menggunakan WebSocket communication, data berupa binary yang berjalan diatas TCP. TCP akan menjaga koneksi tetap terhubung dan dapat bertukar data secara real time. Semua modern web servers dan web browsers sudah mendukung WebSocket.

Pada aplikasi IoT, untuk kasus tertentu, kita dapat manfaatkan keunggulan WebSocket. Contoh, web-based dashboard dengan real time data dari IoT yang berada dibelakang firewall atau secure network. Menggunakan WebSocket connection merupakan cara termudah dan efektif untuk menangai traffic filters, karena HTTP umumnya dapat melewati firewalls.

Selain melakukan komunikasi langsung diatas WebSocket, kita juga dapat menjalankan application layer protocols diatas WebSocket. Contohnya MQTT atau CoAP.

Pada contoh project kita akan bahas penggunaan WebSocket saja. Tujuan dari project menjalankan web server pada ESP32 dengan WebSocket sebagai endpoint. Pada ESP32 akan digunakan sensor DHT11. Ketika client connects, kita dapat melakukan enable/disable readings dan melihat informasi tanpa melakukan refresh.

Persiapan Project

Hardware setup persis dengan modul sebelumnya, DHT11 akan dihubungkan ke GPIO17.

Buat project baru, lalu ubah platformio.ini seperti dibawah:

[env:az-delivery-devkit-v4]
platform = espressif32
board = az-delivery-devkit-v4
framework = espidf

monitor_speed = 115200
lib_extra_dirs = 
    ../esp-idf-lib/components
    ../common
build_flags =
    -DWIFI_SSID=${sysenv.WIFI_SSID}
    -DWIFI_PASS=${sysenv.WIFI_PASS}

board_build.partitions = partitions.csv

Kita akan gunakan custom partitions untuk menyimpan file index.html pada flash.

Buat file partitions.csv dengan isi seperti berikut:

# Name,   Type, SubType, Offset,   Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs,      data, nvs,     ,        0x6000,
phy_init, data, phy,     ,        0x1000,
factory,  app,  factory, ,        1M,
spiffs,   data, spiffs,  0x210000,        1M,

Kita akan upload index.html pada spiffs partition.

Buat data folder untuk menyimpan index.html. Berikut isi file index.html

<!DOCTYPE HTML><html>
<head>
  <title>DHT11 Sensor</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <style>
  html {
    font-family: Arial, Helvetica, sans-serif;
    text-align: center;
  }
  h1 {
    font-size: 1.8rem;
    color: white;
  }
  h2{
    font-size: 1.5rem;
    font-weight: bold;
    color: #143642;
  }
  .topnav {
    overflow: hidden;
    background-color: #143642;
  }
  body {
    margin: 0;
  }
  .content {
    padding: 30px;
    max-width: 600px;
    margin: 0 auto;
  }
  .card {
    background-color: #F8F7F9;;
    box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
    padding-top:10px;
    padding-bottom:20px;
  }
  .button {
    padding: 15px 50px;
    font-size: 24px;
    text-align: center;
    outline: none;
    color: #fff;
    background-color: #0f8b8d;
    border: none;
    border-radius: 5px;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
   }
   
   .button:active {
     background-color: #0f8b8d;
     box-shadow: 2 2px #CDCDCD;
     transform: translateY(2px);
   }
   .state {
     font-size: 1.5rem;
     color:#8c8c8c;
     font-weight: bold;
   }
  </style>

</head>
<body>
  <div class="content">
    <div class="card">
      
      <p class="state">State: <span id="state">%STATE%</span></p>
      <p class="state">Temp: <span id="temp">%TEMP%</span></p>
      <p class="state">Hum: <span id="hum">%HUM%</span></p>
      <p><button id="button" class="button">Toggle</button></p>
    </div>
  </div>
<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  var wscnt=0;
  var toggle=0;
  window.addEventListener('load', onLoad);

  function initWs() {
    websocket = new WebSocket(gateway);
    websocket.onopen    = onOpen;
    websocket.onclose   = onClose;
    websocket.onmessage = onMessage;
  }
  function onOpen(event) {
    console.log('Connection opened ' + (++wscnt));
  }
  function onClose(event) {
    console.log('Connection closed ' + (--wscnt));
    setTimeout(initWs, 2000);
  }
  function onMessage(event) {
    console.log(event.data);
    var res = JSON.parse(event.data);
    document.getElementById('state').innerHTML = res.state;
    document.getElementById('temp').innerHTML = res.temp;
    document.getElementById('hum').innerHTML = res.hum;
  }
  function onLoad(event) {
    initWs();
    document.getElementById('button').addEventListener('click', function() {
    	websocket.send(++toggle);
      console.log('button click ' + toggle);
    });
  }
</script>
</body>
</html>

Edit CMakeLists.txt:

cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(websocket)
spiffs_create_partition_image(spiffs data)

Buka platformio CLI lalu set environment variable:

(penv)$ export WIFI_SSID='\"<your_ssid>\"'
(penv)$ export WIFI_PASS='\"<your_passwd>\"'

Jalankan menuconfig untuk mengatur partitions file dan enable websocket. Untuk websocket config, atur juga Max HTTP Request Header Length menjadi 4096 pada menu yang sama.

Code

Persiapan project sudah selesai, berikutnya buka file src/main.c, lalu masukan code berikut:

//import library dan init variable
#include <string.h>
#include <stdint.h>
#include <sys/stat.h>

#include "esp_log.h"

#include "app_temp.h"
#include "app_wifi.h"

#include "esp_spiffs.h"
#include "esp_http_server.h"

const static char *TAG = "app";

#define INDEX_HTML_PATH "/spiffs/index.html"
static char index_html[4096];

static int temperature, humidity;
static bool enabled = true;

static httpd_handle_t server = NULL;
static int ws_fd = -1;

//init html
static void init_html(void)
{
    esp_vfs_spiffs_conf_t conf = {
        .base_path = "/spiffs",
        .partition_label = NULL,
        .max_files = 5,
        .format_if_mount_failed = true};

    ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf));

    memset((void *)index_html, 0, sizeof(index_html));
    struct stat st;
    if (stat(INDEX_HTML_PATH, &st))
    {
        ESP_LOGE(TAG, "index.html not found");
        return;
    }

    FILE *fp = fopen(INDEX_HTML_PATH, "r");
    if (fread(index_html, st.st_size, 1, fp) != st.st_size)
    {
        ESP_LOGE(TAG, "fread failed");
    }
    fclose(fp);
}

//fungsi send async
static void send_async(void *arg)
{
    if (ws_fd < 0)
    {
        return;
    }

    char buff[128];
    memset(buff, 0, sizeof(buff));
    sprintf(buff, "{\"state\": \"%s\", \"temp\": %d, \"hum\": %d}",
            enabled ? "ON" : "OFF", temperature, humidity);
    
    httpd_ws_frame_t ws_pkt;
    memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
    ws_pkt.payload = (uint8_t *)buff;
    ws_pkt.len = strlen(buff);
    ws_pkt.type = HTTPD_WS_TYPE_TEXT;

    httpd_ws_send_frame_async(server, ws_fd, &ws_pkt);
}

static esp_err_t handle_http_get(httpd_req_t *req)
{
    if (index_html[0] == 0)
    {
        httpd_resp_set_status(req, HTTPD_500);
        return httpd_resp_send(req, "no index.html", HTTPD_RESP_USE_STRLEN);
    }
    return httpd_resp_send(req, index_html, HTTPD_RESP_USE_STRLEN);
}

//function handle_ws_req
static esp_err_t handle_ws_req(httpd_req_t *req)
{
    enabled = !enabled;

    httpd_ws_frame_t ws_pkt;
    uint8_t buff[16];
    memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
    ws_pkt.payload = buff;
    ws_pkt.type = HTTPD_WS_TYPE_BINARY;

    httpd_ws_recv_frame(req, &ws_pkt, sizeof(buff));

    if (!enabled)
    {
        httpd_queue_work(server, send_async, NULL);
    }
    return ESP_OK;
}

//function handle socket open
static esp_err_t handle_socket_opened(httpd_handle_t hd, int sockfd)
{
    ws_fd = sockfd;
    return ESP_OK;
}

//function handle socket close
static void handle_socket_closed(httpd_handle_t hd, int sockfd)
{
    if (sockfd == ws_fd)
    {
        ws_fd = -1;
    }
}

//function start server
static void start_server(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.open_fn = handle_socket_opened;
    config.close_fn = handle_socket_closed;

    if (httpd_start(&server, &config) == ESP_OK)
    {
        httpd_uri_t uri_get = {
            .uri = "/",
            .method = HTTP_GET,
            .handler = handle_http_get,
            .user_ctx = NULL};
        httpd_register_uri_handler(server, &uri_get);

        httpd_uri_t ws = {
            .uri = "/ws",
            .method = HTTP_GET,
            .handler = handle_ws_req,
            .user_ctx = NULL,
            .is_websocket = true};
        httpd_register_uri_handler(server, &ws);
    }
}

//fungsi update reading
static void update_reading(int temp, int hum)
{
    temperature = temp;
    humidity = hum;

    if (server != NULL && enabled)
    {
        httpd_queue_work(server, send_async, NULL);
    }
}

//fungsi handle wifi
static void handle_wifi_connect(void)
{
    start_server();
    apptemp_init(update_reading);
}

//fungsi wifi failed
static void handle_wifi_failed(void)
{
    ESP_LOGE(TAG, "wifi failed");
}

//main function
void app_main()
{
    init_html();

    connect_wifi_params_t cbs = {
        .on_connected = handle_wifi_connect,
        .on_failed = handle_wifi_failed};
    appwifi_connect(cbs);
}

Penjelasan Code

Bagian Import Library

Untuk menggunakan websocket diperlukan file header. esp_http_server.h.

Kita definisikan global variable server dan ws_fd yang digunakan untuk mengirim asynchronous WebSocket messages. ws_fd berguna untuk menunjukan WebSocket descriptor.

Bagian Main Function

Fungsi init_html akan membuka spiffs partition dan membaca file index.html. Fungsi appwifi_connect untuk connect ke local Wi-Fi. Jika koneksi berhasil, handle_wifi_connect akan dipanggil.

Bagian Init Html

Panggil esp_vfs_spiffs_register untuk inisialisasi spiffs partition. Kemudian periksa apakah file index.html tersedia didalam partition.

Fungsi stat akan menyimpan file information dalam variable st dan returns error code jika file tidak tersedia.

Jika file tersedia, kita baca file index.html dan simpan dalam variable index_html, yang akan digunkan ketika GET request terjadi.

Bagian Wifi Connect

Pada handle_wifi_connect, kita jalankan web server dan pembacaan dari DHT11. Jika ada client yang terhubung, fungsi update_reading akan mengirim hasil pembacaan via WebSocket.

Bagian Start Server

Pertama kita definisikan HTTP configuration variable yang akan digunakan untuk menjalankan web server. handle_socket_opened dan handle_socket_closed adalah callback functions untuk track active WebSocket paling akhir.

Kemudian kita start web server. Jika httpd_start berhasil menjalankan web server, lanjutkan dengan register handler unuk menjawab GET requests root endpoint.

Kita juga register-kan handler untuk WebSocket requests yang dikirim ke /ws endpoint.

Untuk menentukan sebuah endpoint adalah WebSocket, gunakan setting is_websocket
field dengan nilai true.

Bagian Handle Socket Open

Ketikan koneksi WebSocket terbentuk, simpan socket descriptor dalam variable ws_fd. Jika socket yang sama ditutup, kita set ws_fd menjadi -1, untuk menunjukan tidak ada active socket yang digunakan.

Bagian Esp Err Handle

Ketika WebSocket request diterima, yang akan melakukan toggling state, kita akan ubah variable enabled. Kita definisikan variable httpd_ws_frame_t type dan memanggil httpd_ws_recv_frame untuk menyimpan data.

Jika diperlukan, kita bisa memeriksa payload. Jika device kita disable (hasil dari toggling GET request ), kita update informasi untuk client dengan memanggil httpd_queue_work.

Web server memiliki kapabilitas berkerja secara asynchronous, jadi kita gunakan callback function, send_async, untuk di schedule oleh web server.

Bagian Send Async

JIka terdapat active socket, kita siapkan data dengan JSON-formatted. Fungsi httpd_ws_send_frame_async akan mengirim data melalui socket.

Bagian Update Reading

update_reading dipanggil setiap 2 second. Didalam fungsi kita set global variables dengan nilai temperature dan humidity, dan jika device dalam enabled state, kita panggil httpd_queue_work untuk schedule send_async.

Bagian Handle Http Get

Jika index_html kosong, berarti ESP32 gagal membaca file index.html dari flash memory. Oleh karena itu kita set status code ke HTTP-500, yang berarti Internal Server Error. Jika index_html berisi web page, kita kirimkan ke client.

Compile, Upload dan Test

Buka PlatformIO CLI, lalukan compile, build dan upload filesystem (berisi file index.html), upload firmware ke devkit dan jalankan serial monitor.

(penv)$ pio run
(penv)$ pio run -t buildfs 
(penv)$ pio run -t uploadfs
(penv)$ pio run -t upload 
(penv)$ pio device monitor

Alamat IP dari devkit akan tampil di serial monitor. Setelah web server berjalan, buka web browser dan buka dengan alamat IP tersebut.

Dapat kita lihat, nila temperature dan humidity di update secara otomatis ketika state ON. Kita dapat ganti state dengan menekan Toggle button.

Sampai disini kita sudah mempelajari penggunaan WebSocket.

Dengan berakhirnya modul ini, tutorial Project IoT menggunakan ESP32 – Advanced telah selesai. Semoga bermanfaat.

Sharing is caring:

Leave a Comment