/      ๆ—ฅๆœฌ่ชž

Pre-installed Sketch

Latest Edition (ESP32 Arduino Core v3.x.x)
This is an overview of the sample sketch spot-server, which operates as a wireless LAN access point and displays data from child devices on a web page.

Obtaining the Source Code

Available from GitHub (monowireless/spot-server).

System Overview

spot-server consists of an Arduino sketch (.ino) for receiving and forwarding data from TWELITE, and a web page (.html / .css / .js) delivered to smartphones.

Diagram

Diagram

Data transmitted from TWELITE nodes is received by the Arduino sketch, which triggers events on the published web page. The published web page dynamically updates its HTML content in response to these events.

What You Need for Development

Setting Up Your Environment

Installing the IDE and Toolchain

Please refer to How to set up the development environment using Arduino IDE 1.x.

Installing Libraries

First, if there is no libraries folder in the Arduino sketchbook location (as specified in the Arduino IDE preferences, e.g., C:\Users\foo\Documents\Arduino), please create it.

Asynchronous TCP Communication Library

  1. Download the Zip file from GitHub (me-no-dev/AsyncTCP)
  2. Extract the Zip file and rename the folder from AsyncTCP-master to AsyncTCP
  3. Place the AsyncTCP folder into the libraries folder

Asynchronous Web Server Library

  1. Download the Zip file from GitHub (me-no-dev/ESPAsyncWebServer)
  2. Extract the Zip file and rename the folder from AsyncWebServer-master to AsyncWebServer
  3. Place the AsyncWebServer folder into the libraries folder

OLED Display Library

  1. Download the Zip file from GitHub (Seeed-Studio/OLED_Display_96X96)
  2. Extract the Zip file and rename the folder from OLED_Display_96X96-master to OLED_Display_96X96
  3. Place the OLED_Display_96X96 folder into the libraries folder

JSON Library

Open the Library Manager and install Arduino_JSON.

Installing Plugins

File System Upload Plugin

To write files such as HTML to the ESP32’s flash area, an Arduino plugin is required.

Here, we use lorol/arduino-esp32fs-plugin: Arduino plugin for uploading files to ESP32 file system (a plugin compatible with LittleFS).

For installation instructions, see TWELITE SPOT Manual: How to write files to ESP32.

Obtaining Project Files

  1. Download the Zip file from GitHub (monowireless/spot-server)
  2. Extract the Zip file and rename the folder from spot-server-main to spot-server
  3. Place the spot-server folder into the Arduino sketchbook location (as specified in the Arduino IDE preferences, e.g., C:\Users\foo\Documents\Arduino)

Writing Project Files

Sketch

See How to write sketches to ESP32.

Web Page

See How to write files to ESP32.

Sketch

This section explains the Arduino sketch spot-server.ino.

Including Libraries

Official Arduino and ESP32 Libraries

Lines 4-9 include the official Arduino and ESP32 libraries.

#include <Arduino.h>
#include <Arduino_JSON.h>
#include <ESPmDNS.h>
#include <LittleFS.h>
#include <WiFi.h>
#include "esp_wifi.h"
#include <Wire.h>
Header FileDescriptionRemarks
Arduino.hBasic Arduino libraryCan sometimes be omitted, but included just in case
Arduino_JSON.hHandles JSON stringsDifferent from ArduinoJson
ESPmDNS.hUses mDNSRequired to use hostnames
LittleFS.hHandles LittleFS file systemNeeded for page publishing
WiFi.hUses ESP32 WiFi
esp_wifi.hAdvanced WiFi settingsNeeded for locale settings
Wire.hUses I2CFor OLED display

Third-Party Libraries

Lines 13-15 include third-party libraries.

#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SeeedGrayOLED.h>
Header FileDescriptionRemarks
AsyncTCP.hPerforms asynchronous TCP communication
ESPAsyncWebServer.hRuns asynchronous web serverDepends on AsyncTCP
SeeedGrayOLED.hUses OLED display

MWings Library

Line 18 includes the MWings library.

#include <MWings.h>

