ESP32 LoRa Sensor Monitoring with Web Server (Long Range Communication)

I’m a big fan of the ESP32 boards because of the number of communication options they managed to squeeze on the board, but I became more impressed recently when I came across the TTGO LoRa32 development boards which adds LoRa to the onboard communication features of the ESP32.

TTGO LoRa 32 Development Board

The LoRa communication capacity of the board opens up a web of possibilities and as a demonstration of how the board works, for today’s tutorial, we are going to build a LoRa Sensor Monitor with a webserver.

The idea behind the project is simple and not so different from the “Introduction to LoRa communication” project we built recently. It comprises two parts; a Transmitter and a Receiver. The transmitter comprises of a TTGO LoRa32 development board along with a BME280 sensor which is used to obtain temperature and humidity parameters from the environment. The data obtained are sent using the LoRa Communication features of the TTGO LoRa32, to the receiver which receives the data and displays it on a webpage via a webserver hosted on the board. By visiting the IP address of the server on any device on the same network as the receiver’s LoRa32 board, users will be able to see the data displayed on a webpage.

At the end of today’s project, you would know how to use both the WiFi and LoRa features of the TTGO LoRa32 board.

Required Components

The following components are required to build this project;

  1. 2 x TTGO LoRa32 Development Board
  2. BME280
  3. Jumper Wires
  4. BreadBoard
  5. Power Bank or 3.7v LiPo Battery (optional)

All components can be bought via the attached links. The battery is useful if you plan to use the project without being tethered to your PC  by USB Cables.

Schematics

Even though the project involves two devices (transmitter and receiver), we will create schematics for just the transmitter since the TTGO LoRa32 board has everything we need for the receiver, onboard. For the transmitter, the BME280 communicates with connected microcontrollers over I2C, as such, all we need do is connect it to the I2C pins of the TTGO LoRa32 as shown in the image below:

Schematics (credits: Randomnerds)

A pin-to-pin connection of the components is described below;

BME280 – LoRa32

GND - GND
VCC - 3.3V
SCL - GPIO13
SDA - GPIO21

The connections when complete should look like the image below

Setting up the Arduino IDE

The code for today’s project will be developed using the Arduino IDE,  we need to install two major support package; the ESP32 Board support package, and the ESP32 files system uploader, on it.

The ESP32 board support package provides all you need to program most ESP32-based boards with the Arduino IDE, while the ESP32 file system uploader allows access to the ESP32’s Serial Peripheral Interface Flash File System (SPIFFS). SPIFFS is a lightweight filesystem for microcontrollers that lets you access the flash memory like you would a normal filesystem on your computer. The ESP32 file uploader serves as a tool through which you can upload things like config files, or webserver files like we will be doing today, to the ESP32 SPIFFS.  So instead of writing the HTML code for the webserver as a String directly on the Arduino sketch, we can write the HTML and CSS in a separate file and save them on the ESP32 filesystem.

The installation process for the ESP32 board support package has been covered in one of our past tutorials here, as such, for today’s tutorial our focus will be on the ESP32 Filesystem Uploader.

Installing the ESP32 Filesystem Uploader

To install the ESP32 filesystem uploader, it is required that you have the latest version of the Arduino IDE installed, along with the ESP32 board support for the IDE. With those in place, follow the steps below to install the file system uploader.

1. Download the File system uploader from the ESP32FS-1.0.zip release page.

2. Go to the Arduino IDE directory, and open the Tools folder.

3. Unzip the downloaded .zip folder to the Tools folder. You should have a similar folder structure: <home_dir>/Arduino-<version>/tools/ESP32FS/tool/esp32fs.jar

4. With this done, restart the Arduino IDE and Check under tools you should see the ESP32 Sketch Data Upload options there as shown below.

Code

There are two sides to today’s project, as such, we will develop two sketches; one for the transmitter and one for the receiver. These sketches are unedited versions of the one used in Randomnerds tutorial‘s article.

Transmitter Sketch

The algorithm behind the transmitter is straight forward and similar to what we did in the “Introduction to LoRa” project. We obtain temperature and humidity data from the environment via the BME280, display it using the OLED on the TTGO LoRa32 and send it out to the receiver using LoRa communication features of the board.

To achieve this with ease, we will use five important libraries including: the Arduino LoRa library by Sandeep Mistry, the  Adafruit SSD1306 library, the Adafruit_GFX library, the Adafruit_BME280 library, and the Adafruit unified sensor library. The Adafruit BME280 and Unified Sensor libraries are involved in reliably obtaining temperature and humidity information from the BME280, the Adafruit SSD1306 and GFX libraries are used in interaction with the OLED, and the Arduino LoRa library handles everything involved with sending the data to the receiver over LoRa. All five libraries can be installed via the Arduino IDE’s Library manager or by downloading them via the links attached to their names and installing them manually.

As usual, I will do a quick breakdown of the code with the aim of explaining parts that may be slightly difficult to follow. The code starts, as always, with the include statement for all the libraries that will be used.

//Libraries for LoRa
#include <SPI.h>
#include <LoRa.h>

//Libraries for OLED Display
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

//Libraries for BME280
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

Next, the pins of the TTGO LoRa32 being used by the onboard LoRa module are specified;

//define the pins used by the LoRa transceiver module
#define SCK 5
#define MISO 19
#define MOSI 27
#define SS 18
#define RST 14
#define DIO0 26

This is followed by a declaration of the module’s frequency and a declaration of the pins of the TTGO LoRa32 being used by the OLED along with the display’s dimension.

//433E6 for Asia
//866E6 for Europe
//915E6 for North America
#define BAND 866E6

//OLED pins
#define OLED_SDA 4
#define OLED_SCL 15 
#define OLED_RST 16
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

It is important that you ensure the frequency of your LoRa module is, by law, allowed for use in projects like this.

Next, we declare the pins of the TTGO LoRa32 to which the BME is connected, and create an instance of the BME library.

//BME280 definition
#define SDA 21
#define SCL 13

TwoWire I2Cone = TwoWire(1);
Adafruit_BME280 bme;

Next, we create a counter variable to serve as an ID for messages, and a few more variables to hold the LoRa message, temperature, humidity, and pressure.

int readingID = 0;

int counter = 0;
String LoRaMessage = "";

float temperature = 0;
float humidity = 0;
float pressure = 0;

We round up this section by creating an instance of the OLED library with the OLED dimensions as arguments.

Next, we create a few functions which will be used later in the code. The functions include; the StartOLED() function which is used in initializing the OLED, the startloRA() function which is used in initializing and setting parameters for LoRa Communication, the StartBME() function which is used to initialize the BME280, the getReadings() function which is used to obtain temperature, humidity and pressure values from the BME280. and the void sendReadings() function which is used in sending the data obtained with the getReadings() function, over LoRa and also display it on the OLED.

void startOLED(){
  //reset OLED display via software
  pinMode(OLED_RST, OUTPUT);
  digitalWrite(OLED_RST, LOW);
  delay(20);
  digitalWrite(OLED_RST, HIGH);

  //initialize OLED
  Wire.begin(OLED_SDA, OLED_SCL);
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3c, false, false)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("LORA SENDER");
}

//Initialize LoRa module
void startLoRA(){
  //SPI LoRa pins
  SPI.begin(SCK, MISO, MOSI, SS);
  //setup LoRa transceiver module
  LoRa.setPins(SS, RST, DIO0);

  while (!LoRa.begin(BAND) && counter < 10) {
    Serial.print(".");
    counter++;
    delay(500);
  }
  if (counter == 10) {
    // Increment readingID on every new reading
    readingID++;
    Serial.println("Starting LoRa failed!"); 
  }
  Serial.println("LoRa Initialization OK!");
  display.setCursor(0,10);
  display.clearDisplay();
  display.print("LoRa Initializing OK!");
  display.display();
  delay(2000);
}

void startBME(){
  I2Cone.begin(SDA, SCL, 100000); 
  bool status1 = bme.begin(0x76, &I2Cone);  
  if (!status1) {
    Serial.println("Could not find a valid BME280_1 sensor, check wiring!");
    while (1);
  }
}

void getReadings(){
  temperature = bme.readTemperature();
  humidity = bme.readHumidity();
  pressure = bme.readPressure() / 100.0F;
}

