This is the multi-page printable view of this section. Click here to print...

Return to the regular view of this page

As of 2025-07-24

Sketches Using TWELITE with Wi-Fi

Advanced sample sketches for TWELITE SPOT using Wi-Fi
This section explains advanced sample sketches that combine TWELITE NET with Wi-Fi functionality.

1 - Pre-installed Sketch

An explanation of the sample sketch spot-server, a local server that displays data from end devices on a web page.
This is an explanation of the sample sketch spot-server, which acts as a wireless LAN access point and displays data from end devices on a web page.

1.1 - 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

1.2 - Pre-installed Sketch

ESP32 Arduino Core v2.x.x Version
This is an explanation of the sample sketch spot-server, which acts as a wireless LAN access point and displays data from child devices on a web page.

Obtaining the Source Code

You can obtain it from GitHub (monowireless/spot-server).

System Overview

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

Image diagram

Image diagram

The data sent by TWELITE child devices is received by the Arduino sketch, which then fires events to the published web page. The published web page dynamically rewrites its HTML content in response to these events.

Requirements for Development

Environment Setup

Installing the IDE and Toolchain

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

Installing Libraries

First, if there is no libraries folder in your Arduino sketchbook location (as specified in Arduino IDE preferences, e.g., C:\Users\foo\Documents\Arduino), 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 your 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 your 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 your libraries folder.

JSON Library

Open the Library Manager and install Arduino_JSON.

Installing Plugins

File System Writing Plugin

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

Here, we use lorol/arduino-esp32fs-plugin: Arduino plugin for uploading files to ESP32 file system.

For installation instructions, see TWELITE SPOT Manual: How to Write Files to ESP32.

Downloading 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 your Arduino sketchbook location (as specified in Arduino IDE preferences, e.g., C:\Users\foo\Documents\Arduino).

How to Write Project Files

Sketch

See How to write a sketch 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 <Wire.h>
Header FileDescriptionRemarks
Arduino.hBasic Arduino libraryMay be omitted, but included just in case
Arduino_JSON.hHandles JSON stringsDifferent from ArduinoJson
ESPmDNS.hUses mDNSRequired for using hostnames
LittleFS.hHandles LittleFS file systemNeeded for serving pages
WiFi.hUses ESP32 WiFi
Wire.hUses I2CFor OLED display

Third-party Libraries

Lines 12-14 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 17 includes the MWings library.

#include <MWings.h>

Definition of Pin Numbers

Lines 20-24 define the 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 Settings Definition

Lines 27-30 define the settings applied to the TWELITE parent device 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 transmission)
TWE_POWERTWELITE transmission output

Wireless LAN Settings Definition

Lines 33-38 define the wireless LAN settings applied to the ESP32 mounted on TWELITE SPOT.

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_SSID_BASECommon part of the SSID string
WIFI_PASSWORDPassword
WIFI_CHESP32 frequency channel
WIFI_IPIP address
WIFI_MASKSubnet mask
HOSTNAMEHost name

Declaration of Global Objects

Lines 41-42 declare global objects.

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

Declaration of Function Prototypes

Lines 45-49 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 Settings

Lines 58-63 call Twelite.begin() to configure and start the TWELITE parent device mounted on 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 status LED
TWE_RSTintPin number connected to TWELITE RST pin
TWE_PRGintPin number connected to TWELITE PRG pin
TWE_CHANNELuint8_tTWELITE frequency channel
TWE_APP_IDuint32_tTWELITE application ID
TWE_RETRYuint8_tTWELITE retransmission count (on transmission)
TWE_POWERuint8_tTWELITE transmission output

App_Twelite: Registering Event Handler

Lines 65-72 call Twelite.on() <ParsedAppTwelitePacket> to register the process to execute when a packet is received from a child device using 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 JSON String

Line 67 generates a JSON string from the received data.

String jsonStr = createJsonFrom(packet);

To display the received data on the web page, it is necessary to send the data to client-side JavaScript, and string data is easier to handle, so it is converted to a JSON string.

Sending Events to the Web Page

Lines 68-70 send the generated JSON string 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.

Line 71 sends a notification to the “Serial Viewer” page that a packet has been received from App_Twelite.

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

App_ARIA: Registering Event Handler

Lines 74-84 call Twelite.on() <ParsedAppAriaPacket> to register the process to execute when a packet is received from a child device in 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());
    });

Filtering the Target

Lines 76-77 limit the processing to the first child device received.

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

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

Creating JSON String

Line 78 generates a JSON string from the received data.

String jsonStr = createJsonFrom(packet);

Sending Events to the Web Page

Lines 79-81 send the generated JSON string 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.

Line 83 sends a notification to the “Serial Viewer” page that a packet has been received from App_Twelite.

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

App_CUE: Registering Event Handler

Lines 86-96 call Twelite.on() <ParsedAppCuePacket> to register the process to execute when a packet is received from a child device in 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

Lines 98-126 register the process to execute when packets are received from child devices of other apps.

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

All: Registering Event Handlers

Lines 128-134 register the process to execute when packets are received from child devices of any app.

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 as well, the packet data string is sent to the “Serial Viewer”.

OLED Display Settings

Lines 137-142 configure the OLED display.

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

Wireless LAN Settings

Lines 146-154 configure the wireless LAN.

WiFi.mode(WIFI_AP);
char uidCString[8];
sprintf(uidCString, " (%02X)", createUidFromMac());
char ssidCString[20];
sprintf(ssidCString, "%s%s", WIFI_SSID_BASE, uidCString);
WiFi.softAP(ssidCString, WIFI_PASSWORD, WIFI_CH, false, 8);
delay(100);    // IMPORTANT: Waiting for SYSTEM_EVENT_AP_START
WiFi.softAPConfig(WIFI_IP, WIFI_IP, WIFI_MASK);
MDNS.begin(HOSTNAME);

File System Settings

Line 187 configures the LittleFS file system.

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

This allows you to retrieve files such as HTML written to the flash area as web pages.

Web Server Settings

Lines 190-217 configure the web server.

Handling GET Requests

For example, lines 195-199 return /signal-viewer.html from the LittleFS file system in response to a GET request to /signal-viewer.

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

Lines 215-217 set the root of the file system as the server root, register the event source, and start the server.

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

Updating TWELITE Data

Line 223 calls Twelite.update().

    Twelite.update();

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

Web Page

This section does not provide a detailed explanation of the web page. Only important points are explained.

HTML: Grid System

The HTML of this sample uses Flexbox Grid (the source file is data/css/flexboxgrid.min.css).

It uses a 12-column grid system similar to Bootstrap, 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 is set to 6/12 width, and the element centered on the text is also 6/12 width, so both are arranged in a row with equal width. The elements centered on the text TWELITE SPOT and CUE Viewer are both 12/12 width, meaning they are arranged in two rows, each occupying one full row.

HTML: Data Display Section

Elements displaying data received from TWELITE child devices are given unique IDs.

Below is an excerpt showing the part 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. This ID is used to update the value from the script.

JS: Global Variables

Lines 4-8 declare global variables for storing 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

Lines 11-133 configure the graph drawing library Chart.js | Chart.js and its plugin chartjs-plugin-streaming.

JS: Updating Page Content

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

For example, lines 184-208 update the voltage value and emoji 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 drops from 3000mV → 2700mV → 2400mV, the CSS class applied to the voltage value text color is switched accordingly.

Registering Event Listeners

Lines 254-257 register the process to execute when events are received from the sketch.

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 as a JSON string, and the resulting data is passed to the function processDataAppCueTweliteCueMode().

Arduino

ESP32

Community

Libraries

Plugins

ECMAScript (JavaScript)

Community

2 - Relay for WebSocket

An explanation of the sample sketch spot-router, which relays data from the end device to a WebSocket server.
This is an explanation of the sample sketch spot-router, which acts as a wireless LAN end device and relays received packet data strings to a WebSocket server on the LAN.

2.1 - Relay for WebSocket

Latest version
This is an explanation of the sample sketch spot-router, which acts as a wireless LAN client and relays received packet data strings to a WebSocket server on the LAN.

Obtaining the source code

Available from GitHub (monowireless/spot-router).

System overview

spot-router forwards strings output based on data received by the TWELITE parent device (in ModBus ASCII format of App_Wings) to a WebSocket server.

Requirements for development

Environment setup

Installing IDE and toolchain

Please refer to How to set up a development environment with Arduino IDE 1.x.

Installing libraries

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

WebSocket library

  1. Download the Zip file from GitHub (Links2004/arduinoWebSockets)
  2. Extract the Zip file and place the arduinoWebSockets-<version> folder into the libraries folder

Obtaining the project files

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

Changing user settings

Open config.h from the top tab in Arduino IDE and modify the wireless LAN and WebSocket server settings (Details).

How to upload the project file

Please refer to How to upload sketches to ESP32.

Sketch

This is an explanation of the Arduino sketch spot-router.ino.

Including libraries

Arduino and ESP32 official libraries

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

#include <Arduino.h>
#include <WiFi.h>
Header fileDescriptionNotes
Arduino.hBasic Arduino librarySometimes can be omitted but included here for completeness
WiFi.hESP32 WiFi

Third-party libraries

Line 8 includes a third-party library.

#include <WebSocketsClient.h>
Header fileDescriptionNotes
WebSocketsClient.hActs as a WebSocket client

MWings library

Line 11 includes the MWings library.

#include <MWings.h>

Defining user settings

Line 14 includes config.h.

#include "config.h"

Defining wireless LAN settings

Lines 4-5 in config.h define wireless LAN settings applied to the ESP32 onboard TWELITE SPOT.

const char* WIFI_SSID = "YOUR SSID";            // Modify it
const char* WIFI_PASSWORD = "YOUR PASSWORD";    // Modify it
NameDescription
WIFI_SSIDSSID of the network to connect to
WIFI_PASSWORDPassword of the network to connect to