Pin Number Definitions

Lines 21-25 define pin numbers.

const uint8_t TWE_RST = 5;
const uint8_t TWE_PRG = 4;
const uint8_t LED = 18;
const uint8_t ESP_RXD1 = 16;
const uint8_t ESP_TXD1 = 17;
NameDescription
TWE_RSTPin number connected to the RST pin of TWELITE
TWE_PRGPin number connected to the PRG pin of TWELITE
LEDPin number connected to the ESP32 onboard LED
ESP_RXD1Pin number connected to the TX pin of TWELITE
ESP_TXD1Pin number connected to the RX pin of TWELITE

TWELITE Configuration Definitions

Lines 28-31 define the settings applied to the TWELITE parent module mounted on TWELITE SPOT.

const uint8_t TWE_CH = 18;
const uint32_t TWE_APPID = 0x67720102;
const uint8_t TWE_RETRY = 2;
const uint8_t TWE_POWER = 3;
NameDescription
TWE_CHTWELITE frequency channel
TWE_APPIDTWELITE Application ID
TWE_RETRYTWELITE retransmission count (on transmit)
TWE_POWERTWELITE transmit power

Wireless LAN Configuration Definitions

Lines 34-46 define the wireless LAN settings applied to the ESP32 mounted on TWELITE SPOT.

wifi_country_t WIFI_COUNTRY_JP = {
  cc: "JP",         // Contry code
  schan: 1,         // Starting channel
  nchan: 14,        // Number of channels
  max_tx_power: 20, // Maximum power in dBm
  policy: WIFI_COUNTRY_POLICY_MANUAL
};
const char* WIFI_SSID_BASE = "TWELITE SPOT";
const char* WIFI_PASSWORD = "twelitespot";
const uint8_t WIFI_CH = 13;
const IPAddress WIFI_IP = IPAddress(192, 168, 1, 1);
const IPAddress WIFI_MASK = IPAddress(255, 255, 255, 0);
const char* HOSTNAME = "spot";    // spot.local
NameDescription
WIFI_COUNTRY_JPLocale setting (Japan)
WIFI_SSID_BASECommon part of SSID string
WIFI_PASSWORDPassword
WIFI_CHESP32 frequency channel
WIFI_IPIP address
WIFI_MASKSubnet mask
HOSTNAMEHost name

Declaration of Global Objects

Lines 49-50 declare global objects.

AsyncWebServer server(80);
AsyncEventSource events("/events");
NameDescription
serverInterface for asynchronous web server opened on port 80
eventsInterface for server-sent events opened at /events ?

Declaration of Function Prototypes

Lines 53-57 declare function prototypes.

uint16_t createUidFromMac();
String createJsonFrom(const ParsedAppTwelitePacket& packet);
String createJsonFrom(const ParsedAppAriaPacket& packet);
String createJsonFrom(const ParsedAppCuePacket& packet);
String createJsonFrom(const BarePacket& packet);
NameDescription
createUidFromMac()Creates an identifier for SSID from MAC address
createJsonFrom()<ParsedAppTwelitePacket&>Creates a JSON string from App_Twelite packet data
createJsonFrom()<ParsedAppAriaPacket&>Creates a JSON string from App_ARIA packet data
createJsonFrom()<ParsedAppCuePacket&>Creates a JSON string from App_CUE packet data
createJsonFrom()<BarePacket&>Creates a JSON string from all packet data

TWELITE Configuration

In lines 66-71, Twelite.begin() is called to configure and start the TWELITE parent module mounted on the TWELITE SPOT.

    Serial2.begin(115200, SERIAL_8N1, ESP_RXD1, ESP_TXD1);
    if (Twelite.begin(Serial2,
                      LED, TWE_RST, TWE_PRG,
                      TWE_CH, TWE_APPID, TWE_RETRY, TWE_POWER)) {
        Serial.println("Started TWELITE.");
    }