void sendReadings() {
  LoRaMessage = String(readingID) + "/" + String(temperature) + "&" + String(humidity) + "#" + String(pressure);
  //Send LoRa packet to receiver
  LoRa.beginPacket();
  LoRa.print(LoRaMessage);
  LoRa.endPacket();
  
  display.clearDisplay();
  display.setCursor(0,0);
  display.setTextSize(1);
  display.print("LoRa packet sent!");
  display.setCursor(0,20);
  display.print("Temperature:");
  display.setCursor(72,20);
  display.print(temperature);
  display.setCursor(0,30);
  display.print("Humidity:");
  display.setCursor(54,30);
  display.print(humidity);
  display.setCursor(0,40);
  display.print("Pressure:");
  display.setCursor(54,40);
  display.print(pressure);
  display.setCursor(0,50);
  display.print("Reading ID:");
  display.setCursor(66,50);
  display.print(readingID);
  display.display();
  Serial.print("Sending packet: ");
  Serial.println(readingID);
  readingID++;
}

Next up is the void setup() function.

The creation of the functions above helps reduce the amount of code required under the void setup() function. All we need to do is to initialize the serial communication so the serial monitor can be used for debugging purposes and call the initialization functions that were created above.

void setup() {
  //initialize Serial Monitor
  Serial.begin(115200);
  startOLED();
  startBME();
  startLoRA();
}

Next is the void loop() function. Just like we did under the void setup() function, all we have to do is call the required functions, which include the getReading() and sendReadings() functions.  A delay of 10000ms was also added to allow ample time for sensor refresh.

void loop() {
  getReadings();
  sendReadings();
  delay(10000);
}

The complete transmitter sketch is provided below and attached under the download section.

/*********
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-lora-sensor-web-server/
  
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.
  
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*********/

//Libraries for LoRa
#include <SPI.h>
#include <LoRa.h>

//Libraries for OLED Display
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

//Libraries for BME280
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

//define the pins used by the LoRa transceiver module
#define SCK 5
#define MISO 19
#define MOSI 27
#define SS 18
#define RST 14
#define DIO0 26

//433E6 for Asia
//866E6 for Europe
//915E6 for North America
#define BAND 866E6

//OLED pins
#define OLED_SDA 4
#define OLED_SCL 15 
#define OLED_RST 16
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

//BME280 definition
#define SDA 21
#define SCL 13

TwoWire I2Cone = TwoWire(1);
Adafruit_BME280 bme;

//packet counter
int readingID = 0;

int counter = 0;
String LoRaMessage = "";

float temperature = 0;
float humidity = 0;
float pressure = 0;

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RST);

//Initialize OLED display
void startOLED(){
  //reset OLED display via software
  pinMode(OLED_RST, OUTPUT);
  digitalWrite(OLED_RST, LOW);
  delay(20);
  digitalWrite(OLED_RST, HIGH);

  //initialize OLED
  Wire.begin(OLED_SDA, OLED_SCL);
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3c, false, false)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("LORA SENDER");
}

//Initialize LoRa module
void startLoRA(){
  //SPI LoRa pins
  SPI.begin(SCK, MISO, MOSI, SS);
  //setup LoRa transceiver module
  LoRa.setPins(SS, RST, DIO0);

  while (!LoRa.begin(BAND) && counter < 10) {
    Serial.print(".");
    counter++;
    delay(500);
  }
  if (counter == 10) {
    // Increment readingID on every new reading
    readingID++;
    Serial.println("Starting LoRa failed!"); 
  }
  Serial.println("LoRa Initialization OK!");
  display.setCursor(0,10);
  display.clearDisplay();
  display.print("LoRa Initializing OK!");
  display.display();
  delay(2000);
}

void startBME(){
  I2Cone.begin(SDA, SCL, 100000); 
  bool status1 = bme.begin(0x76, &I2Cone);  
  if (!status1) {
    Serial.println("Could not find a valid BME280_1 sensor, check wiring!");
    while (1);
  }
}

void getReadings(){
  temperature = bme.readTemperature();
  humidity = bme.readHumidity();
  pressure = bme.readPressure() / 100.0F;
}

void sendReadings() {
  LoRaMessage = String(readingID) + "/" + String(temperature) + "&" + String(humidity) + "#" + String(pressure);
  //Send LoRa packet to receiver
  LoRa.beginPacket();
  LoRa.print(LoRaMessage);
  LoRa.endPacket();
  
  display.clearDisplay();
  display.setCursor(0,0);
  display.setTextSize(1);
  display.print("LoRa packet sent!");
  display.setCursor(0,20);
  display.print("Temperature:");
  display.setCursor(72,20);
  display.print(temperature);
  display.setCursor(0,30);
  display.print("Humidity:");
  display.setCursor(54,30);
  display.print(humidity);
  display.setCursor(0,40);
  display.print("Pressure:");
  display.setCursor(54,40);
  display.print(pressure);
  display.setCursor(0,50);
  display.print("Reading ID:");
  display.setCursor(66,50);
  display.print(readingID);
  display.display();
  Serial.print("Sending packet: ");
  Serial.println(readingID);
  readingID++;
}

void setup() {
  //initialize Serial Monitor
  Serial.begin(115200);
  startOLED();
  startBME();
  startLoRA();
}
void loop() {
  getReadings();
  sendReadings();
  delay(10000);
}

Receiver Code

The receiver, as mentioned earlier, receives temperature and humidity values from the transmitter and displays it, along with the time it was received and the signal strength(RSSI), on a webpage hosted on the TTGO LoRa32 board.

All of this, especially the need to create a webpage and host it on the board, introduces new layers of complexity compared to the transmitter sketch. To create the webserver, we need three different files; the Arduino Sketch, the HTML File, and the Images used by the HTML file. Thankfully, the ESP32 has a file system (SPIFFS) which is what is used in storing the image and the HTML files. To do this, the sketch’s file system needs to be set up as described in the image below;

(credits: Randomnerds)

 

We essentially need to create a sketch folder and add a “data” folder in which the image and HTML file will be stored.  Start by creating the index.html file using software like notepad++ or any other editor. The index.html file contain 3 element; the HTML code which creates the basic webpage structure, the CSS style snippets (placed between  <style> </style>) which ensures everything looks pretty, and the javascript lines (placed between  <script> and </script>) which are responsible for updating the sensor readings on the webpage. Copy the code below and paste it in the index.html file.

<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <title>ESP32 (LoRa + Server)</title>
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
  <style>
    body {
      margin: 0;
      font-family: Arial, Helvetica, sans-serif;
      text-align: center;
    }
    header {
      margin: 0;
      padding-top: 5vh;
      padding-bottom: 5vh;
      overflow: hidden;
      background-image: url(winter);
      background-size: cover;
      color: white;
    }
    h2 {
      font-size: 2.0rem;
    }
    p { font-size: 1.2rem; }
    .units { font-size: 1.2rem; }
    .readings { font-size: 2.0rem; }
  </style>
</head>
<body>
  <header>
    <h2>ESP32 (LoRa + Server)</h2>
    <p><strong>Last received packet:<br/><span id="timestamp">%TIMESTAMP%</span></strong></p>
    <p>LoRa RSSI: <span id="rssi">%RSSI%</span></p>
  </header>
<main>
  <p>
    <i class="fas fa-thermometer-half" style="color:#059e8a;"></i> Temperature: <span id="temperature" class="readings">%TEMPERATURE%</span>
    <sup>&deg;C</sup>
  </p>
  <p>
    <i class="fas fa-tint" style="color:#00add6;"></i> Humidity: <span id="humidity" class="readings">%HUMIDITY%</span>
    <sup>%</sup>
  </p>
  <p>
    <i class="fas fa-angle-double-down" style="color:#e8c14d;"></i> Pressure: <span id="pressure" class="readings">%PRESSURE%</span>
    <sup>hpa</sup>
  </p>
</main>
<script>
setInterval(updateValues, 10000, "temperature");
setInterval(updateValues, 10000, "humidity");
setInterval(updateValues, 10000, "pressure");
setInterval(updateValues, 10000, "rssi");
setInterval(updateValues, 10000, "timestamp");

function updateValues(value) {
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      document.getElementById(value).innerHTML = this.responseText;
    }
  };
  xhttp.open("GET", "/" + value, true);
  xhttp.send();
}
</script>
</body>
</html>

Arduino Sketch

The Arduino sketch basically takes data from the receiver, over LoRa, and passes it on, along with the signal strength and a timestamp that indicates when the message was received, to the webpage.