Defining WebSocket settings

Lines 8-10 in config.h define WebSocket client settings.

const char* WS_SERVER_IP = "YOUR ADDRESS";    // Modify it
const int WS_SERVER_PORT = 8080;
const char* WS_SERVER_PATH = "/";
NameDescription
WS_SERVER_IPIP address of the server to send to
WS_SERVER_PORTPort number of the server to send to
WS_SERVER_PATHPath of the WebSocket server to send to

Defining pin numbers

Lines 17-21 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 TWELITE’s RST pin
TWE_PRGPin number connected to TWELITE’s PRG pin
LEDPin number connected to the ESP32 onboard LED
ESP_RXD1Pin number connected to TWELITE’s TX pin
ESP_TXD1Pin number connected to TWELITE’s RX pin

Defining TWELITE settings

Lines 24-27 define settings applied to the TWELITE parent device onboard 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 retry count (when sending)
TWE_POWERTWELITE transmission power

Declaring global objects

Line 30 declares a global object.

WebSocketsClient webSocket;
NameDescription
webSocketWebSocket client interface

Declaring function prototypes

Line 33 declares a function prototype.

String createPacketStringFrom(const BarePacket& packet);
NameDescription
createPacketStringFrom()Reconstructs a formatted string from received packet data

Setting up TWELITE

Lines 42-47 call Twelite.begin() to configure and start the TWELITE parent device onboard 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.");
    }
ParameterTypeDescription
Serial2HardwareSerial&Serial port used for communication with TWELITE
LEDintPin number connected to status LED
TWE_RSTintPin number connected to TWELITE’s RST pin
TWE_PRGintPin number connected to TWELITE’s PRG pin
TWE_CHANNELuint8_tTWELITE frequency channel
TWE_APP_IDuint32_tTWELITE application ID
TWE_RETRYuint8_tTWELITE retry count (when sending)
TWE_POWERuint8_tTWELITE transmission power

Registering event handlers

Lines 49-54 register processing to be performed when packets are received from any client application.

Twelite.on([](const BarePacket& packet) {
    String packetStr = createPacketStringFrom(packet);
    if (not(packetStr.length() <= 0)) {
        webSocket.sendTXT(packetStr.c_str());
    }
});

Here, a formatted string (in ModBus ASCII format) is reconstructed from the packet data and sent to the WebSocket server.

Configuring wireless LAN

Lines 57-71 configure the wireless LAN.

WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
    static int count = 0;
    Serial.print('.');
    delay(500);
    // Retry every 5 seconds
    if (count++ % 10 == 0) {
        WiFi.disconnect();
        WiFi.reconnect();
        Serial.print('!');
    }
}

Here, the device is set as a wireless LAN client and connects to the specified network.

Configuring WebSocket

Lines 76-77 configure the WebSocket.

webSocket.begin(WS_SERVER_IP, WS_SERVER_PORT, WS_SERVER_PATH);
webSocket.setReconnectInterval(5000);

Here, the WebSocket server and reconnection interval are specified.

Also, lines 78-97 register events for when the connection to the server is disconnected, connected, and when messages are received.

webSocket.onEvent([](WStype_t type, uint8_t* payload, size_t length) {
    switch (type) {
    case WStype_DISCONNECTED: {
        Serial.println("Disconnected!");
        break;
    }
    case WStype_CONNECTED: {
        Serial.print("Connected to url: ");
        Serial.println(reinterpret_cast<char*>(payload));
        webSocket.sendTXT("This is TWELITE SPOT to ground control");
        break;
    }
    case WStype_TEXT: {
        Serial.print("Got text: ");
        Serial.println(reinterpret_cast<char*>(payload));
        break;
    }
    default: break;
    }
});

In particular, when connected to the server, a message is sent to the server.

webSocket.sendTXT("This is TWELITE SPOT to ground control");

Updating TWELITE data

Line 102 calls Twelite.update().

Twelite.update();

Twelite.update() reads packet data bytes (in ModBus ASCII format) sequentially from the TWELITE parent device.

Updating WebSocket data

Line 103 calls the process to update WebSocket data.

webSocket.loop();

Appendix: Verifying operation with WebSocket server

extra/python-websocket-server/server.py is a Python script that sets up a WebSocket server and displays packet data strings from the ESP32. Using this script, you can verify the sketch operation.

# -*- coding: utf-8-unix -*-
# Python 3.11

import logging
from websocket_server import WebsocketServer

def new_client(client, server):
    server.send_message_to_all("This is ground control to TWELITE SPOT")

def new_message(client, server, message):
    print("Received an message:")
    print(message)

server = WebsocketServer(host="YOUR IP ADDRESS", port=8080, loglevel=logging.INFO)
server.set_fn_new_client(new_client)
server.set_fn_message_received(new_message)
server.run_forever()

The coding variable is specified because the author’s environment is Emacs. It is not a magic spell.

Verification procedure

Running the script

Install dependencies and then run.


pip3 install websocket-server
python3 server.py

When running, the following messages appear.

INFO:websocket_server.websocket_server:Listening on port 8080 for clients..
INFO:websocket_server.websocket_server:Starting WebsocketServer on main thread.

Confirming client connection

When the ESP32 successfully connects to the wireless LAN, it attempts to connect to the WebSocket server.

Upon successful connection, the client-side serial console outputs as follows.

Started TWELITE.
Connecting to WiFi .....
Connected. IP: xxx.xxx.xxx.xxx
Connected to url: /
Got text: This is ground control to TWELITE SPOT

On the server-side terminal, the output is as follows.

Received an message:
This is TWELITE SPOT to ground control

Afterwards, when TWELITE SPOT receives packets from client devices, the packet data strings are output to the server terminal as follows.

Received an message:
:80000000DE10098201BC8201800607003400038135001205350401000000113008020A8C1130010203AF0000000180050100020AC60102000211D7AF30

Received an message:
:80000000E4100A8201BC8201800607003400038135001205350401000000113008020A8C1130010203AC0000000180050100020AC40102000211DB0DCC

TWELITE

Arduino

ESP32

Community

Libraries

Plugins

WebSocket

Community

3 - Using the REST API

An explanation of the sample sketch spot-httpbin, which uses data from a child device in an HTTP GET request
This is an explanation of the sample sketch spot-httpbin, which acts as a Wi-Fi child device and sends received packet data to the mock server httpbin.org on the web.

3.1 - Using REST API

Latest Edition
This is a sample sketch spot-httpbin that behaves as a Wi-Fi sub-device and sends received packet data to the mock server httpbin.org on the Web.

Obtaining the Source Code

Available on GitHub repository monowireless/spot-httpbin.

System Overview

spot-httpbin sends part of the data received by the TWELITE parent device and the current time obtained via NTP to the mock server as an HTTP GET request, and displays the response on the serial monitor.

Required Components for Development

Environment Setup

Installing IDE and Toolchain

See How to set up a development environment with Arduino IDE 1.x.

Installing Libraries

This sample includes all required libraries by default.

Getting the Project Files

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

Changing User Settings

Open ‘config.h’ from the top tab in Arduino IDE and set the Wi-Fi SSID and password. WPA2-PSK network is assumed. Also, register the root certificate. You can obtain the root certificate from the security panel of each page in web browsers such as Chrome.

Writing the Project Files

See How to upload the sketch to ESP32.

Sketch

Explanation of the Arduino sketch spot-httpbin.ino and config.h.

Including Libraries

Official Arduino and ESP32 Libraries

Lines 4–6 include official Arduino and ESP32 libraries.

#include <Arduino.h>
#include <WiFiClientSecure.h>
#include <WiFiUdp.h>
Header FileDescriptionNote
Arduino.hBasic Arduino librarySometimes can be omitted
WiFiClientSecure.hSSL communication on ESP32
WiFiUdp.hUDP communicationRequired for NTP

Third-party Libraries

Lines 9–10 include bundled third-party libraries.

#include "src/NTPClient/NTPClient.h"
#include "src/Time/TimeLib.h"
Header FileDescriptionNote
NTPClient.hAccess NTP servers
TimeLib.hConvert epoch time

MWings Library

Line 13 includes the MWings library.

#include <MWings.h>

User Configuration

Line 16 includes config.h.

#include "config.h"

Defining Data Types

Lines 19–26 define a structure type for storing data received from the sub-device.

struct DataFromAria {
    uint32_t serialId;
    uint8_t logicalId;
    uint16_t supplyVoltage;
    uint8_t linkQuality;
    int16_t temp100x;
    uint16_t humid100x;
};
NameDescription
serialIdSerial ID
logicalIdLogical device ID
supplyVoltageSupply voltage
linkQualityLQI
temp100xTemperature ×100
humid100xHumidity ×100

This structure assumes the use of TWELITE ARIA.

config.h

Defining Reboot Interval

Line 4 in config.h specifies the reboot interval for the ESP32.

const uint32_t REBOOT_INTERVAL = 21600; // seconds

21600 seconds = 6 hours.

Defining TWELITE Settings

Lines 7–8 in config.h define the settings to be applied to the TWELITE parent module mounted on TWELITE SPOT.

const uint8_t TWE_CH = 18;
const uint32_t TWE_APPID = 0x67720102;
NameDescription
TWE_CHTWELITE frequency channel
TWE_APPIDTWELITE application ID

Defining Wi-Fi Settings

Lines 11–12 in config.h define the Wi-Fi settings to be applied to the ESP32 mounted on TWELITE SPOT.

const char* WIFI_SSID = "YOUR SSID";
const char* WIFI_PASSWORD = "YOUR PASSWORD";
NameDescription
WIFI_SSIDSSID of the network to connect
WIFI_PASSWORDPassword for the network

Root Certificate

The template for the root certificate is provided at lines 14–16 in config.h.

const char *CA_CERT =
    "-----BEGIN CERTIFICATE-----\n"
    "-----END CERTIFICATE-----\n";