ArgumentTypeDescription
Serial2HardwareSerial&Serial port used for communication with TWELITE
LEDintPin number connected to the status LED
TWE_RSTintPin number connected to the RST pin of TWELITE
TWE_PRGintPin number connected to the PRG pin of TWELITE
TWE_CHANNELuint8_tTWELITE frequency channel
TWE_APP_IDuint32_tTWELITE Application ID
TWE_RETRYuint8_tTWELITE retransmission count (on transmit)
TWE_POWERuint8_tTWELITE transmit power

App_Twelite: Registering Event Handler

In lines 73-80, Twelite.on() <ParsedAppTwelitePacket> is called to register the process to be executed when a packet is received from a child device running the super-easy standard app.

Twelite.on([](const ParsedAppTwelitePacket& packet) {
    Serial.println("Received a packet from App_Twelite");
    String jsonStr = createJsonFrom(packet);
    if (not(jsonStr.length() <= 0)) {
        events.send(jsonStr.c_str(), "data_app_twelite", millis());
    }
    events.send("parsed_app_twelite", "data_parsing_result", millis());
});

Creating a JSON String

In line 75, a JSON string is generated from the received data.

String jsonStr = createJsonFrom(packet);

To display received data on the web page, it is necessary to send the data to the client-side JavaScript. Since string data is easier to handle in this case, a JSON string is used.

Sending Events to the Web Page

In lines 76-78, the generated JSON string is sent to the “Signal Viewer” page.

if (not(jsonStr.length() <= 0)) {
    events.send(jsonStr.c_str(), "data_app_twelite", millis());
}

The event name is data_app_twelite.

In line 79, notification that a packet has been received from App_Twelite is sent to the “Serial Viewer” page.

events.send("parsed_app_twelite", "data_parsing_result", millis());

App_ARIA: Registering Event Handler

In lines 82-92, Twelite.on() <ParsedAppAriaPacket> is called to register the process to be executed when a packet is received from a child device running the ARIA app (TWELITE ARIA mode).

Twelite.on([](const ParsedAppAriaPacket& packet) {
        Serial.println("Received a packet from App_ARIA");
        static uint32_t firstSourceSerialId = packet.u32SourceSerialId;
        if (packet.u32SourceSerialId == firstSourceSerialId) {
            String jsonStr = createJsonFrom(packet);
            if (not(jsonStr.length() <= 0)) {
                events.send(jsonStr.c_str(), "data_app_aria_twelite_aria_mode", millis());
            }
        }
        events.send("parsed_app_aria_twelite_aria_mode", "data_parsing_result", millis());
    });

Target Filtering

In lines 84-85, the processing is limited to the first child device received.