To reduce the complexity of the sketch, we will be using 3 libraries in addition to the ones used for the transmitter’s sketch. These libraries include; the  NTPClient library forked by Taranis, the ESPAsyncWebServer library, and the Async TCP library.

The ESPAsyncWebserver and AsyncTCP library makes it easy to implement an asynchronous web server on the ESP, while the forked NTPClient library makes it easy to obtain network date and time which is used in time-stamping the data received from the transmitter. All these libraries are not available through the Arduino IDE’s Library Manager, as such, you will need to download them from the attached links and install them by unzipping them to the Arduino Libraries folder. Remember to restart the Arduino IDE after the installation.

Just like with the other sketch, I will do a quick run-through of the sketch explaining some of the key parts.

The sketch starts with include statements for all the libraries that will be used.

// Import Wi-Fi library
#include <WiFi.h>
#include "ESPAsyncWebServer.h"

#include <SPIFFS.h>

//Libraries for LoRa
#include <SPI.h>
#include <LoRa.h>

//Libraries for OLED Display
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// Libraries to get time from NTP Server
#include <NTPClient.h>
#include <WiFiUdp.h>

Then the pins of the TTGO LoRa32 board being used by the onboard LoRa module are specified.

//define the pins used by the LoRa transceiver module
#define SCK 5
#define MISO 19
#define MOSI 27
#define SS 18
#define RST 14
#define DIO0 26

Next, the LoRa module’s frequency is specified and the pins of the TTGO LoRa32 being used by the OLED along with the display’s dimension are also specified.

//433E6 for Asia
//866E6 for Europe
//915E6 for North America
#define BAND 866E6

//OLED pins
#define OLED_SDA 4
#define OLED_SCL 15 
#define OLED_RST 16
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

For obvious reasons, you must ensure the frequency of the LoRa module on the receiver is the same as that of the transmitter and it’s legal to use it in projects like this in your country,

Next, provide the credentials of the WiFI access point through which the ESP is expected to connect to your local network.

// Replace with your network credentials
const char* ssid     = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

and create an NTP client to be used in fetching date and time information, along with variables to store them.

// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

// Variables to save date and time
String formattedDate;
String day;
String hour;
String timestamp;

We create a few more variables to store the message received from the transmitter.

// Initialize variables to get and save LoRa data
int rssi;
String loRaMessage;
String temperature;
String humidity;
String pressure;
String readingID;

and an AsyncWebServer object on port 80 and also create an instance of the SSD1306 library.

AsyncWebServer server(80);

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RST);

Next, we create a few functions which will be used later in the code. The functions include;  the processor() function which is used to send values to the place holders created in the HTML file, the startOLED() function which is used to initialize the OLED, the startloRA() function which is used to initialize and set parameters for LoRa Communication, the connectWiFi() function which uses the SSID and password credentials to connect the TTGO LoRa32 board to your Local network, the void getTimeStamp() function which is used to obtain time information from the NTPClient, and void getLoRaData() which receives the String sent from the transmitter and breaks it down to separate each value into its corresponding variable.

// Replaces placeholder with DHT values
String processor(const String& var){
  //Serial.println(var);
  if(var == "TEMPERATURE"){
    return temperature;
  }
  else if(var == "HUMIDITY"){
    return humidity;
  }
  else if(var == "PRESSURE"){
    return pressure;
  }
  else if(var == "TIMESTAMP"){
    return timestamp;
  }
  else if (var == "RRSI"){
    return String(rssi);
  }
  return String();
}

//Initialize OLED display
void startOLED(){
  //reset OLED display via software
  pinMode(OLED_RST, OUTPUT);
  digitalWrite(OLED_RST, LOW);
  delay(20);
  digitalWrite(OLED_RST, HIGH);

  //initialize OLED
  Wire.begin(OLED_SDA, OLED_SCL);
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3c, false, false)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("LORA SENDER");
}

//Initialize LoRa module
void startLoRA(){
  int counter;
  //SPI LoRa pins
  SPI.begin(SCK, MISO, MOSI, SS);
  //setup LoRa transceiver module
  LoRa.setPins(SS, RST, DIO0);

  while (!LoRa.begin(BAND) && counter < 10) {
    Serial.print(".");
    counter++;
    delay(500);
  }
  if (counter == 10) {
    // Increment readingID on every new reading
    Serial.println("Starting LoRa failed!"); 
  }
  Serial.println("LoRa Initialization OK!");
  display.setCursor(0,10);
  display.clearDisplay();
  display.print("LoRa Initializing OK!");
  display.display();
  delay(2000);
}

void connectWiFi(){
  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  display.setCursor(0,20);
  display.print("Access web server at: ");
  display.setCursor(0,30);
  display.print(WiFi.localIP());
  display.display();
}

// Read LoRa packet and get the sensor readings
void getLoRaData() {
  Serial.print("Lora packet received: ");
  // Read packet
  while (LoRa.available()) {
    String LoRaData = LoRa.readString();
    // LoRaData format: readingID/temperature&soilMoisture#batterylevel
    // String example: 1/27.43&654#95.34
    Serial.print(LoRaData); 
    
    // Get readingID, temperature and soil moisture
    int pos1 = LoRaData.indexOf('/');
    int pos2 = LoRaData.indexOf('&');
    int pos3 = LoRaData.indexOf('#');
    readingID = LoRaData.substring(0, pos1);
    temperature = LoRaData.substring(pos1 +1, pos2);
    humidity = LoRaData.substring(pos2+1, pos3);
    pressure = LoRaData.substring(pos3+1, LoRaData.length());    
  }
  // Get RSSI
  rssi = LoRa.packetRssi();
  Serial.print(" with RSSI ");    
  Serial.println(rssi);
}

// Function to get date and time from NTPClient
void getTimeStamp() {
  while(!timeClient.update()) {
    timeClient.forceUpdate();
  }
  // The formattedDate comes with the following format:
  // 2018-05-28T16:00:13Z
  // We need to extract date and time
  formattedDate = timeClient.getFormattedDate();
  Serial.println(formattedDate);

  // Extract date
  int splitT = formattedDate.indexOf("T");
  day = formattedDate.substring(0, splitT);
  Serial.println(day);
  // Extract time
  hour = formattedDate.substring(splitT+1, formattedDate.length()-1);
  Serial.println(hour);
  timestamp = day + " " + hour;
}

Up next is the void setup() function. Just like the transmitter sketch, the creation of the functions above helps reduce the amount of code required leaving the initialization of the SPIffS system, along with the implementation of the server.on routine as the major tasks. The javascript snippet of code in the HTML file is set to update the webpage every 10 seconds, as such, it makes a request to the Arduino Sketch for data when that happens. The Server.on routine handles that request and sends all the values to the server.

void setup() { 
  // Initialize Serial Monitor
  Serial.begin(115200);
  startOLED();
  startLoRA();
  connectWiFi();
  
  if(!SPIFFS.begin()){
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }
  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/index.html", String(), false, processor);
  });
  server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", temperature.c_str());
  });
  server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", humidity.c_str());
  });
  server.on("/pressure", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", pressure.c_str());
  });
  server.on("/timestamp", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", timestamp.c_str());
  });
  server.on("/rssi", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", String(rssi).c_str());
  });
  server.on("/winter", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/winter.jpg", "image/jpg");
  });
  // Start server
  server.begin();

The setup also includes the line of code to initialize the NTPClient and offset the data received with your timezone.

  // Initialize a NTPClient to get time
  timeClient.begin();
  // Set offset time in seconds to adjust for your timezone, for example:
  // GMT +1 = 3600
  // GMT +8 = 28800
  // GMT -1 = -3600
  // GMT 0 = 0
  timeClient.setTimeOffset(0);
}

Next, is the void loop() function. The loop starts with a line of code to listen for incoming packets which if available, the getLoRaData() and getTimeStamp() functions are called.

void loop() {
  // Check if there are LoRa packets available
  int packetSize = LoRa.parsePacket();
  if (packetSize) {
    getLoRaData();
    getTimeStamp();
  }
}

The complete Arduino Sketch for the receiver is provided below and also attached under the download section.

/*********
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-lora-sensor-web-server/

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.
  
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*********/

// Import Wi-Fi library
#include <WiFi.h>
#include "ESPAsyncWebServer.h"

#include <SPIFFS.h>

//Libraries for LoRa
#include <SPI.h>
#include <LoRa.h>