Obtain the root certificate from the security panel of the relevant page in browsers such as Chrome. Enclose each line in double quotes and append the newline character \n before the closing quote.

Defining Host Settings

Lines 18–19 in config.h define the host settings.

const char *SERVER_HOST = "www.httpbin.org";
const uint16_t SERVER_PORT = 443;
NameDescription
SERVER_HOSTHost name of the server
SERVER_PORTPort number of the server

Defining Constants

From line 21 in config.h, various constants are defined.

const uint32_t NTP_UPDATE_INTERVAL = 10000; // ms

const int QUERIES_MAX_LENGTH = 128;         // bytes (without \0)
const int32_t CONNECT_TIMEOUT = 10;     // seconds
const uint32_t RECONNECT_MIN_INTERVAL = 5; // seconds
// SEND_MIN_INTERVAL must be longer than NTP_UPDATE_INTERVAL
const uint32_t SEND_MIN_INTERVAL = 10; // seconds
const uint32_t REQUEST_TIMEOUT = 10;   // seconds
NameDescription
NTP_UPDATE_INTERVALInterval for obtaining NTP time
QUERIES_MAX_LENGTHMax length of query string (excluding null terminator)
CONNECT_TIMEOUTTimeout for connecting to the server
RECONNECT_MIN_INTERVALMinimum interval to reconnect to Wi-Fi AP
SEND_MIN_INTERVALMinimum interval between requests
REQUEST_TIMEOUTTimeout from request to response

Defining Pin Numbers

Lines 29–34 define pin numbers.

static const int RST_PIN = 5;
static const int PRG_PIN = 4;
static const int LED_PIN = 18;

static const int8_t RX1_PIN = 16;
static const int8_t TX1_PIN = 17;
NameDescription
RST_PINPin connected to the RST pin of TWELITE
PRG_PINPin connected to the PRG pin of TWELITE
LED_PINPin connected to the ESP32 LED on the board
RX1_PINPin connected to the RX1 pin of TWELITE
TX1_PINPin connected to the TX1 pin of TWELITE

Declaring Global Objects

Lines 37–40 declare global objects.

static WiFiClientSecure client;
static WiFiUDP ntpUDP;
static NTPClient timeClient(ntpUDP, "ntp.nict.jp",
                            32400, NTP_UPDATE_INTERVAL); // JST(UTC+9)
NameDescription
clientInterface for HTTPS communication
ntpUDPInterface for UDP communication for NTP
timeClientInterface for NTP

Declaring Global Variables

Lines 43–44 declare global variables.

static DataFromAria LatestDataFromAria;
static bool IsThereNewDataFromAria;
NameDescription
LatestDataFromAriaLatest data received from TWELITE ARIA
IsThereNewDataFromAriaFlag indicating new data was received from TWELITE ARIA

Declaring Function Prototypes

Lines 47–59 declare function prototypes.

void anotherLoopForTWELITE();
void anotherLoopForNTP();
NameDescription
anotherLoopForTWELITELoop function for processing TWELITE data
anotherLoopForNTPLoop function for retrieving time from NTP
void initTWELITE();
void initWiFi();
void initNTP();
NameDescription
initTWELITEInitialization function for TWELITE
initWiFiInitialization function for Wi-Fi
initNTPInitialization function for NTP
void onAppAriaPacket(const ParsedAppAriaPacket& packet);
NameDescription
onAppAriaPacketCallback function triggered when data is received from TWELITE ARIA
void sendAriaData(const DataFromAria& data)
NameDescription
sendAriaDataSends TWELITE ARIA data via HTTP GET request

setup()

Lines 62–90 perform the overall initialization.