static uint32_t firstSourceSerialId = packet.u32SourceSerialId;
if (packet.u32SourceSerialId == firstSourceSerialId) {

This is done to maintain graph consistency when there are multiple child devices.

Creating a JSON String

In line 86, a JSON string is generated from the received data.

String jsonStr = createJsonFrom(packet);

Sending Events to the Web Page

In lines 87-89, the generated JSON string is sent to the “ARIA Viewer” page.

if (not(jsonStr.length() <= 0)) {
    events.send(jsonStr.c_str(), "data_app_aria_twelite_aria_mode", millis());
}

The event name is data_app_aria_twelite_aria_mode.

In line 91, notification that a packet has been received from App_Twelite is sent to the “Serial Viewer” page.

events.send("parsed_app_aria_twelite_aria_mode", "data_parsing_result", millis());

App_CUE: Registering Event Handler

In lines 94-104, Twelite.on() <ParsedAppCuePacket> is called to register the process to be executed when a packet is received from a child device running the CUE app (TWELITE CUE mode).

Twelite.on([](const ParsedAppCuePacket& packet) {
    Serial.println("Received a packet from App_CUE");
    static uint32_t firstSourceSerialId = packet.u32SourceSerialId;
    if (packet.u32SourceSerialId == firstSourceSerialId) {
        String jsonStr = createJsonFrom(packet);
        if (not(jsonStr.length() <= 0)) {
            events.send(jsonStr.c_str(), "data_app_cue_twelite_cue_mode", millis());
        }
    }
    events.send("parsed_app_cue_twelite_cue_mode", "data_parsing_result", millis());
});

Others: Registering Event Handlers

In lines 106-134, the processes to be executed when packets are received from child devices running other apps are registered.

As with the ARIA app, events are sent to the “Serial Viewer.”

All: Registering Event Handler

In lines 136-142, the process to be executed when packets are received from all apps’ child devices is registered.

Twelite.on([](const BarePacket& packet) {
    String jsonStr = createJsonFrom(packet);
    if (not(jsonStr.length() <= 0)) {
        events.send(jsonStr.c_str(), "data_bare_packet", millis());
    }
    events.send("unparsed_bare_packet", "data_parsing_result", millis());
});

Here too, the packet data string is sent to the “Serial Viewer.”

OLED Display Configuration

In lines 145-150, the OLED display is configured.

    Wire.begin();
    SeeedGrayOled.init(SSD1327);
    SeeedGrayOled.setNormalDisplay();
    SeeedGrayOled.setVerticalMode();
    SeeedGrayOled.setGrayLevel(0x0F);
    SeeedGrayOled.clearDisplay();

Wireless LAN Configuration

In lines 154-165, the wireless LAN is configured.

    WiFi.mode(WIFI_AP);
    esp_wifi_set_country(&WIFI_COUNTRY_JP);
    char uidCString[8];
    sprintf(uidCString, " (%02X)", createUidFromMac());
    char ssidCString[20];
    sprintf(ssidCString, "%s%s", WIFI_SSID_BASE, uidCString);
    if (not WiFi.softAP(ssidCString, WIFI_PASSWORD, WIFI_CH, false, 10)) {
      Serial.println("Failed to start AP");
    }
    delay(100);    // IMPORTANT: Waiting for SYSTEM_EVENT_AP_START
    WiFi.softAPConfig(WIFI_IP, WIFI_IP, WIFI_MASK);
    MDNS.begin(HOSTNAME);

Configuring the File System

In line 198, the LittleFS file system is configured.

if (LittleFS.begin()) { Serial.println("Mounted file system."); }

This allows files such as HTML written in the flash area to be retrieved as web pages.

Web Server Configuration

In lines 201-228, the web server is configured.

Handling GET Requests

For example, in lines 206-210, a GET request to /signal-viewer returns /signal-viewer.html from the LittleFS file system.

server.on("/signal-viewer", HTTP_GET,
          [](AsyncWebServerRequest* request) {
              Serial.println("HTTP_GET: signal-viewer.html");
              request->send(LittleFS, "/signal-viewer.html", "text/html");
          });

Server Initialization

In lines 226-228, the root of the file system is set as the server root, the event source is registered, and the server is started.

server.serveStatic("/", LittleFS, "/");
server.addHandler(&events);
server.begin();

Updating TWELITE Data

In line 234, Twelite.update() is called.

    Twelite.update();

Twelite.update() is a function that sequentially reads out packet data (in ModBus ASCII format) sent from the TWELITE parent module, one byte at a time.

Web Page

We will not provide a detailed explanation of the web page here. Instead, we will focus on the important points.

HTML: Grid System

This sample’s HTML uses Flexbox Grid (the source file is data/css/flexboxgrid.min.css).

A 12-column grid system similar to Bootstrap is used as shown below.

      <div class="col-xs-6 col-sm-6 col-md-5 col-lg-4">
        <div class="neumorphic inset dense row center-xs middle-xs">
          <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 npr npl">
            <img src="./images/logo-lands.svg" class="logo" />
          </div>
        </div>
      </div>

      <div class="col-xs-6 col-sm-6 col-md-7 col-lg-8">
        <div class="neumorphic inset dense row center-xs middle-xs">
          <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 nwp npr npl">
            <span class="medium bold">TWELITE SPOT</span>
          </div>
          <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 nwp npr npl">
            <span class="small bold">CUE Viewer</span>
          </div>
        </div>
      </div>

Here, the element centered on the logo has a width of 6/12, and the element centered on the text also has a width of 6/12; that is, both are placed side by side with equal width in one row. Also, the elements centered on the text TWELITE SPOT and CUE Viewer both have a width of 12/12, so they are arranged as two separate rows.

HTML: Data Display Section

Elements that display data received from TWELITE child devices are assigned unique IDs.

Below is an excerpt of the section that displays the X-axis acceleration received from TWELITE CUE.

<div class="col-xs-4 nwp npr npl">
  <code class="medium"
        id="latest-accel-x">ยฑ--.--</code>
  <code class="small">G</code>
</div>

Here, the ID latest-accel-x is assigned. Using this ID, the value is updated from the script.

JS: Global Variables

In lines 4-8, global variables are declared to store the latest acceleration values.

let latest_accel = {
    x: 0.0,
    y: 0.0,
    z: 0.0
};

These values are also used by the graph, so global variables are used to simplify the implementation.

JS: Graph Settings

In lines 11-133, configuration is done for the graph drawing library Chart.js | Chart.js and its plugin chartjs-plugin-streaming.

JS: Updating Page Content

The function processDataAppCueTweliteCueMode() in lines 136-235 updates the page content when a data_app_cue_twelite_cue_mode event is received from the sketch.

For example, in lines 184-208, the voltage value and emoji are updated according to the power supply voltage of TWELITE CUE.

if (data.vcc >= 3000) {
    document.getElementById("latest-vcc-icon").innerHTML = "๐Ÿ”‹";
    document.getElementById("latest-vcc-data").innerHTML = `${(data.vcc / 1000.0).toFixed(2).toString().padStart(4)}`;
    document.getElementById("latest-vcc-data").classList.remove("red");
    document.getElementById("latest-vcc-data").classList.remove("yellow");
    document.getElementById("latest-vcc-data").classList.add("green");
} else if (data.vcc >= 2700) {
    document.getElementById("latest-vcc-icon").innerHTML = "๐Ÿ”‹";
    document.getElementById("latest-vcc-data").innerHTML = `${(data.vcc / 1000.0).toFixed(2).toString().padStart(4)}`;
    document.getElementById("latest-vcc-data").classList.remove("red");
    document.getElementById("latest-vcc-data").classList.remove("yellow");
    document.getElementById("latest-vcc-data").classList.remove("green");
} else if (data.vcc >= 2400) {
    document.getElementById("latest-vcc-icon").innerHTML = "๐Ÿชซ";
    document.getElementById("latest-vcc-data").innerHTML = `${(data.vcc / 1000.0).toFixed(2).toString().padStart(4)}`;
    document.getElementById("latest-vcc-data").classList.remove("red");
    document.getElementById("latest-vcc-data").classList.add("yellow");
    document.getElementById("latest-vcc-data").classList.remove("green");
} else {
    document.getElementById("latest-vcc-icon").innerHTML = "๐Ÿชซ";
    document.getElementById("latest-vcc-data").innerHTML = `${(data.vcc / 1000.0).toFixed(2).toString().padStart(4)}`;
    document.getElementById("latest-vcc-data").classList.add("red");
    document.getElementById("latest-vcc-data").classList.remove("yellow");
    document.getElementById("latest-vcc-data").classList.remove("green");
}

Here, when the power supply voltage drops below 2700mV, the emoji changes from ๐Ÿ”‹ to ๐Ÿชซ, and as the voltage decreases from 3000mV โ†’ 2700mV โ†’ 2400mV, the CSS class applied to the voltage value text color is changed accordingly.

Registering Event Listeners

In lines 254-257, the process for handling events received from the sketch is registered.

source.addEventListener("data_app_cue_twelite_cue_mode", (e) => {
    console.log("data_app_cue_twelite_cue_mode", e.data);
    processDataAppCueTweliteCueMode(JSON.parse(e.data));
}, false);

Here, the event message received from the sketch is parsed from a JSON string and the parsed data is passed to the function processDataAppCueTweliteCueMode().

Arduino

ESP32

Community

Libraries

Plugins

ECMAScript (JavaScript)

Community