//Libraries for OLED Display
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// Libraries to get time from NTP Server
#include <NTPClient.h>
#include <WiFiUdp.h>

//define the pins used by the LoRa transceiver module
#define SCK 5
#define MISO 19
#define MOSI 27
#define SS 18
#define RST 14
#define DIO0 26

//433E6 for Asia
//866E6 for Europe
//915E6 for North America
#define BAND 866E6

//OLED pins
#define OLED_SDA 4
#define OLED_SCL 15 
#define OLED_RST 16
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Replace with your network credentials
const char* ssid     = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

// Variables to save date and time
String formattedDate;
String day;
String hour;
String timestamp;


// Initialize variables to get and save LoRa data
int rssi;
String loRaMessage;
String temperature;
String humidity;
String pressure;
String readingID;

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RST);

// Replaces placeholder with DHT values
String processor(const String& var){
  //Serial.println(var);
  if(var == "TEMPERATURE"){
    return temperature;
  }
  else if(var == "HUMIDITY"){
    return humidity;
  }
  else if(var == "PRESSURE"){
    return pressure;
  }
  else if(var == "TIMESTAMP"){
    return timestamp;
  }
  else if (var == "RRSI"){
    return String(rssi);
  }
  return String();
}

//Initialize OLED display
void startOLED(){
  //reset OLED display via software
  pinMode(OLED_RST, OUTPUT);
  digitalWrite(OLED_RST, LOW);
  delay(20);
  digitalWrite(OLED_RST, HIGH);

  //initialize OLED
  Wire.begin(OLED_SDA, OLED_SCL);
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3c, false, false)) { // Address 0x3C for 128x32
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("LORA SENDER");
}

//Initialize LoRa module
void startLoRA(){
  int counter;
  //SPI LoRa pins
  SPI.begin(SCK, MISO, MOSI, SS);
  //setup LoRa transceiver module
  LoRa.setPins(SS, RST, DIO0);

  while (!LoRa.begin(BAND) && counter < 10) {
    Serial.print(".");
    counter++;
    delay(500);
  }
  if (counter == 10) {
    // Increment readingID on every new reading
    Serial.println("Starting LoRa failed!"); 
  }
  Serial.println("LoRa Initialization OK!");
  display.setCursor(0,10);
  display.clearDisplay();
  display.print("LoRa Initializing OK!");
  display.display();
  delay(2000);
}

void connectWiFi(){
  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  display.setCursor(0,20);
  display.print("Access web server at: ");
  display.setCursor(0,30);
  display.print(WiFi.localIP());
  display.display();
}

// Read LoRa packet and get the sensor readings
void getLoRaData() {
  Serial.print("Lora packet received: ");
  // Read packet
  while (LoRa.available()) {
    String LoRaData = LoRa.readString();
    // LoRaData format: readingID/temperature&soilMoisture#batterylevel
    // String example: 1/27.43&654#95.34
    Serial.print(LoRaData); 
    
    // Get readingID, temperature and soil moisture
    int pos1 = LoRaData.indexOf('/');
    int pos2 = LoRaData.indexOf('&');
    int pos3 = LoRaData.indexOf('#');
    readingID = LoRaData.substring(0, pos1);
    temperature = LoRaData.substring(pos1 +1, pos2);
    humidity = LoRaData.substring(pos2+1, pos3);
    pressure = LoRaData.substring(pos3+1, LoRaData.length());    
  }
  // Get RSSI
  rssi = LoRa.packetRssi();
  Serial.print(" with RSSI ");    
  Serial.println(rssi);
}

// Function to get date and time from NTPClient
void getTimeStamp() {
  while(!timeClient.update()) {
    timeClient.forceUpdate();
  }
  // The formattedDate comes with the following format:
  // 2018-05-28T16:00:13Z
  // We need to extract date and time
  formattedDate = timeClient.getFormattedDate();
  Serial.println(formattedDate);

  // Extract date
  int splitT = formattedDate.indexOf("T");
  day = formattedDate.substring(0, splitT);
  Serial.println(day);
  // Extract time
  hour = formattedDate.substring(splitT+1, formattedDate.length()-1);
  Serial.println(hour);
  timestamp = day + " " + hour;
}

void setup() { 
  // Initialize Serial Monitor
  Serial.begin(115200);
  startOLED();
  startLoRA();
  connectWiFi();
  
  if(!SPIFFS.begin()){
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }
  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/index.html", String(), false, processor);
  });
  server.on("/temperature", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", temperature.c_str());
  });
  server.on("/humidity", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", humidity.c_str());
  });
  server.on("/pressure", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", pressure.c_str());
  });
  server.on("/timestamp", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", timestamp.c_str());
  });
  server.on("/rssi", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/plain", String(rssi).c_str());
  });
  server.on("/winter", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/winter.jpg", "image/jpg");
  });
  // Start server
  server.begin();
  
  // Initialize a NTPClient to get time
  timeClient.begin();
  // Set offset time in seconds to adjust for your timezone, for example:
  // GMT +1 = 3600
  // GMT +8 = 28800
  // GMT -1 = -3600
  // GMT 0 = 0
  timeClient.setTimeOffset(0);
}

void loop() {
  // Check if there are LoRa packets available
  int packetSize = LoRa.parsePacket();
  if (packetSize) {
    getLoRaData();
    getTimeStamp();
  }
}

With the sketch saved, go to the sketch folder and create a folder called data. Move the image and the index.html file we created earlier into the folder so the file hierarchy looks as we described earlier. With that done, we are ready to upload the sketches and test the project.

Demo

For the transmitter, connect the setup to your computer, ensure you select the right port and board type (in this case, the TTGO LoRa32-OLED V1.) and hit the upload button. If successful, you should see the OLED come up with the latest temperature, humidity, and pressure data as shown in the image below.

Transmitter Demo (credits: Randomnerds)

The serial monitor should also show the LoRa Transmission progress. Since we did not include a way to check if the data was received, the transmitter will keep sending the data out.

The upload mechanism for the receiver is a bit different. We need to first upload the Data file to the SPIFFS using the ESP32 Filesystem Uploader we installed earlier, after which we upload the main sketch. With the HTML file and image in the right directory, go to tools, select the right port, and board type(), then click the ESP32 Sketch Data Upload option.

After a few seconds, the data file should be successfully uploaded. Now upload the sketch to the board using the IDE’s upload button.  With the upload completed, open the serial monitor. You should see the IP address along with LoRa Packets being received from the transmitter (you can power the transmitter with a battery or any other power source for this).

Enter the IP address into the address bar of a browser on any device on the same network as the ESP. you should see the webserver displayed with the latest sensor readings as shown in the image below.

Demo (credits: Randomnerds)

That’s it!

There are dozens of ways to take the project forward. You could decide to hook up the receiver to the internet and instead of a local server, report the data to a globally accessible webpage. You could also decide to retain the local network features but have it display data from multiple transmitters (sensors).

References:

Temper: Sleek temperature sensor built on ESP8266

Meet Temper, a compact, low power temperature sensor based on ESP8266 and SHT30 with a large 13×7 pixel led display. It accesses WiFi periodically to display temperature and humidity data as well as battery percentage via the MQTT protocol. The device’s display uses three 74HC595 + 7 n-fets,  TP4054 to handle battery charging through a USB and MCP1700T LDO to power the ESP.

For WiFi setup, Temper comes with a super simple Web config interface and is also compatible with a home assistant and it’s auto-discovery feature. It can also be used with any platform that supports MQTT and can be attached to walls using magnets.

The case for the device is a stone-age light (PLA) from Spectrum filaments. It comes with a large reset button on the left forces the device to request MQTT temperature data and displays it within 5s after the button press.

Explaining the design, Martin Černý said:

“The ESP is set to wake up every hour to send the temperature to MQTT. Pushing a button (hard resetting it) will show the temperature on the display and battery status as well. It should be able to make half a year of battery life or more (during deep sleep, it draws 32uA which is beyond awesome). This, of course, is not perfect and an e-ink display would make more sense to keep the values visible but also would quadruple the cost of the thing (but I ordered some e-inks anyway :D).”

He also notes that:

“There is a simple battery sensing onboard to keep an eye on battery level. Battery sensing is also used as a trigger for the config portal as there was not enough place for more buttons. How it works is after each button push (hard reset), it will check the battery level. If the battery level is above the threshold, it will start the configuration access point (for wifi settings, MQTT, etc.).”