void setup() {
    Serial.begin(115200);

    initTWELITE();
    initWiFi();
    initNTP();

    // Attach another loop function for TWELITE
    // Note: Core 0 is also used for the WiFi task, which priority is 19 (ESP_TASKD_EVENT_PRIO - 1)
    xTaskCreatePinnedToCore(
        [](void *params) {
            while (true) {
                anotherLoopForTWELITE();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },
        "Task for anotherLoopForTWELITE()", 8192, nullptr, 18, nullptr,
        0); // Priority is 18 (lower than WiFi)
    // Attach another loop function for NTP
    xTaskCreatePinnedToCore(
        [](void *params) {
            while (true) {
                anotherLoopForNTP();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },
        "Task for anotherLoopForNTP()", 8192, nullptr, 17, nullptr,
        0); // Priority is 17 (lower than WiFi and TWELITE)
}

By using xTaskCreatePinnedToCore(), tasks other than loop() are registered.

The following section is an anonymous lambda function with no capture. This avoids unnecessary pollution of the global namespace.

        [](void *params) {
            while (true) {
                anotherLoopForTWELITE();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },

loop()

Lines 93–114 are the main loop processing.

This section handles HTTP request processing, reconnection when Wi-Fi is disconnected, and periodic resets.

void loop() {
    static uint32_t lastTimeReconnected = 0;
    if (WiFi.status() == WL_CONNECTED) {
        // Regular operations
        // Check for new data
        if (IsThereNewDataFromAria) {
            IsThereNewDataFromAria = false; // Clear first; data is updated on another thread
            DataFromAria data = LatestDataFromAria; // Now, the buffer is open for incoming data
            sendAriaData(data);
        }
    } else if (millis() - lastTimeReconnected > RECONNECT_MIN_INTERVAL * 1000) {
        // Lost connection, reconnect periodically
        Serial.println("Disconnected. Reconnecting to WiFi...");
        WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
        lastTimeReconnected = millis();
    }
    // Reboot every x interval
    if (millis() > REBOOT_INTERVAL * 1000) {
        Serial.println("Rebooting...");
        ESP.restart();
    }
}

anotherLoopForTWELITE()

Lines 117–119 are the loop processing for TWELITE.

To sequentially receive and interpret data, this is made a separate task from loop(), which may contain blocking processing.

void anotherLoopForTWELITE() {
    Twelite.update();
}

anotherLoopForNTP()

Lines 120–123 are the loop processing for NTP.

This is also made a separate task from loop() because UDP communication may involve blocking processing.

void anotherLoopForNTP() {
    timeClient.update();
    setTime(timeClient.getEpochTime());
}

initTWELITE()

Lines 126–133 are the initialization process for TWELITE.

This starts the TWELITE mounted on TWELITE SPOT with the specified settings and registers a callback function for packet reception.

void initTWELITE() {
    Serial2.begin(115200);
    if (Twelite.begin(Serial2, LED_PIN, RST_PIN, PRG_PIN, TWE_CHANNEL, TWE_APP_ID)) {
        Serial.println("Started TWELITE.");
    }
    // Attach event handlers to process packets
    Twelite.on(onAppAriaPacket);
}

initWiFi()

Lines 136–160 are the Wi-Fi initialization process.

If not connected, it retries to connect every 5 seconds.

void initWiFi() {
    Serial.print("\nConnecting to the WiFi network ");
    Serial.print(WIFI_SSID);
    Serial.println("...");
    // Begin
    WiFi.mode(WIFI_STA);
    WiFi.setAutoReconnect(true);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    // Wait for connection
    Serial.print("Connecting.");
    while (WiFi.status() != WL_CONNECTED) {
        static int count = 0;
        Serial.print('.');
        delay(500);
        // Retry every 5 seconds
        if (count++ % 10 == 0) {
            WiFi.disconnect();
            WiFi.reconnect();
            Serial.print('!');
        }
    }
    Serial.println("\nConnected!");
    // Set Root CA certificate
    client.setCACert(CA_CERT);
}

initNTP()

Lines 163–167 are the initialization process for NTP.

void initNTP() {
    timeClient.begin();
    timeClient.update();
    setTime(timeClient.getEpochTime());
}

onAppAriaPacket()

Lines 170–180 describe the processing when data is received from TWELITE ARIA.

Here, HTTP transmission is not performed; instead, the data is set to a global variable. The data set to the global variable is processed by sendAriaData() in another task.

void onAppAriaPacket(const ParsedAppAriaPacket& packet)
{
    // Store data
    LatestDataFromAria.serialId = packet.u32SourceSerialId;
    LatestDataFromAria.logicalId = packet.u8SourceLogicalId;
    LatestDataFromAria.supplyVoltage = packet.u16SupplyVoltage;
    LatestDataFromAria.linkQuality = packet.u8Lqi;
    LatestDataFromAria.temp100x = packet.i16Temp100x;
    LatestDataFromAria.humid100x = packet.u16Humid100x;
    IsThereNewDataFromAria = true;
}

sendAriaData()

Lines 183–240 are a function that sets the data from TWELITE ARIA into the query string of an HTTP GET request and sends it.

To prevent excessive server load, sending is skipped if packets arrive too frequently.

void sendAriaData(const DataFromAria& data)
{
    static uint32_t lastTimeRequested = 0;
    if (millis() - lastTimeRequested > SEND_MIN_INTERVAL * 1000 or lastTimeRequested == 0) {
        Serial.println("Connecting to the server...");
        if (not client.connect(SERVER_HOST, SERVER_PORT, CONNECT_TIMEOUT * 1000)) {
            Serial.println("Connection failed!");
        } else {
            Serial.println("Connected to the server!");
            // Make a query string
            char queries[QUERIES_MAX_LENGTH+1];
            snprintf(queries, sizeof(queries),
                     "datetime=%04d%02d%02d%02d%02d%02d&sid=%X&lid=%d&temp=%d&humid=%d&bat=%d&lqi=%d",
                     // Note that NTP_UPDATE_INTERVAL is set for 10000ms by default; second() delays up to 10s.
                     // To prevent duplication of datetime, SEND_MIN_INTERVAL is set for 10s.
                     year(), month(), day(), hour(), minute(), second(),
                     data.serialId,
                     data.logicalId,
                     data.temp100x,
                     data.humid100x,
                     data.supplyVoltage,
                     data.linkQuality);

            // Send a request
            client.println(String("GET https://") +
                           SERVER_HOST +
                           String("/get?") +
                           queries +
                           String(" HTTP/1.1"));
            client.println("Accept: */*");
            client.println(String("Host: ") + SERVER_HOST);
            client.println("Connection: close");
            client.println();
            uint32_t timeSentRequest = millis();

            // Handle a response
            while (client.connected()) {
                String line = client.readStringUntil('\n');
                if (line == "\r") {
                    Serial.println("Headers received");
                    break;
                }
                if (millis() - timeSentRequest > REQUEST_TIMEOUT * 1000) {
                    Serial.println("Request was timed out");
                    break;
                }
            }
            while (client.available()) {
                char c = client.read();
                Serial.write(c);
            }
            client.stop();
        }
        lastTimeRequested = millis();
    } else {
        Serial.println("Requests are too frequently; skip.");
    }
}

3.2 - Using the REST API

mwings-v1.1.3
This is an explanation of the sample sketch spot-httpbin, which acts as a Wi-Fi child device and sends received packet data to the mock server httpbin.org.

Getting the Source Code

You can obtain it from the GitHub repository monowireless/spot-httpbin.

System Overview

spot-httpbin sends part of the data received by the TWELITE parent device along with the current time obtained via NTP as an HTTP GET request to a mock server, and displays the response on the serial monitor.

Requirements for Development

Environment Setup

Installing IDE and Toolchain

See Setting up the development environment using Arduino IDE 1.x.

Installing Libraries

This sample includes required libraries from the beginning.

Obtaining the Project Files

  1. Download the ZIP file from GitHub (monowireless/spot-httpbin)
  2. Extract the ZIP file and rename the folder from spot-httpbin-main to spot-httpbin
  3. Place the spot-httpbin folder in your Arduino sketchbook directory (set in Arduino IDE preferences, e.g. C:\Users\foo\Documents\Arduino)

Modifying User Settings

Open config.h from the tabs at the top of the Arduino IDE and set your Wi-Fi SSID and password. WPA2-PSK network is assumed. Also, register the root certificate. The root certificate can be obtained from the security page for each website using Chrome or another browser.

Writing the Project File

See How to upload a sketch to the ESP32.

Sketch

This section explains the Arduino sketch spot-httpbin.ino and config.h.

Including Libraries

Official Arduino and ESP32 Libraries

Lines 4–6 include official Arduino and ESP32 libraries.

#include <Arduino.h>
#include <WiFiClientSecure.h>
#include <WiFiUdp.h>
Header FileDescriptionRemarks
Arduino.hBasic Arduino libraryCan sometimes be omitted, but included for safety
WiFiClientSecure.hEnables SSL communication on ESP32
WiFiUdp.hHandles UDP communicationRequired for NTP

Third-Party Libraries

Lines 9–10 include bundled third-party libraries.

#include "src/NTPClient/NTPClient.h"
#include "src/Time/TimeLib.h"
Header FileDescriptionRemarks
NTPClient.hAccesses NTP servers
TimeLib.hConverts epoch time

MWings Library

Line 13 includes the MWings library.

#include <MWings.h>

Defining User Settings

Line 16 includes the configuration file.

#include "config.h"

Definition of Data Types

Lines 19–26 define the struct type used to store data received from the child device.

struct DataFromAria {
    uint32_t serialId;
    uint8_t logicalId;
    uint16_t supplyVoltage;
    uint8_t linkQuality;
    int16_t temp100x;
    uint16_t humid100x;
};
NameDescription
serialIdSerial ID
logicalIdLogical device ID
supplyVoltageSupply voltage
linkQualityLQI (Link Quality Index)
temp100xTemperature ×100
humid100xHumidity ×100

Here, TWELITE ARIA is used.

config.h

Definition of Reboot Interval

Line 4 of config.h specifies the reboot interval for the ESP32.

const uint32_t REBOOT_INTERVAL = 21600; // seconds

Here, 21600 seconds = 6 hours.

Definition of TWELITE Settings

Lines 7–8 of config.h define the settings applied to the TWELITE parent device installed in TWELITE SPOT.

const uint8_t TWE_CH = 18;
const uint32_t TWE_APPID = 0x67720102;
NameDescription
TWE_CHTWELITE channel
TWE_APPIDTWELITE application ID

Definition of Wi-Fi Settings

Lines 11–12 of config.h define the Wi-Fi settings applied to the ESP32 installed in TWELITE SPOT.

const char* WIFI_SSID = "YOUR SSID";
const char* WIFI_PASSWORD = "YOUR PASSWORD";
NameDescription
WIFI_SSIDSSID of the network to connect to
WIFI_PASSWORDPassword of the network to connect to

Root Certificate

Lines 14–16 of config.h provide a template for describing the contents of the root certificate.

const char *CA_CERT =
    "-----BEGIN CERTIFICATE-----\n"
    "-----END CERTIFICATE-----\n";

Obtain the root certificate from the security screen for each website using Chrome or another web browser. All lines must be enclosed in double quotes, and a newline character \n must be added before the ending double quote.

Host Settings

Lines 18–19 of config.h define the host settings.

const char *SERVER_HOST = "www.httpbin.org";
const uint16_t SERVER_PORT = 443;
NameDescription
SERVER_HOSTServer host name
SERVER_PORTServer port number

Definition of Various Constants

From line 21 of config.h, various constants are defined.

const uint32_t NTP_UPDATE_INTERVAL = 10000; // ms

const int QUERIES_MAX_LENGTH = 128;         // bytes (without \0)
const int32_t CONNECT_TIMEOUT = 10;     // seconds
const uint32_t RECONNECT_MIN_INTERVAL = 5; // seconds
// SEND_MIN_INTERVAL must be longer than NTP_UPDATE_INTERVAL
const uint32_t SEND_MIN_INTERVAL = 10; // seconds
const uint32_t REQUEST_TIMEOUT = 10;   // seconds
NameDescription
NTP_UPDATE_INTERVALInterval for obtaining NTP time
QUERIES_MAX_LENGTHMaximum length of query string (excluding null character)
CONNECT_TIMEOUTTimeout when connecting to the server
RECONNECT_MIN_INTERVALMinimum interval when reconnecting to Wi-Fi access point
SEND_MIN_INTERVALMinimum interval between requests
REQUEST_TIMEOUTTimeout from request to response

Definition of Pin Numbers

Lines 29–31 define the pin numbers.

static const int RST_PIN = 5;
static const int PRG_PIN = 4;
static const int LED_PIN = 18;
NameDescription
RST_PINPin number connected to TWELITE’s RST pin
PRG_PINPin number connected to TWELITE’s PRG pin
LED_PINPin number connected to the ESP32 LED on the board

Declaration of Global Objects

Lines 34–37 declare global objects.

static WiFiClientSecure client;
static WiFiUDP ntpUDP;
static NTPClient timeClient(ntpUDP, "ntp.nict.jp",
                            32400, NTP_UPDATE_INTERVAL); // JST(UTC+9)
NameDescription
clientInterface for HTTPS communication
ntpUDPInterface for UDP communication for NTP
timeClientInterface for NTP

Declaration of Global Variables

Lines 40–41 declare global variables.

static DataFromAria LatestDataFromAria;
static bool IsThereNewDataFromAria;
NameDescription
LatestDataFromAriaLatest data received from TWELITE ARIA
IsThereNewDataFromAriaFlag indicating new data has been received from TWELITE ARIA

Declaration of Function Prototypes

Lines 44–56 declare function prototypes.

void anotherLoopForTWELITE();
void anotherLoopForNTP();
NameDescription
anotherLoopForTWELITELoop function for processing TWELITE data
anotherLoopForNTPLoop function for obtaining time via NTP
void initTWELITE();
void initWiFi();
void initNTP();
NameDescription
initTWELITEFunction to initialize TWELITE
initWiFiFunction to initialize Wi-Fi
initNTPFunction to initialize NTP
void onAppAriaPacket(const ParsedAppAriaPacket& packet);
NameDescription
onAppAriaPacketCallback function when data is received from TWELITE ARIA
void sendAriaData(const DataFromAria& data)
NameDescription
sendAriaDataFunction to send TWELITE ARIA data via HTTP GET request

setup()

Lines 59–87 perform overall initialization.

void setup() {
    Serial.begin(115200);

    initTWELITE();
    initWiFi();
    initNTP();

    // Attach another loop function for TWELITE
    // Note: Core 0 is also used for the WiFi task, which priority is 19 (ESP_TASKD_EVENT_PRIO - 1)
    xTaskCreatePinnedToCore(
        [](void *params) {
            while (true) {
                anotherLoopForTWELITE();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },
        "Task for anotherLoopForTWELITE()", 8192, nullptr, 18, nullptr,
        0); // Priority is 18 (lower than WiFi)
    // Attach another loop function for NTP
    xTaskCreatePinnedToCore(
        [](void *params) {
            while (true) {
                anotherLoopForNTP();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },
        "Task for anotherLoopForNTP()", 8192, nullptr, 17, nullptr,
        0); // Priority is 17 (lower than WiFi and TWELITE)
}

xTaskCreatePinnedToCore() is used to register a separate task from the loop() function.

The following part is an unnamed function without a capture. This avoids unnecessary pollution of the global namespace.

        [](void *params) {
            while (true) {
                anotherLoopForTWELITE();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },

loop()

Lines 90–111 are the main loop process.

This handles HTTP requests, reconnection to Wi-Fi when disconnected, and periodic resets.

void loop() {
    static uint32_t lastTimeReconnected = 0;
    if (WiFi.status() == WL_CONNECTED) {
        // Regular operations
        // Check for new data
        if (IsThereNewDataFromAria) {
            IsThereNewDataFromAria = false; // Clear first; data is updated on another thread
            DataFromAria data = LatestDataFromAria; // Now, the buffer is open for incoming data
            sendAriaData(data);
        }
    } else if (millis() - lastTimeReconnected > RECONNECT_MIN_INTERVAL * 1000) {
        // Lost connection, reconnect periodically
        Serial.println("Disconnected. Reconnecting to WiFi...");
        WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
        lastTimeReconnected = millis();
    }
    // Reboot every x interval
    if (millis() > REBOOT_INTERVAL * 1000) {
        Serial.println("Rebooting...");
        ESP.restart();
    }
}

anotherLoopForTWELITE()

Lines 114–116 are the loop process for TWELITE.

To sequentially receive and interpret data, this is run as a separate task from the (potentially blocking) loop().

void anotherLoopForTWELITE() {
    Twelite.update();
}

anotherLoopForNTP()

Lines 117–120 are the loop process for NTP.

Since this involves UDP communication, it is also run as a separate task from the (potentially blocking) loop().

void anotherLoopForNTP() {
    timeClient.update();
    setTime(timeClient.getEpochTime());
}

initTWELITE()

Lines 123–130 perform the initialization process for TWELITE.

This starts the TWELITE installed in TWELITE SPOT with the specified settings and registers a callback function for packet reception.

void initTWELITE() {
    Serial2.begin(115200);
    if (Twelite.begin(Serial2, LED_PIN, RST_PIN, PRG_PIN, TWE_CHANNEL, TWE_APP_ID)) {
        Serial.println("Started TWELITE.");
    }
    // Attach event handlers to process packets
    Twelite.on(onAppAriaPacket);
}

initWiFi()

Lines 133–157 perform Wi-Fi initialization.

If not connected, reconnection is attempted every 5 seconds.

void initWiFi() {
    Serial.print("\nConnecting to the WiFi network ");
    Serial.print(WIFI_SSID);
    Serial.println("...");
    // Begin
    WiFi.mode(WIFI_STA);
    WiFi.setAutoReconnect(true);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    // Wait for connection
    Serial.print("Connecting.");
    while (WiFi.status() != WL_CONNECTED) x
        static int count = 0;
        Serial.print('.');
        delay(500);
        // Retry every 5 seconds
        if (count++ % 10 == 0) {
            WiFi.disconnect();
            WiFi.reconnect();
            Serial.print('!');
        }
    }
    Serial.println("\nConnected!");
    // Set Root CA certificate
    client.setCACert(CA_CERT);
}

initNTP()

Lines 160–164 perform NTP initialization.

void initNTP() {
    timeClient.begin();
    timeClient.update();
    setTime(timeClient.getEpochTime());
}

onAppAriaPacket()

Lines 167–177 describe the process performed when data is received from TWELITE ARIA.

Here, HTTP transmission is not performed; instead, the data is set to a global variable. The data set in the global variable is processed by sendAriaData() in a separate task.

void onAppAriaPacket(const ParsedAppAriaPacket& packet)
{
    // Store data
    LatestDataFromAria.serialId = packet.u32SourceSerialId;
    LatestDataFromAria.logicalId = packet.u8SourceLogicalId;
    LatestDataFromAria.supplyVoltage = packet.u16SupplyVoltage;
    LatestDataFromAria.linkQuality = packet.u8Lqi;
    LatestDataFromAria.temp100x = packet.i16Temp100x;
    LatestDataFromAria.humid100x = packet.u16Humid100x;
    IsThereNewDataFromAria = true;
}

sendAriaData()

Lines 180–237 define a function that sets TWELITE ARIA data into the query string of an HTTP GET request and sends it.

To avoid excessive load on the server, transmission is skipped if packets arrive too frequently.

void sendAriaData(const DataFromAria& data)
{
    static uint32_t lastTimeRequested = 0;
    if (millis() - lastTimeRequested > SEND_MIN_INTERVAL * 1000 or lastTimeRequested == 0) {
        Serial.println("Connecting to the server...");
        if (not client.connect(SERVER_HOST, SERVER_PORT, CONNECT_TIMEOUT * 1000)) {
            Serial.println("Connection failed!");
        } else {
            Serial.println("Connected to the server!");
            // Make a query string
            char queries[QUERIES_MAX_LENGTH+1];
            snprintf(queries, sizeof(queries),
                     "datetime=%04d%02d%02d%02d%02d%02d&sid=%X&lid=%d&temp=%d&humid=%d&bat=%d&lqi=%d",
                     // Note that NTP_UPDATE_INTERVAL is set for 10000ms by default; second() delays up to 10s.
                     // To prevent duplication of datetime, SEND_MIN_INTERVAL is set for 10s.
                     year(), month(), day(), hour(), minute(), second(),
                     data.serialId,
                     data.logicalId,
                     data.temp100x,
                     data.humid100x,
                     data.supplyVoltage,
                     data.linkQuality);

            // Send a request
            client.println(String("GET https://") +
                           SERVER_HOST +
                           String("/get?") +
                           queries +
                           String(" HTTP/1.1"));
            client.println("Accept: */*");
            client.println(String("Host: ") + SERVER_HOST);
            client.println("Connection: close");
            client.println();
            uint32_t timeSentRequest = millis();

            // Handle a response
            while (client.connected()) {
                String line = client.readStringUntil('\n');
                if (line == "\r") {
                    Serial.println("Headers received");
                    break;
                }
                if (millis() - timeSentRequest > REQUEST_TIMEOUT * 1000) {
                    Serial.println("Request was timed out");
                    break;
                }
            }
            while (client.available()) {
                char c = client.read();
                Serial.write(c);
            }
            client.stop();
        }
        lastTimeRequested = millis();
    } else {
        Serial.println("Requests are too frequently; skip.");
    }
}

4 - Using Google Sheets

Explanation of the sample sketch ‘spot-google-sheets’ for uploading data from TWELITE ARIA to Google Sheets
This section explains the sample sketch spot-google-sheets, which acts as a Wi-Fi client and uploads data received from TWELITE ARIA to Google Sheets in the cloud.

4.1 - Using Google Sheets

Latest Edition
This is an explanation of the sample sketch spot-google-sheets, which acts as a Wi-Fi client and uploads data received from TWELITE ARIA to Google Sheets in the cloud. This sketch uses FreeRTOS functions from the ESP32 Arduino environment.

Getting the Source Code

You can obtain it from GitHub (monowireless/spot-google-sheets).

System Overview

TWELITE SPOT automatically creates a spreadsheet using a pre-created service account and shares the file with a specified user account.

By logging into the user account, the user can view and edit the spreadsheet created by TWELITE SPOT from the “Shared with me” page in Google Drive.

Image of the created spreadsheet

Image of the created spreadsheet

TWELITE SPOT continuously adds data rows to the created spreadsheet.

Requirements for Development

Environment Setup

Installing the IDE and Toolchain

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

Installing Libraries

ESP-Google-Sheet-Client Library

Open the Library Manager and search for esp-google-sheet to install it. You can also obtain it from GitHub (mobizt/ESP-Google-Sheet-Client).

Official NTP Library

Open the Library Manager and search for ntpclient to install it.

TimeLib Library

Open the Library Manager and search for timelib to install it.

Preliminary Setup: API Configuration

Before using the API, you need to set up your environment. A Google account is required.

The following steps will be performed here:

  • Create a Google Cloud project
  • Enable the Google Sheets API
  • Enable the Google Drive API
  • Create and configure a service account
  • Obtain authentication credentials for the service account

Creating the Project

To use the API, first create a Google Cloud project.

A Google Cloud project encompasses the entire system. It’s recommended to name the project after the system you’re building. Here, we will use SPOT-DEV as an example.

Visit the following link and create a project.

https://console.cloud.google.com/projectcreate

Example screen for creating a project (personal)

Example screen for creating a project (personal)

Enabling the Sheets API

To operate spreadsheets from TWELITE SPOT, enable the Sheets API.

Visit the following link and enable the API.

https://console.cloud.google.com/apis/library/sheets.googleapis.com

Example screen for enabling the Sheets API

Example screen for enabling the Sheets API

Enabling the Drive API

To share spreadsheets from TWELITE SPOT, enable the Drive API.

Visit the following link and enable the API.

https://console.cloud.google.com/apis/library/drive.googleapis.com

Example screen for enabling the Drive API

Example screen for enabling the Drive API

Creating and Configuring the Service Account

To create spreadsheets from TWELITE SPOT, you need to create a service account.

Visit the following link, select your project (here, SPOT-DEV), and display the list of service accounts. Then, use the button at the top of the page to begin creating a service account.

https://console.cloud.google.com/iam-admin/serviceaccounts

Example screen showing the list of service accounts

Example screen showing the list of service accounts

In “① Service account details”, enter the name of the service account.

In the example below, the name is set as spot-dev-sa.

Example screen for entering the service account name

Example screen for entering the service account name

After entering the name, click the “Create and continue” button to proceed.

In “② Grant this service account access to the project (optional)”, configure the permissions for the service account.

Here, select “Owner” as shown in the example below.

Example screen for entering service account permissions

Example screen for entering service account permissions

After selecting the role, click the “Continue” button to proceed.

In “③ Grant users access to this service account (optional)”, do nothing and click “Done” to skip.

Example screen to be skipped

Example screen to be skipped

Once the service account is created, you will return to the service account list. Verify that the newly created service account appears.

Obtaining Service Account Credentials

Once you have confirmed the created service account, click the link in the “Email” column to open the service account details page.

Example screen after creating a service account

Example screen after creating a service account

Select the “Keys” tab at the top to navigate to the screen for managing the private key required for service account authentication.

Example screen of the service account details page

Example screen of the service account details page

Click the “Add Key” button and select “Create new key” to begin creating a private key.

Example of the key creation button

Example of the key creation button

On the next screen, leave “JSON” selected and click the “Create” button.

Example screen for selecting the key type

Example screen for selecting the key type

Clicking the “Create” button will automatically download the private key file (.json).

When you open the private key file in a text editor, it should look like the following.

{
  "type": "service_account",
  "project_id": "???",
  "private_key_id": "???",
  "private_key": "-----BEGIN PRIVATE KEY-----\n???\n-----END PRIVATE KEY-----\n",
  "client_email": "???@???.iam.gserviceaccount.com",
  "client_id": "???",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "???",
  "universe_domain": "googleapis.com"
}

Among the above, the values of project_id, private_key, and client_email will be used during the operation check.

Operation Check

Let’s start by checking the operation.

Obtaining the Project Files

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

Editing the Sketch Configuration File

Open the Arduino sketch spot-google-sheets.ino, select the config.h tab at the top of the screen, and modify the values from lines 4 to 11.

Lines 4–5 are Wi-Fi related settings.

const char* WIFI_SSID = "YOUR SSID";            // Modify it
const char* WIFI_PASSWORD = "YOUR PASSWORD";    // Modify it

These specify the SSID and password.

On the other hand, lines 8–11 are spreadsheet-related settings.

const char* PROJECT_ID = "YOUR-PROJECT-ID";                                                                         // Modify it
const char* SERVICE_ACCOUNT_EMAIL = "YOUR-SERVICE-ACCOUNT@YOUR-PROJECT-ID.iam.gserviceaccount.com";                 // Modify it
const char PRIVATE_KEY[] PROGMEM = "-----BEGIN PRIVATE KEY-----\nYOUR-PRIVATE-KEY\n-----END PRIVATE KEY-----\n";    // Modify it
const char* USER_ACCOUNT_EMAIL = "YOUR-ACCOUNT@EMAIL";                                                              // Modify it

For the first three items, copy the values from the .json file, and for USER_ACCOUNT_EMAIL, enter your logged-in Google account email address.

Uploading the Sketch

Refer to How to upload a sketch to ESP32 to upload the sketch.

Starting the Parent and Child Devices

Press the reset button on TWELITE SPOT (ESP32 side).

If you see the following output in the Arduino serial console, the startup was successful.

Initializing queue...
Completed.
Started TWELITE.
Connecting to WiFi ...!...
Connected. IP: xxx.xxx.xxx.xxx
Initializing NTP...Completed. UNIX time: xxxxxxxxxx
Initializing sheets...
Creating sheets...
OAuth2.0 access token on initializing
OAuth2.0 access token on signing
OAuth2.0 access token on exchange request
OAuth2.0 access token ready
Requesting to create...
Succeeded.
Adding headers for ARIA...
Requesting to add header...
Succeeded.
Formatting the sheet for ARIA...
Requesting to format...
Succeeded.
Extending the sheet for ARIA...
Requesting to extend...
Succeeded.
Completed.

Insert the coin battery into TWELITE ARIA (with default settings) and power it on.

Inserting the coin battery

Inserting the coin battery

When TWELITE SPOT successfully receives packets from TWELITE ARIA and adds the data rows, you will see the following output.

Got a new packet from ARIA.
Got a new packet from ARIA.
Requesting to add data...
Got a new packet from ARIA.
Succeeded.

Incidentally, in the example above, packets are being received during a request — this indicates successful multitasking (see Task Registration).

Accessing Google

Access Shared with me on Google Drive and open the spreadsheet named SPOT Sheet (xxx).

You should see a screen similar to the one below.

Example of the spreadsheet screen

Example of the spreadsheet screen

Scroll down to find the data received from TWELITE ARIA.

Sketch Explanation

This section explains the Arduino sketch spot-google-sheets.ino.

Including Libraries

Official Arduino and ESP32 Libraries

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

#include <Arduino.h>
#include <NTPClient.h>
#include <WiFi.h>
#include <WiFiUdp.h>
Header FileDescriptionRemarks
Arduino.hBasic Arduino library
NTPClient.hUse NTPUsed for file name and receive time
WiFi.hUse ESP32 WiFi
WiFiUdp.hUse UDPRequired for NTPClient

Third-Party Libraries

Lines 10-11 include third-party libraries.

#include <ESP_Google_Sheet_Client.h>
#include <TimeLib.h>
Header FileDescriptionRemarks
ESP_Google_Sheet_Client.hAccess Google
TimeLib.hFormat UNIX time

MWings Library

Line 14 includes the MWings library.

#include <MWings.h>

User Configuration Definitions

Line 17 includes config.h.

#include "config.h"

Wi-Fi Configuration Definition

Lines 4-5 of config.h define the Wi-Fi settings applied to the ESP32 on TWELITE SPOT.

const char* WIFI_SSID = "YOUR SSID";            // Modify it
const char* WIFI_PASSWORD = "YOUR PASSWORD";    // Modify it
NameDescription
WIFI_SSIDSSID of the network to connect to
WIFI_PASSWORDPassword of the network to connect to

API Configuration Definition

Lines 8-11 of config.h define the API settings.

const char* PROJECT_ID = "YOUR-PROJECT-ID";                                                                         // Modify it
const char* SERVICE_ACCOUNT_EMAIL = "YOUR-SERVICE-ACCOUNT@YOUR-PROJECT-ID.iam.gserviceaccount.com";                 // Modify it
const char PRIVATE_KEY[] PROGMEM = "-----BEGIN PRIVATE KEY-----\nYOUR-PRIVATE-KEY\n-----END PRIVATE KEY-----\n";    // Modify it
const char* USER_ACCOUNT_EMAIL = "YOUR-ACCOUNT@EMAIL";                                                              // Modify it
NameDescription
PROJECT_IDProject ID
SERVICE_ACCOUNT_EMAILEmail address of the service account
PRIVATE_KEYContent of the private key
USER_ACCOUNT_EMAILEmail address of the user account to share the spreadsheet with

Definition of Pin Numbers

Lines 20-24 define the 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 LED on the board
ESP_RXD1Pin number connected to the TX pin of TWELITE
ESP_TXD1Pin number connected to the RX pin of TWELITE

Definition of TWELITE Settings

Lines 27-30 define the settings applied to the TWELITE parent device 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 (when transmitting)
TWE_POWERTWELITE transmission output

Lines 32-43 define information related to the sheet.

const char* SPREADSHEET_TITLE_PREFIX = "SPOT Sheet";
const char* SPREADSHEET_LOCALE = "ja_JP";
const char* SPREADSHEET_TIME_ZONE = "Asia/Tokyo";

const int MIN_REQUEST_INTERVAL = 1000;    // 60 requests per minute

const int SHEETS_DEFAULT_ROWS = 1000;            // Default length is 1000 rows
const int SHEETS_ROWS = 100000;                  // Max 1,000,000 rows at 10 columns

const int ARIA_SHEET_ID = 1;
const char* ARIA_SHEET_TITLE = "ARIA";
constexpr int ARIA_BUFFER_PACKETS = 32;    // Maximum number of rows per addition request
NameDescription
SPREADSHEET_TITLE_PREFIXFixed part of the spreadsheet file name
SPREADSHEET_LOCALESpreadsheet locale
SPREADSHEET_TIME_ZONESpreadsheet time zone
MIN_REQUEST_INTERVALMinimum interval between requests
SHEETS_DEFAULT_ROWSDefault number of rows per sheet
SHEETS_ROWSNumber of rows per sheet
ARIA_SHEET_IDSheet ID for ARIA
ARIA_SHEET_TITLESheet name for ARIA
ARIA_BUFFER_PACKETSQueue length for storing packets from ARIA

Type Declarations

Lines 46-50 declare types.

struct ParsedAppAriaPacketWithTime {
    ParsedAppAriaPacket packet;
    uint32_t elapsedMillis;
    uint32_t unixTime;
};
NameDescription
ParsedAppAriaPacketWithTimeType for storing received packet data together with the reception time in the queue
  • elapsedMillis: Elapsed time since startup at the time the packet was received (milliseconds)
  • unixTime: UNIX time (seconds) when the packet was received

Declaration of Global Objects

Lines 53-61 declare global objects.

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "ntp.nict.jp", 32400);

String spreadsheetIdString;    // Identifier of newly created file
bool readyForNewRequests = false;
uint32_t lastTimeRequestWasSent = UINT32_MAX;

QueueHandle_t ariaPacketQueue;       // Store received data from ARIA
uint32_t rowToAddNewAriaData = 2;    // Starting with the Row 2
NameDescription
ntpUDPUDP interface for NTP
timeClientNTP interface
spreadsheetIdStringID of the created spreadsheet
readyForNewRequestsBecomes true when ready to send new requests
lastTimeRequestWasSentTime when the last request was sent
ariaPacketQueueQueue to store packets and reception times received from ARIA
rowToAddNewAriaDataRow number to add the next data received from ARIA

Function Prototype Declarations

Lines 64-71 declare function prototypes.

void anotherLoop();

void waitUntilNewRequestsReady();
String createSpreadsheet();
bool formatSheet(const String spreadsheetId, const int sheetId);
bool extendSheet(const String spreadsheetId, const int sheetId, const int rows);
bool addSheetAriaHeaderRow(const String spreadsheetId, const char* const sheetTitle);
bool addSheetsDataRow(const String spreadsheetId);
NameDescription
anotherLoop()Another loop() for asynchronously processing TWELITE
waitUntilNewRequestsReady()Wait until the next request can be sent
createSpreadsheet()Create a new spreadsheet
formatSheet()Format the specified sheet
extendSheet()Increase the number of rows in the specified sheet and format it
addSheetAriaHeaderRow()Add a header row for ARIA to the specified sheet
addSheetsDataRow()Add a data row to the sheet

Queue Initialization

Lines 82-83 initialize the queue for storing received packet data along with the reception time.

ariaPacketQueue = xQueueCreate(ARIA_BUFFER_PACKETS, sizeof(ParsedAppAriaPacketWithTime));
if (ariaPacketQueue == 0) { Serial.println("Failed to init a queue."); }

xQueueCreate() is a FreeRTOS function running inside the ESP32. It allows you to easily create queues that support multitasking.

TWELITE Configuration

Lines 88-92 call Twelite.begin() to configure and start the TWELITE parent device mounted on 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 (when transmitting)
TWE_POWERuint8_tTWELITE transmission output

Event Handler Registration

Lines 94-103 register the process to be executed when a packet is received from TWELITE ARIA.

Twelite.on([](const ParsedAppAriaPacket& packet) {
    Serial.println("Got a new packet from ARIA.");
    ParsedAppAriaPacketWithTime packetWithTime;
    packetWithTime.elapsedMillis = millis();
    packetWithTime.unixTime = timeClient.getEpochTime();
    packetWithTime.packet = packet;
    if (not(xQueueSend(ariaPacketQueue, &packetWithTime, 0) == pdPASS)) {
        Serial.println("Failed to add packet data to the queue.");
    }
});

Here, xQueueSend() is used to store the received packet data along with the reception time at the end of the queue.

Wi-Fi Settings

Lines 106-120 perform the Wi-Fi settings.

WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
    static int count = 0;
    Serial.print('.');
    delay(500);
    // Retry every 5 seconds
    if (count++ % 10 == 0) {
        WiFi.disconnect();
        WiFi.reconnect();
        Serial.print('!');
    }
}

Here, it is set as a Wi-Fi station and connects to the specified network.

NTP Settings

Lines 126-127 perform the NTP settings.

timeClient.begin();
timeClient.update();

Google Spreadsheet Settings

Lines 132-145 configure the Google Spreadsheet.

GSheet.setTokenCallback([](TokenInfo info) {
    // Print token initialization states
    if (info.status == esp_signer_token_status_error) {
        Serial.print("Token error ");
        Serial.println(GSheet.getTokenError(info));
    }
    Serial.print(GSheet.getTokenType(info));
    Serial.print(" ");
    Serial.println(GSheet.getTokenStatus(info));
});
GSheet.setPrerefreshSeconds(60);    // Set refresh rate for auth token

Serial.println("Initializing sheets...");
GSheet.begin(SERVICE_ACCOUNT_EMAIL, PROJECT_ID, PRIVATE_KEY);

In lines 132-141, it registers processing to display the status when obtaining a service account token, sets the token refresh interval in line 142, and initializes the service account in line 145.

Additionally, lines 147-173 create the spreadsheet, add the header row for ARIA, format the cells, and extend the number of rows.

Serial.println("Creating sheets...");
waitUntilNewRequestsReady();    // Wait for token
spreadsheetIdString = createSpreadsheet();
if (not(spreadsheetIdString.length() > 0)) {
    Serial.println("Failed to create sheets.");
}

Serial.println("Adding headers for ARIA...");
delay(MIN_REQUEST_INTERVAL);
waitUntilNewRequestsReady();
if (not addSheetAriaHeaderRow(spreadsheetIdString, ARIA_SHEET_TITLE)) {
    Serial.println("Failed to add headers.");
}

Serial.println("Formatting the sheet for ARIA...");
delay(MIN_REQUEST_INTERVAL);
waitUntilNewRequestsReady();
if (not formatSheet(spreadsheetIdString, ARIA_SHEET_ID)) {
    Serial.println("Failed to format.");
}

Serial.println("Extending the sheet for ARIA...");
delay(MIN_REQUEST_INTERVAL);
waitUntilNewRequestsReady();
if (not extendSheet(spreadsheetIdString, ARIA_SHEET_ID, SHEETS_ROWS - SHEETS_DEFAULT_ROWS)) {
    Serial.println("Failed to extend.");
}

Task Registration

Lines 179-186 register a task to update TWELITE data asynchronously.

xTaskCreatePinnedToCore(
    [](void* params) {
        while (true) {
            anotherLoop();
            vTaskDelay(1);
        }
    },
    "Task for anotherLoop()", 8192, nullptr, 18, nullptr, 0);

xTaskCreatePinnedToCore() is a function provided by the FreeRTOS multitasking framework.

Here, a lambda function (lines 180–185) is passed to create a task that calls anotherLoop() repeatedly.

[](void* params) {
    while (true) {
        anotherLoop();
        vTaskDelay(1);    // IMPORTANT for Watchdog
    }
},

The task is named Task for anotherLoop(), its stack size is 8192, it takes no parameters, its priority is 18 (the higher the number, the higher the priority; Wi-Fi-related processing is at 19), there is no interface to manipulate the task, and it runs on the same CPU core as Wi-Fi and other RF processing, Core 0 (loop() runs on Core 1).

"Task for anotherLoop()", 8192, nullptr, 18, nullptr, 0);    // Priority is 18 (lower than WiFi)

Updating TWELITE Data

At line 203, Twelite.update() is called within anotherLoop().

Twelite.update();

Twelite.update() reads out the packet data (ModBus ASCII format) sent from the TWELITE parent device, one byte at a time.

Updating the Spreadsheet

Lines 192–195 call the spreadsheet update process.

if (millis() - lastTimeRequestWasSent > MIN_REQUEST_INTERVAL) {
    // Add any available data
    addSheetsDataRow(spreadsheetIdString);
}

The if statement on line 192 ensures that at least 1 second has passed since the last request was sent, in order to comply with the API limit of 60 requests per minute (similar to “throttle” in JavaScript’s Throttle/Debounce).

Line 194 adds a new data row to the spreadsheet as needed.

addSheetsDataRow(spreadsheetIdString);

Updating the API Library

Line 196 updates the Google API library and checks whether new requests can be sent.

readyForNewRequests = GSheet.ready();

Updating the NTP Library

Line 197 updates the NTP library.

timeClient.update();

Spreadsheet Operations

Starting from line 217, operations on the spreadsheet are performed using the Sheets API.

For details, see the API reference for the library or Sheets API REST resources.

Google

Arduino

ESP32

Community

Libraries

Plugins

5 - Graph Display Using ThingSpeak

Explanation of the sample sketch spot-thingspeak for displaying TWELITE ARIA data on the ThingSpeak website
This is an explanation of the sample sketch spot-thingspeak that acts as a wireless LAN client and uses MathWorks’ service ThingSpeak to graph temperature and humidity data.

5.1 - Graph Display with ThingSpeak

Latest version
This is an explanation of the sample sketch spot-thingspeak, which acts as a Wi-Fi client and uses MathWorks’ service ThingSpeak to graph temperature and humidity data.

Getting the Source Code

You can obtain it from the GitHub repository monowireless/spot-thingspeak.

System Overview

The spot-thingspeak sample receives data from TWELITE ARIA and sends it as an HTTP GET request to the ThingSpeak server, making it possible to display the data as a graph.

Display Example

Display Example

What You Need for Development

Environment Setup

ThingSpeak Setup

  1. Access ThingSpeak’s website and create a MathWorks account.
  2. Create a “Channel” and set it as shown below.
Example Channel Settings

Example Channel Settings

  1. On the “API Keys” tab, make a note of the 16-character “Write API key”.

Installing the IDE and Toolchain

See How to set up the development environment with Arduino IDE 1.x.

Obtaining the Project Files

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

Changing User Settings

Open config.h from the tab at the top of the Arduino IDE and set the Wi-Fi SSID and password. WPA2-PSK networks are assumed.

Also, set the 16-character “Write API key” you noted earlier. If necessary, rewrite the root certificate as well.

Writing the Project Files

See How to upload sketches to ESP32.

Setting up and Starting TWELITE ARIA

  1. Change the settings for TWELITE ARIA and set the Transmission Interval in TWELITE ARIA Mode to 30 seconds or more (e.g., 60 seconds) to avoid overloading the server.
  2. Insert a CR2032 battery and start TWELITE ARIA.

Sketch

Explanation of the Arduino sketch spot-thingspeak.ino and config.h.

Including Libraries

Official Arduino and ESP32 Libraries

Lines 5-7 include the official Arduino and ESP32 libraries.

#include <Arduino.h>
#include <WiFiClientSecure.h>
#include <WiFi.h>
Header FileDescriptionNotes
Arduino.hBasic Arduino librarySometimes can be omitted, but included just in case
WiFiClientSecure.hSSL communication on ESP32
WiFi.hHandles Wi-FiSometimes can be omitted, but included just in case

MWings Library

Line 13 includes the MWings library.

#include <MWings.h>

Definition of User Settings

Line 16 includes config.h.

#include "config.h"

Definition of Data Types

Lines 19-26 define a struct type to store data received from the child device.

struct DataFromAria {
    uint32_t serialId;
    uint8_t logicalId;
    uint16_t supplyVoltage;
    uint8_t linkQuality;
    int16_t temp100x;
    uint16_t humid100x;
};
NameDescription
serialIdSerial ID
logicalIdLogical Device ID
supplyVoltageSupply Voltage
linkQualityLQI
temp100xTemperature multiplied by 100
humid100xHumidity multiplied by 100

Here, TWELITE ARIA is used.

config.h

Definition of Reboot Interval

Line 4 of config.h specifies the reboot interval for the ESP32.

const uint32_t REBOOT_INTERVAL = 21600; // seconds

Here, 21600 seconds = 6 hours.

Definition of TWELITE Settings

Lines 7-8 of config.h define the settings applied to the TWELITE parent device mounted on TWELITE SPOT.

const uint8_t TWE_CH = 18;
const uint32_t TWE_APPID = 0x67720102;
NameDescription
TWE_CHTWELITE frequency channel
TWE_APPIDTWELITE application ID

Definition of Wi-Fi Settings

Lines 11-12 of config.h define the Wi-Fi settings applied to the ESP32 mounted on TWELITE SPOT.

const char* WIFI_SSID = "YOUR SSID";
const char* WIFI_PASSWORD = "YOUR PASSWORD";
NameDescription
WIFI_SSIDSSID of the network to connect to
WIFI_PASSWORDPassword of the network to connect to

ThingSpeak Write API key

Line 15 of config.h defines the “Write API key” required to add data to a “Field” in a ThingSpeak “Channel”.

Root Certificate

Lines 18-41 of config.h contain the root certificate obtained from api.thingspeak.com using Google Chrome. Rewrite as necessary.

const char *CA_CERT = R"(
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
)";

Host Settings

Lines 43-44 of config.h define the host settings.

const char *SERVER_HOST = "api.thingspeak.com";
const uint16_t SERVER_PORT = 443;
NameDescription
SERVER_HOSTServer host name
SERVER_PORTServer port number

Definition of Various Constants

From line 46 in config.h, various constants are defined.

const int QUERIES_MAX_LENGTH = 128;         // bytes (without \0)
const int32_t CONNECT_TIMEOUT = 10;     // seconds
const uint32_t RECONNECT_MIN_INTERVAL = 5; // seconds
// According to thingspeck free limitations, SEND_MIN_INTERVAL is set for 20s.
const uint32_t SEND_MIN_INTERVAL = 20; // seconds
const uint32_t REQUEST_TIMEOUT = 10;   // seconds
NameDescription
QUERIES_MAX_LENGTHMaximum length of the query string (excluding null terminator)
CONNECT_TIMEOUTTimeout when connecting to the server
RECONNECT_MIN_INTERVALMinimum interval for reconnecting to Wi-Fi access point
SEND_MIN_INTERVALMinimum interval between requests
REQUEST_TIMEOUTTimeout from request to response

Definition of Pin Numbers

Lines 29-34 of spot-thingspeak.ino define the pin numbers.

static const int RST_PIN = 5;
static const int PRG_PIN = 4;
static const int LED_PIN = 18;

static const int8_t RX1_PIN = 16;
static const int8_t TX1_PIN = 17;
NameDescription
RST_PINPin number connected to TWELITE’s RST pin
PRG_PINPin number connected to TWELITE’s PRG pin
LED_PINPin number connected to the ESP32 LED on the board
RX1_PINPin number connected to TWELITE’s RX1 pin
TX1_PINPin number connected to TWELITE’s TX1 pin

Declaration of Global Objects

Line 37 declares a global object.

static WiFiClientSecure client;
NameDescription
clientInterface for HTTPS communication

Declaration of Global Variables

Lines 40-41 declare global variables.

static DataFromAria LatestDataFromAria;
static bool IsThereNewDataFromAria;
NameDescription
LatestDataFromAriaLatest data received from TWELITE ARIA
IsThereNewDataFromAriaFlag indicating new data received from TWELITE ARIA

Declaration of Function Prototypes

Lines 44-54 declare function prototypes.

void anotherLoopForTWELITE();
NameDescription
anotherLoopForTWELITELoop function for processing TWELITE data
void initTWELITE();
void initWiFi();
NameDescription
initTWELITETWELITE initialization function
initWiFiWi-Fi initialization function
void onAppAriaPacket(const ParsedAppAriaPacket& packet);
NameDescription
onAppAriaPacketCallback function when data is received from TWELITE ARIA
void sendAriaData(const DataFromAria& data)
NameDescription
sendAriaDataFunction to send TWELITE ARIA data via HTTP GET request

setup()

Lines 57-74 perform overall initialization.

void setup() {
    Serial.begin(115200);

    initTWELITE();
    initWiFi();

    // Attach another loop function for TWELITE
    // Note: Core 0 is also used for the WiFi task, which priority is 19 (ESP_TASKD_EVENT_PRIO - 1)
    xTaskCreatePinnedToCore(
        [](void *params) {
            while (true) {
                anotherLoopForTWELITE();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },
        "Task for anotherLoopForTWELITE()", 8192, nullptr, 18, nullptr,
        0); // Priority is 18 (lower than WiFi)
}

xTaskCreatePinnedToCore() is used to register a task separate from loop().

The following part is an anonymous function without a capture. This avoids unnecessary pollution of the global namespace.

        [](void *params) {
            while (true) {
                anotherLoopForTWELITE();
                vTaskDelay(1); // IMPORTANT for Watchdog
            }
        },

loop()

Lines 77-99 are the main loop process.

It handles HTTP requests, Wi-Fi reconnection if disconnected, and periodic resets.

void loop() {
    static uint32_t lastTimeReconnected = 0;
    if (WiFi.status() == WL_CONNECTED) {
        // Regular operations
        // Check for new data
        if (IsThereNewDataFromAria) {
            IsThereNewDataFromAria = false; // Clear first; data is updated on another thread
            DataFromAria data = LatestDataFromAria; // Now, the buffer is open for incoming data
            sendAriaData(data);
        }
    } else if (millis() - lastTimeReconnected > RECONNECT_MIN_INTERVAL * 1000) {
        // Lost connection, reconnect periodically
        Serial.println("Disconnected. Reconnecting to WiFi...");
        WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
        lastTimeReconnected = millis();
    }

    // Reboot every x interval
    if (millis() > REBOOT_INTERVAL * 1000) {
        Serial.println("Rebooting...");
        ESP.restart();
    }
}

anotherLoopForTWELITE()

Lines 102-104 are the loop process for TWELITE.

To sequentially receive and interpret data, this is handled in a separate task from the blocking loop().

void anotherLoopForTWELITE() {
    Twelite.update();
}

initTWELITE()

Lines 107-114 handle initialization for TWELITE.

The TWELITE mounted on TWELITE SPOT is started with the specified settings, and a callback function is registered for packet reception.

void initTWELITE() {
    Serial2.begin(115200);
    if (Twelite.begin(Serial2, LED_PIN, RST_PIN, PRG_PIN, TWE_CHANNEL, TWE_APP_ID)) {
        Serial.println("Started TWELITE.");
    }
    // Attach event handlers to process packets
    Twelite.on(onAppAriaPacket);
}

initWiFi()

Lines 117-144 handle Wi-Fi initialization.

If connection fails, it will retry every 5 seconds.

void initWiFi() {
    Serial.print("\nConnecting to the WiFi network ");
    Serial.print(WIFI_SSID);
    Serial.println("...");

    // Begin
    WiFi.mode(WIFI_STA);
    WiFi.setAutoReconnect(true);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    // Wait for connection
    Serial.print("Connecting.");
    while (WiFi.status() != WL_CONNECTED) {
        static int count = 0;
        Serial.print('.');
        delay(500);
        // Retry every 5 seconds
        if (count++ % 10 == 0) {
            WiFi.disconnect();
            WiFi.reconnect();
            Serial.print('!');
        }
    }
    Serial.println("\nConnected!");

    // Set Root CA certificate
    client.setCACert(CA_CERT);
}

onAppAriaPacket()

Lines 147-157 describe the processing when data is received from TWELITE ARIA.

Here, HTTP transmission is not performed; instead, the data is set to a global variable. The data set in the global variable will be processed by sendAriaData() in a separate task.

void onAppAriaPacket(const ParsedAppAriaPacket& packet)
{
    // Store data
    LatestDataFromAria.serialId = packet.u32SourceSerialId;
    LatestDataFromAria.logicalId = packet.u8SourceLogicalId;
    LatestDataFromAria.supplyVoltage = packet.u16SupplyVoltage;
    LatestDataFromAria.linkQuality = packet.u8Lqi;
    LatestDataFromAria.temp100x = packet.i16Temp100x;
    LatestDataFromAria.humid100x = packet.u16Humid100x;
    IsThereNewDataFromAria = true;
}

sendAriaData()

Lines 160-220 define a function that sets TWELITE ARIA data in the query string of an HTTP GET request and sends it.

To prevent excessive load on the server, sending is skipped if packets arrive too frequently.

void sendAriaData(const DataFromAria& data)
{
    static uint32_t lastTimeRequested = 0;
    if (millis() - lastTimeRequested > SEND_MIN_INTERVAL * 1000 or lastTimeRequested == 0) {
        Serial.println("Connecting to the server...");
        if (not client.connect(SERVER_HOST, SERVER_PORT, CONNECT_TIMEOUT * 1000)) {
            Serial.println("Connection failed!");
        } else {
            Serial.println("Connected to the server!");

            // Make a query string for the Channel on the ThingSpeak
            char queries[QUERIES_MAX_LENGTH+1];
            snprintf(queries, sizeof(queries),
                     "api_key=%s&field1=%s&field2=%s&field3=%s&field4=%d",
                     // Write API key for the Channel
                     WRITE_API_KEY,
                     // Field 1: Temperature
                     String(data.temp100x / 100.0f, 2),
                     // Field 2: Humidity
                     String(data.humid100x / 100.0f, 2),
                     // Field 3: Supply Voltage
                     String(data.supplyVoltage / 1000.0f, 2),
                     // Field 4: Link Quality
                     data.linkQuality);

            // Send a request
            client.println(String("GET https://") +
                           SERVER_HOST +
                           String("/update?") +
                           queries +
                           String(" HTTP/1.1"));
            client.println("Accept: */*");
            client.println(String("Host: ") + SERVER_HOST);
            client.println("Connection: close");
            client.println();
            uint32_t timeSentRequest = millis();

            // Handle a response
            while (client.connected()) {
                String line = client.readStringUntil('\n');
                if (line == "\r") {
                    Serial.print("Index (if succeeded): ");
                    break;
                }
                if (millis() - timeSentRequest > REQUEST_TIMEOUT * 1000) {
                    Serial.println("Request was timed out");
                    break;
                }
            }
            while (client.available()) {
                char c = client.read();
                Serial.write(c);
            }
            client.stop();
            Serial.println("");
        }
        lastTimeRequested = millis();
    } else {
        Serial.println("Requests are too frequently; skip.");
    }
}