Martin says he won’t be selling Temper himself, stating that they are “overwhelmingly time-consuming” to make by himself. However, the firmware and documentation are neatly documented on Github with some more helpful information on the wiki.

To put the project together you will the items listed below along with some experience with hot air soldering/ reflow oven.

  • Hot air station and/or reflow oven (SHT30 has pins at the bottom, that can’t be hand-soldered)
  • Components listed in the “PCB and BOM” directory of the GitHub repo.
  • The plastic case.
  • Hot glue to fixate PCB in the enclosure.
  • Some Patience

You will notice on the board that the GPIO2 and GPIO12 are broken out, GPIO12 is safe for any use, but GPIO2 has to be pulled high on startup or the ESP won’t boot up correctly.

More information on the project and it’s build instructions can be found on its GitHub project page. 

M95M04-DR – 4-Mbit serial SPI bus EEPROM

The M95M04 devices are electrically erasable programmable memories (EEPROMs) organized as 524288 x 8 bits, accessed through the SPI bus.

The M95M04 can operate with a supply range from 1.8 to 5.5 V, and is guaranteed over the -40 °C/+85 °C temperature range. The M95M04 offer an additional page, named the Identification page (512 bytes). The Identification page can be used to store sensitive application parameters that can be (later) permanently locked in read-only mode.

Key Features

  • Compatible with the serial peripheral interface (SPI) bus
  • Memory array
    • 4 Mbit (512 Kbytes) of EEPROM
    • Page size: 512 bytes
    • Additional write lockable page (Identification page)
  • Write time
    • Byte Write within 5 ms
    • Page Write within 5 ms
  • Write protect
    • quarter array
    • half array
    • whole memory array
  • Max clock frequency: 10 MHz
  • Single supply voltage: 1.8 V to 5.5 V
  • Operating temperature range: from -40 °C up to +85 °C
  • Enhanced ESD protection (up to 4 kV in human body model)
  • More than 4 million Write cycles
  • More than 40-year data retention
  •  Packages
    • SO8N (ECOPACK2)
    • TSSOP8 (ECOPACK2)
    • WLCSP (ECOPACK2)
more information: www.st.com

Advantech Launches UNO-247 Fanless Entry-Level Edge Computer for IT Applications

Advantech, a leading provider of industrial computing platforms, is pleased to announce the UNO-247—a fanless entry-level Internet of Things (IoT) edge computer aimed at information technology (IT) applications. Equipped with an Intel Celeron J3455 processor, comprehensive I/O ports, and VGA, and HDMI display interfaces, UNO-247 is designed to deliver edge computing power at a competitive price. To ensure flexible configuration and easy deployment for diverse industries, the system I/O includes 4 x USB, 2 x GigaLAN, 4 x RS-232, and 2 x RS-485 ports. The fanless design reduces the accumulation of dust and foreign contaminants in harsh environments, while the threaded DC jack enables locking to prevent unexpected power disconnections or interruptions. Overall, the UNO-247 provides a reliable edge gateway solution that can be configured according to specific applications, such as factory automation and environment monitoring.

Optimized Form Factor for Easy Assembly and Convenient Maintenance

Advantech’s UNO-247 is a compact platform that ensures convenient installation with high applicability and integration potential. The entire form factor has been optimized for internal space savings, an increased mean time before failure (MTBF), more reliable signal transmissions, and higher shock and vibration tolerance. Moreover, to reduce system downtime and ensure convenient maintenance, the platform’s mechanical design has been improved to enable memory installation/swapping without disassembling the entire chassis.

Fanless Platform with Lockable DC Jack for Industrial Environments

The UNO-247 features a fanless design that reduces the accumulation of dust and foreign contaminants, making it suitable for operation in harsh industrial environments. To prevent unexpected power disconnections or interruptions, the UNO-247 power adaptor is equipped with a threaded DC jack that allows the connector to be locked in place, ensuring a stable and reliable power supply.

Cost-Efficient Software-Ready Solution with Comprehensive I/O

Powered by an Intel® Celeron J3455 processor equipped with 4 x USB, 2 x GigaLAN, 4 x RS-232, and 2 x RS-485 ports, as well as 1 x VGA and 1 x HDMI, UNO-247 delivers comprehensive I/O to facilitate a wide range of applications. Moreover, UNO-247 supports the Windows 10 LTSC and Linux operating systems, and can be equipped with Advantech’s WISE-PaaS/DeviceOn software solution to enable remote monitoring and management. The extensive system features and software-ready design make the UNO-247 a cost-efficient intelligent edge gateway ideal for diverse IoT operations.

UNO-247 ports detail

Key Features

  • Intel Celeron J3455 processor
  • Robust system with high stability
  • 2 x GbE, 2 x USB 3.0, 2 x USB 2.0, 4 x RS-232, 2 x RS485, 1 x HDMI, 1 x VGA
  • Compact form factor with fanless design
  • Rubber stopper design for stand mount and optional kit for DIN-rail mounting
  • Optional 3G/GPS/GPRS/Wi-Fi communication
  • Threaded DC jack for connection stability
  • Optimized mechanical design for easy RAM installation

Advantech’s UNO-247 fanless entry-level IoT edge computer is available for order now. For more information about this or other Advantech products and services, contact your local sales support team or visit the Advantech website at www.advantech.com.

Kontron presents D3713-V/R mITX motherboard for AMD Ryzen™ Embedded V1000/R1000 processor

Ideal for demanding graphics applications in professional casino gaming systems, medical displays, thin clients and industrial PCs through AMD Radeon™ Vega Graphics

Kontron, a leading global provider of IoT/Embedded Computing Technology (ECT), will present the D3713-V/R mITX industrial motherboard based on the AMD Ryzen™ Embedded V1000 and R1000 line at this year’s embedded world tradeshow. It features the SoC integrated AMD Radeon™ Vega GPU with particularly brilliant graphics and supports up to four independent displays in 4K resolution via DisplayPorts, one Embedded DisplayPort and a dual-channel LVDS (24bit). With five different AMD processors, the board can be adapted for various graphics applications, e.g. for kiosk, infotainment, digital signage, professional casino gaming systems, as well as medical displays, thin clients and industrial PCs.

The Kontron D3713-V/R mITX motherboard is “Designed by Fujitsu” and manufactured in Germany. This guarantees short delivery times, highest manufacturing quality, competent technical support directly from Augsburg, as well as long-term repair service. Kontron also offers a “kitting” service, where motherboards are assembled ‘ex factory’ with the requested processors, memory latches and even an individual BIOS.

D3713-V/R mITX port details

For the D3713-V/R mITX motherboard, the Kontron SMARTCASE™ S711 is also currently in preparation. It offers a customer-specific configured and certified system solution consisting of the board, CPU, memory, expansion cards, BIOS, cooling and housing.

The D3713-V/R mITX motherboard optionally offers different processors of the AMD Ryzen™ Embedded V1000 and R1000 line: AMD Embedded SoC V1202B, V1605B, V1807B, R1305G or R1606G. Due to the Intel® i210LM Ethernet Controller with 10/100/1000 MBit/s the board supports protocols such as EtherCAT or TSN. Furthermore it offers 2x SO DIMM sockets for up to 32GB memory. The use of a data carrier or extensions is possible via 2x Serial ATA III 600 interfaces (up to 6GBit/s), a Mini PCIe (halfsize/fullsize), a PCI Express® x4 Gen3, an M.2 PCIe x2 Key-M and an M.2 PCIe x4 Key B. It also offers various interfaces, such as USB 3.1 Gen1/Gen2, USB 2.0, serial I/O, GPIO and High Definition Audio Input/Output via the Realtek ALC256 chip codec. The board contains an AMI Aptio 5.x (UEFI) BIOS, a HW Watchdog, a BIOS integrated HW Diagnostic Tool, AMD integrated TPM V2.0 onboard, and a socket for an optional TPM module.

For more information please visit: https://www.kontron.com/products/boards-and-standard-form-factors/motherboards/mini-itx/d3713-v-r-mitx.html

ESP8266 based Online Weather Widget using Wemos D1

A few years back I wrote this tutorial on connected weather monitor with plans of creating a 3D printed enclosure to make it portable and easy to use. I never got around doing that but a few days ago, I came across a similar project built by BitsandBlobs, which went as far as creating the 3D enclosure and I thought their example will be a fun way of sharing the project. So for today’s tutorial, we will build a Connected Weather Widget.

The Weather Widget (Credits: bitsandblobs)

Having an idea of what the current weather conditions are, or what it will look like in a couple of hours or days, has proven to be very useful, not just when it ensures you don’t get caught in the rain without your coat, but also for different important scenarios across different industries like Agriculture, Advertising, Transportation and even Warfare.

While one could build a weather monitoring station to get weather information about a particular place, there are scenarios where you want to obtain weather information about places where you do not have a station. For these scenarios, people usually turn to the news or their phone, but with everyone trying to reduce the amount of time spends with their phone, maybe it will be a good idea to have a device which provides you the weather information without the usual temptation to check your APP feed. This is what we are going to build today.

The project uses the WiFi features of the Wemos D1 mini to connect to an online open-source weather monitoring platform called OpenWeatherMap, where it obtains the required weather information, sorts it and displays it on the OLED.

At the end of today’s tutorial, you would know how to interface an OLED with the Wemos D1 and also obtain weather information from” OpenWeatherMap”.

Required Components

The following components are required to build this project;

  1. Wemos D1 Mini
  2. 0.96″ I2C OLED Display
  3. Jumper Wires
  4. BreadBoard (Optional)
  5. 3.7v Lipo Battery (optional)

All components can be bought from the attached links. While the components are soldered together for this project, you can choose to implement the project on a breadboard or even take things further and provide a backup battery to power the device instead of a USB. This is why the optional components have been added to the list.

Schematics

The use of just two components makes the schematics for this project quite straight forward. The OLED communicates with host microcontroller over I2C, as such, all we need to do is to connect it to the I2C pins of the Wemos D1 as shown below:

Schematic (Credits: bitsandblobs)

To make the schematics easier to follow, a pin-pin map showing how to components are connected is provided below:

OLED – Wemos D1 Mini

SCL - D1
SDA - D2
VCC - 5v
GND - GND

It is important to note the maximum input voltage of your OLED display and be sure it is 5V tolerant. You should connect the VCC pin of the OLED to the Wemos D1’s 3.3v pin if 5V is too high for the display.

Obtain OpenWeatherMap Credentials

As mentioned during the introduction, the weather information to be displayed on the OLED will be obtained from OpenWeatherMap, as such, before we proceed to write the code for this project, it is important to Signup and obtain the credentials, in this case, an API key, that will be used by our device to connect to the service.

To do this follow the steps below:

1. Point your web browser to OpenWeatherMaps Website and hit the “Sign up” button. If you already have an OpenWeatherMap account, just skip to the 3rd step.

2. Once the signup page opens, fill in all the necessary details and click on create an account. If successful, you will be automatically subscribed to the free OpenWeatherMap plan which will allow you to perform up to 60 API calls per minute among other perks. Since this is just a demo project, I believe this to be good enough but if you’d like more access, you can check out some of the other packages and subscribe to them if needed.

3. With registration complete, select the API Keys tab at the top of the resulting page. It should lead to a page with your API Key displayed as shown in the image below:

Copy the API key and paste in a notepad or any other safe place. We will need it for our code.

With this done successfully, we can proceed to write the code for the project.

Code

The Arduino IDE was used in developing the code for this project, as such, if this is the first time you are using an ESP8266 based board with the IDE, you will need to install the ESP8266 support for it. Follow this tutorial we wrote a while back on “Programming an ESP8266 Board with the Arduino IDE” to get it done.

The logic behind the code for the project is quite straight forward. We start by connecting the device to a WiFi access point through which it will access the OpenWeatherMaps server using the API key we obtained earlier. The response from OpenWeatherMap to the API call is parsed and the weather information contained is displayed on the OLED display.

The response from OpenWeatherMap is usually a JSON file, as such, to easily parse the data to obtain the information we need, we will use the Arduino Json Library which was created to help with creating or Parsing JSON files. We will also use the ESP8266 WIFI library, which will handle all network-related tasks, and the U8g2lib library to make interacting with the OLED seamless. All of the above libraries can be installed via the Arduino Library Manager or by downloading from the links attached to them and installing them manually.

As usual, I will do a quick run through the sketch and explain parts of it that I feel might be a little bit difficult to follow.

The sketch starts, as usual, with the inclusion of the libraries we will be using.

//Written  By Frenoy Osburn
// Modified by Emmanuel Odunlade

#include <ESP8266WiFi.h>
#include <stdio.h>
#include <ArduinoJson.h>
#include <U8g2lib.h>

Next, we create variables that are matched to different weather conditions with 0 representing the sun and 4 representing thunder.

#define SUN  0
#define SUN_CLOUD  1
#define CLOUD 2
#define RAIN 3
#define THUNDER 4

Next, we create an instance of the U8G2 library with the I2C pins of the Wemos D1 as arguments.

U8G2_SSD1306_128X64_NONAME_1_SW_I2C u8g2(U8G2_R0, /* clock=*/ SCL, /* data=*/ SDA, /* reset=*/ U8X8_PIN_NONE);   // All Boards without Reset of the Display

Next, we supply the credentials of the WiFI access point through which the device will access the internet and create a few more variables to be used in tracking time within the code.

//initiate the WifiClient
WiFiClient client;

const char* ssid = "mySSID";                          
const char* password = "myPASSWORD";
unsigned long lastCallTime = 0;               // last time you called the updateWeather function, in milliseconds
const unsigned long postingInterval = 30L * 1000L;  // delay between updates, in milliseconds

Next, we create a variable to hold the API key we obtained earlier from OpenWeatherMap and the name of your city or the city whose weather report you are interested in. Do note the format in which the name of the city was written in my example and write yours the same way.

String APIKEY = "put your APIKEY here";
String NameOfCity = "put the name of the city you will like to get data for here" //e.g Lagos, NG if your city is Lagos in Nigeria.  

With this done, we proceed to the void setup() function.

We start the function by initializing the OLED and creating a page which will serve as a splash screen for the device.

void setup()
{  
  u8g2.begin();
  
  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_ncenB14_tr);
    u8g2.drawStr(0,20,"Online");
    u8g2.drawStr(28,40,"Weather");
    u8g2.drawStr(56,60,"Display");
  } while ( u8g2.nextPage() );

Next, we enable the display of UTF8 symbols on the display so it’s easy to do display things like degrees among others.

u8g2.enableUTF8Print();   //for the degree symbol

Next, we initialize the serial monitor for debugging purposes and kickstart wifi communications by connecting to the APN credentials we provided earlier.

Serial.begin(115200);
  Serial.println("\n\nOnline Weather Display\n");

  Serial.println("Connecting to network");
  WiFi.begin(ssid, password);

  int counter = 0;
  while (WiFi.status() != WL_CONNECTED) 
  {
    delay(200);    
    if (++counter > 100) 
      ESP.restart();
    Serial.print( "." );
  }
  Serial.println("\nWiFi connected");
  printWifiStatus();
}

Next is the void loop() function.

The loop() function for this project simply calls the UpdateWeather function at certain intervals defined by the difference between when the last call was made and the current program run time.

void loop() 
{    
  if (millis() - lastCallTime > postingInterval) 
  {
    updateWeather();
  }
}

Next, is the void updateWeather() function. The function, when called within the loop() function, connects to the OpenWeatherMap servers and uses the API key and the city name we provided go send a request for weather information to the server.

void updateWeather()
{     
  // if there's a successful connection:
  if (client.connect("api.openweathermap.org", 80)) 
  {
    Serial.println("Connecting to OpenWeatherMap server...");
    // send the HTTP PUT request:
    client.println("GET /data/2.5/weather?q=" + NameOfCity + "&units=metric&APPID=" + APIKEY + "HTTP/1.1");
    client.println("Host: api.openweathermap.org");
    client.println("Connection: close");
    client.println();

If the server responds with the data, it is passed and each one is stored with a matching variable name.

// Check HTTP status
    char status[32] = {0};
    client.readBytesUntil('\r', status, sizeof(status));
    // It should be "HTTP/1.0 200 OK" or "HTTP/1.1 200 OK"
    if (strcmp(status + 9, "200 OK") != 0) 
    {
      Serial.print(F("Unexpected response: "));
      Serial.println(status);
      return;
    }

    // Skip HTTP headers
    char endOfHeaders[] = "\r\n\r\n";
    if (!client.find(endOfHeaders)) 
    {
      Serial.println(F("Invalid response"));
      return;
    }

    // Allocate the JSON document
    // Use arduinojson.org/v6/assistant to compute the capacity.
    const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + 2*JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(4) + 2*JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(13) + 270;
    DynamicJsonDocument doc(capacity);
    
    // Parse JSON object
    DeserializationError error = deserializeJson(doc, client);
    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.c_str());
      return;
    }
        
    int weatherId = doc["weather"][0]["id"].as<int>();
    float weatherTemperature = doc["main"]["temp"].as<float>();
    int weatherHumidity = doc["main"]["humidity"].as<int>();

The response is displayed on the serial monitor and the corresponding weather status is printed on the OLED.

Serial.println(F("Response:"));
    Serial.print("Weather: ");
    Serial.println(weatherId);
    Serial.print("Temperature: ");
    Serial.println(weatherTemperature);
    Serial.print("Humidity: ");
    Serial.println(weatherHumidity);
    Serial.println();
    
    char scrollText[15];
    sprintf(scrollText, "Humidity:%3d%%", weatherHumidity);

    if(weatherId == 800)    //clear
    {
      draw(scrollText, SUN, weatherTemperature);
    }
    else
    {
      switch(weatherId/100)
      {
        case 2:     //Thunderstorm
            draw(scrollText, THUNDER, weatherTemperature);
            break;
    
        case 3:     //Drizzle
        case 5:     //Rain
            draw(scrollText, RAIN, weatherTemperature);
            break;
    
        case 7:     //Sun with clouds
            draw(scrollText, SUN_CLOUD, weatherTemperature);
            break;
        case 8:     //clouds
            draw(scrollText, CLOUD, weatherTemperature);
            break;
        
        default:    //Sun with clouds           
            draw(scrollText, SUN_CLOUD, weatherTemperature);
            break;
      }    
    }
  } 
  else 
  {
    // if you couldn't make a connection:
    Serial.println("connection failed");
  }

  // note the time that this function was called
   lastCallTime = millis();
}

The code contains a few more functions that are used in displaying the icons. It is quite self-explanatory and you should get the idea. You can also check out one of our past tutorials on “creating custom graphics for displays” to understand better.

The complete sketch is provided below:

//Written  By Frenoy Osburn
// Modified by Emmanuel Odunlade

#include <ESP8266WiFi.h>
#include <stdio.h>
#include <ArduinoJson.h>
#include <U8g2lib.h>

#define SUN  0
#define SUN_CLOUD  1
#define CLOUD 2
#define RAIN 3
#define THUNDER 4

U8G2_SSD1306_128X64_NONAME_1_SW_I2C u8g2(U8G2_R0, /* clock=*/ SCL, /* data=*/ SDA, /* reset=*/ U8X8_PIN_NONE);   // All Boards without Reset of the Display

//initiate the WifiClient
WiFiClient client;

const char* ssid = "mySSID";                          
const char* password = "myPASSWORD";                 

unsigned long lastCallTime = 0;               // last time you called the updateWeather function, in milliseconds
const unsigned long postingInterval = 30L * 1000L;  // delay between updates, in milliseconds

String APIKEY = "put your APIKEY here";
String NameOfCity = "put the name of the city you will like to get data for here" //e.g Lagos, NG if your city is Lagos in Nigeria.  

void setup()
{  
  u8g2.begin();
  
  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_ncenB14_tr);
    u8g2.drawStr(0,20,"Online");
    u8g2.drawStr(28,40,"Weather");
    u8g2.drawStr(56,60,"Display");
  } while ( u8g2.nextPage() );
  
  u8g2.enableUTF8Print();   //for the degree symbol
  
  Serial.begin(115200);
  Serial.println("\n\nOnline Weather Display\n");

  Serial.println("Connecting to network");
  WiFi.begin(ssid, password);

  int counter = 0;
  while (WiFi.status() != WL_CONNECTED) 
  {
    delay(200);    
    if (++counter > 100) 
      ESP.restart();
    Serial.print( "." );
  }
  Serial.println("\nWiFi connected");
  printWifiStatus();
}

void loop() 
{    
  if (millis() - lastCallTime > postingInterval) 
  {
    updateWeather();
  }
}

void updateWeather()
{     
  // if there's a successful connection:
  if (client.connect("api.openweathermap.org", 80)) 
  {
    Serial.println("Connecting to OpenWeatherMap server...");
    // send the HTTP PUT request:
    client.println("GET /data/2.5/weather?q=" + NameOfCity + "&units=metric&APPID=" + APIKEY + "HTTP/1.1");
    client.println("Host: api.openweathermap.org");
    client.println("Connection: close");
    client.println();

    // Check HTTP status
    char status[32] = {0};
    client.readBytesUntil('\r', status, sizeof(status));
    // It should be "HTTP/1.0 200 OK" or "HTTP/1.1 200 OK"
    if (strcmp(status + 9, "200 OK") != 0) 
    {
      Serial.print(F("Unexpected response: "));
      Serial.println(status);
      return;
    }

    // Skip HTTP headers
    char endOfHeaders[] = "\r\n\r\n";
    if (!client.find(endOfHeaders)) 
    {
      Serial.println(F("Invalid response"));
      return;
    }

    // Allocate the JSON document
    // Use arduinojson.org/v6/assistant to compute the capacity.
    const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + 2*JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(4) + 2*JSON_OBJECT_SIZE(5) + JSON_OBJECT_SIZE(13) + 270;
    DynamicJsonDocument doc(capacity);
    
    // Parse JSON object
    DeserializationError error = deserializeJson(doc, client);
    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.c_str());
      return;
    }
        
    int weatherId = doc["weather"][0]["id"].as<int>();
    float weatherTemperature = doc["main"]["temp"].as<float>();
    int weatherHumidity = doc["main"]["humidity"].as<int>();
    
    //Disconnect
    client.stop();
    
    Serial.println(F("Response:"));
    Serial.print("Weather: ");
    Serial.println(weatherId);
    Serial.print("Temperature: ");
    Serial.println(weatherTemperature);
    Serial.print("Humidity: ");
    Serial.println(weatherHumidity);
    Serial.println();
    
    char scrollText[15];
    sprintf(scrollText, "Humidity:%3d%%", weatherHumidity);

    if(weatherId == 800)    //clear
    {
      draw(scrollText, SUN, weatherTemperature);
    }
    else
    {
      switch(weatherId/100)
      {
        case 2:     //Thunderstorm
            draw(scrollText, THUNDER, weatherTemperature);
            break;
    
        case 3:     //Drizzle
        case 5:     //Rain
            draw(scrollText, RAIN, weatherTemperature);
            break;
    
        case 7:     //Sun with clouds
            draw(scrollText, SUN_CLOUD, weatherTemperature);
            break;
        case 8:     //clouds
            draw(scrollText, CLOUD, weatherTemperature);
            break;
        
        default:    //Sun with clouds           
            draw(scrollText, SUN_CLOUD, weatherTemperature);
            break;
      }    
    }
  } 
  else 
  {
    // if you couldn't make a connection:
    Serial.println("connection failed");
  }

  // note the time that this function was called
   lastCallTime = millis();
}

void printWifiStatus() 
{
  // print the SSID of the network you're attached to:
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your board's IP address:
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print the received signal strength:
  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
}

void drawWeatherSymbol(u8g2_uint_t x, u8g2_uint_t y, uint8_t symbol)
{
  // fonts used:
  // u8g2_font_open_iconic_embedded_6x_t
  // u8g2_font_open_iconic_weather_6x_t
  // encoding values, see: https://github.com/olikraus/u8g2/wiki/fntgrpiconic
  
  switch(symbol)
  {
    case SUN:
      u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
      u8g2.drawGlyph(x, y, 69);  
      break;
    case SUN_CLOUD:
      u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
      u8g2.drawGlyph(x, y, 65); 
      break;
    case CLOUD:
      u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
      u8g2.drawGlyph(x, y, 64); 
      break;
    case RAIN:
      u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
      u8g2.drawGlyph(x, y, 67); 
      break;
    case THUNDER:
      u8g2.setFont(u8g2_font_open_iconic_embedded_6x_t);
      u8g2.drawGlyph(x, y, 67);
      break;      
  }
}

void drawWeather(uint8_t symbol, int degree)
{
  drawWeatherSymbol(0, 48, symbol);
  u8g2.setFont(u8g2_font_logisoso32_tf);
  u8g2.setCursor(48+3, 42);
  u8g2.print(degree);
  u8g2.print("°C");   // requires enableUTF8Print()
}

/*
  Draw a string with specified pixel offset. 
  The offset can be negative.
  Limitation: The monochrome font with 8 pixel per glyph
*/
void drawScrollString(int16_t offset, const char *s)
{
  static char buf[36];  // should for screen with up to 256 pixel width 
  size_t len;
  size_t char_offset = 0;
  u8g2_uint_t dx = 0;
  size_t visible = 0;
  len = strlen(s);
  if ( offset < 0 )
  {
    char_offset = (-offset)/8;
    dx = offset + char_offset*8;
    if ( char_offset >= u8g2.getDisplayWidth()/8 )
      return;
    visible = u8g2.getDisplayWidth()/8-char_offset+1;
    strncpy(buf, s, visible);
    buf[visible] = '\0';
    u8g2.setFont(u8g2_font_8x13_mf);
    u8g2.drawStr(char_offset*8-dx, 62, buf);
  }
  else
  {
    char_offset = offset / 8;
    if ( char_offset >= len )
      return; // nothing visible
    dx = offset - char_offset*8;
    visible = len - char_offset;
    if ( visible > u8g2.getDisplayWidth()/8+1 )
      visible = u8g2.getDisplayWidth()/8+1;
    strncpy(buf, s+char_offset, visible);
    buf[visible] = '\0';
    u8g2.setFont(u8g2_font_8x13_mf);
    u8g2.drawStr(-dx, 62, buf);
  }
}

void draw(const char *s, uint8_t symbol, int degree)
{
  int16_t offset = -(int16_t)u8g2.getDisplayWidth();
  int16_t len = strlen(s);
  for(;;)
  {
    u8g2.firstPage();
    do {
      drawWeather(symbol, degree);      
      drawScrollString(offset, s);
    } while ( u8g2.nextPage() );
    delay(20);
    offset+=2;
    if ( offset > len*8+1 )
      break;
  }
}

Demo

Go over the connections again and ensure you have the ESP8266’s Arduino IDE board support package installed along with the U8g2 library. Once confirmed, connect the Wemos D1 to your computer and select it in the Arduino IDE along with the port to which it is connected. Hit the upload button and wait for the completion of the upload process. When completed, you should see the weather information displayed on the OLED as shown in the image below.

Demo (Credits: bitsandblobs)

To take things further, a nice improvement BitsandBlobs made compared to my own project was the creation of a 3D printed enclosure to house the device. It makes the project look really nice and presentable, and it’s one of the reasons why I decided to share it here. The design and assembly instructions of the enclosure are available on Thingiverse, so all you need to do is to download and print it. After putting it in the 3D printed enclosure, the device should look like the image below.

Demo (Credits: bitsandblobs)

That’s it for this tutorial, thanks for reading. Do let me know via the comment section if you built one of these and also let me know if you have any challenges replicating them.

Gumstix Raspberry Pi Zero Battery Board offers two-hour mobility

Take your Raspberry Pi Zero anywhere with the Raspberry Pi Zero Battery IMU. Add a camera to enable totally wireless video streaming. Charge the two AA batteries by plugging in the Pi Zero to a micro-USB connector.

Details:

  • Memory (256Kb I2C Serial Board EEPROM)
  • CAT24C256 Series 256 kbit (32 K x 8) 1.8-5.5V I2C CMOS Serial EEPROM – TSSOP-8
  • Switch slide R/A -SWITCH SLIDE 1PDT 6VDC 0.3A SMT RIGHT ANGLE
  • Battery Holder 2xAA – Battery holder
  • IMU 16bit 3D Accel + 3D Gyro
  • IMU 16bit 3D ACCEL+3DGYRO SMD
  • Linear Technology LTC4060 Fast Battery Charger
  • Complete fast charger controller for single, 2-, 3- or 4-series cell NiMH/NiCd batteries.

Designed by Gumstix in Geppetto, the Raspberry Pi Zero Battery IMU board allows you to take your Raspberry Pi Zero anywhere. Simply pop in two rechargeable NiMH or NiCd batteries into the holder and you’re ready to go anywhere with you Raspberry Pi Zero. Charging the batteries is as easy as plugging in your Raspberry Pi Zero using a USB cable, as per usual. With the onboard Bosch BMI160 3-axis accelerometer and 3-axis gyroscope, you can easily track motion. Add a camera to enable totally wireless video streaming.

The Gumstix Raspberry Pi Zero Battery IMU is available now for $50 Pre-Order. More information may be found at the Gumstix product and shopping page.

SEGGER Releases Floating-Point Library to Support RISC-V

Comprehensive array of arithmetic functions, hand-coded & optimized in assembly language.

SEGGER‘s stand-alone Floating-Point Library has now been extended to include an assembly-optimized variant for RISC-V implementations. The library contains a complete set of high level mathematical functions that have been written in C, and uses advanced algorithms to maximize performance levels.

All of the functionality is fully verified, for both single and double precision operations. The RISC-V variant, like the existing variant for ARM, is optimized for both high-speed operation and small code size. The balance between size and speed can be configured at library build time.

The SEGGER Floating-Point Library for RISC-V is much smaller than equivalent open-source libraries currently available, while achieving up to 100 times the performance on some key operations. This library is also a part of the company’s Runtime Library, which is already included in the widely-used Embedded Studio platform.

For details on what makes a well thought-out runtime library different from a conventional runtime library, refer to the SEGGER Runtime Library webpage: https://www.segger.com/runtime-library

More information on the SEGGER Floating-Point Library can be accessed at: https://www.segger.com/floating-point-library

Detailed performance data is available at: https://wiki.segger.com/SEGGER_Floating-Point_Library

To fully experience it, download Embedded Studio (Windows, Linux and macOS) from: https://www.segger.com/downloads/embedded-studio

Advantech Releases NXP i.MX8 QuadMax ROM-7720, Qseven Module for AI and Machine Vision Application

Advantech, a global leader in the embedded computing market, is pleased to announce its first NXP i.MX8 product: ROM-7720, an Arm-based Qseven module powered by the NXP Arm® CortexTM-A72 i.MX8 high-performance processor. ROM-7720 supports 4K resolution via HDMI 2.0 with H.265 H264 hardware accelerator engines for video decoding and additional GPU for image processing. Using OpenVX, it is ideal for AI, machine vision, and big data processing and analytics applications in the IoT era.

Powerful Computing Performance for Visual Processing

ROM-7720 adopts NXP i.MX8 Quad Max SoC, which integrates two CortexTM-A72, four Cortex-A53 and two Cortex-M4F processor cores and the Vivante GC7000XS/VX graphics engine, which supports OpenGL ES, OpenCL and OpenVL (vision) SDK for developing images, data processing, and analytics applications. ROM-7720 supports display output with HDMI2.0 (3840 x 2160 @60Hz), dual 24bit LVDS for multiple displays, and 2 + 4 lanes MIPI-CSI camera input. It supports a high resolution display and meets the camera requirements of different machine vision applications.

New High Speed Interface for AI and Machine Vision Applications

ROM-7720 offers new high speed I/O, including USB 3.0, PCIe 3.0 and SATA 3.0. It provides efficient interfaces for extending peripheral devices, such as the SSD, 5G cellular card, and FPGA IC to empower new AI and machine vision development.

Value-Added Embedded Software Services: AIM-Android Services

Advantech will support allied, industrial and modular (AIM) frameworks for Linux applications that help accelerate software development via flexible, long-term support. AIM-Linux services offer verified embedded OS platforms and industrial-focused apps and SDKs through which users can easily select the embedded software tools they need to focus on their vertical software development.

ROM-7720 Key Features

  • NXP i.MX8 QuadMax 8-Core Processor (2x CortexTM-A72, 4x Cortex-A53 and 2x Cortex-M4F)
  • 64-bit LPDDR4 2GB/4GB
  • 4K h.265 decoder, HD h.264 encoder capability
  • Onboard QSPI Flash 256MB, eMMC Flash 8GB, boot selection from SPI/MMC/SD or SATA
  • 3 USB 3.0 with OTG, 4K HDMI, Dual MIPI Camera
  • Multi OS support in Yocto Linux and Android

Advantech ROM-7720 is available now! Please contact an Advantech sales office or authorized channel partner to learn more about it. For more information on Advantech’s Arm computing products and services, visit risc.advantech.com now!

www.advantech.com

TOP PCB Companies