BME680 Webserver for raspberry Pi, using Python and Flask

To create a simple web server on a Raspberry Pi that displays readings from a BME680 sensor, you’ll need to set up a few things. Here’s a guide on how to do it:

  1. Install Required Libraries:
    • Ensure that you have Flask (a micro web framework) installed to create a simple web server.
    • Install Adafruit_BME680 to interact with the BME680 sensor.
    • Run the following commands to install the required libraries:
sudo apt-get update
sudo apt-get install python3-pip
pip3 install flask
pip3 install adafruit-circuitpython-bme680

Wire the BME680 Sensor to the Raspberry Pi:

  • Connect the BME680 sensor to the Raspberry Pi via I2C.
  • Enable I2C on the Raspberry Pi using raspi-config.
  • Make the following connections: SDA to SDA, SCL to SCL, VCC to 3.3V, GND to GND.

Create a Python Script for the Web Server:

  • I am using Flask to create a simple web server that displays sensor readings.
  • Using Adafruit’s library to fetch readings from the BME680 sensor.
from flask import Flask, render_template_string
import adafruit_bme680
import board
import busio

# Create Flask app
app = Flask(__name__)

# Set up I2C bus and BME680 sensor
i2c = busio.I2C(board.SCL, board.SDA)
bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c)

# Set the temperature offset (optional, adjust if needed)
bme680.sea_level_pressure = 1013.25  # Standard sea level pressure in hPa

# Flask route for the home page
@app.route('/')
def index():
    # Get sensor readings
    temperature = bme680.temperature  # Celsius
    pressure = bme680.pressure  # hPa
    humidity = bme680.humidity  # %
    gas_resistance = bme680.gas  # Ohms

    # Display the readings on a simple web page
    html_content = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <title>BME680 Sensor Readings</title>
    </head>
    <body>
        <h1>BME680 Sensor Readings</h1>
        <p>Temperature: {temperature:.2f} °C</p>
        <p>Humidity: {humidity:.2f} %</p>
        <p>Pressure: {pressure:.2f} hPa</p>
        <p>Gas Resistance: {gas_resistance:.2f} Ohms</p>
    </body>
    </html>
    """
    return render_template_string(html_content)

# Run the Flask app
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

Save the script to a file, e.g., webserver.py.

Run the script with Python:

python3 webserver.py

You should see a message indicating that the server is running and available on your Raspberry Pi’s IP address on port 5000.

Accessing the Web Server:

Open a web browser on your Raspberry Pi or any other device on the same network. Navigate to http://<your_raspberry_pi_ip&gt;:5000/. You should see the sensor readings displayed on the web page.

Port 5000 is often used as a default port for running development servers in frameworks like Flask. It is not reserved for any specific service by default, and it’s commonly used in tutorial and development environments.

Here’s why I picked port 5000:

  1. Common Flask Default:
    • Flask’s default behavior when running a development server is to use port 5000. Many examples and tutorials use this port, so it’s recognizable.
  2. Unassigned by IANA:
    • The Internet Assigned Numbers Authority (IANA) maintains a list of officially assigned ports. Port 5000 is not specifically reserved for any known services, reducing the risk of conflicts with other common applications or system services.
  3. Convenience:
    • It is a high-numbered port, less likely to require administrative privileges to bind to it (as might be required for lower-numbered ports).
  4. Flexibility:
    • You can change the port if needed by modifying the code or using an environment variable. This makes it easy to adapt to different environments or to avoid conflicts.

Despite these reasons, you can use other ports if port 5000 doesn’t suit your environment. If there’s already a service running on port 5000, or if you have specific security or network constraints, you can select a different port. Just make sure it doesn’t conflict with well-known or commonly used ports. To change the port in Flask, you can specify a different port when starting the server or change the app.run line in the script:

# Example: Changing to port 8000
app.run(host='0.0.0.0', port=8000)

However, if you prefer to run it on port 80, that is possible too

LoRa Receiver with RFM95

#include <RH_RF95.h> // RadioHead library for RFM95 LoRa

#define RFM95_CS 10 // Chip select pin for the RFM95
#define RFM95_RST 9 // Reset pin for the RFM95
#define RFM95_INT 2 // Interrupt pin for the RFM95

RH_RF95 rf95(RFM95_CS, RFM95_INT);

void setup() {
  Serial.begin(9600); // Start serial communication for output
  pinMode(RFM95_RST, OUTPUT); // Set RFM95 reset pin as output

  // Reset the RFM95 module
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);

  // Initialize the RF95 module with frequency (example: 915 MHz for USA)
  if (!rf95.init()) {
    Serial.println("RFM95 initialization failed.");
    while (1); // Stop if initialization fails
  }

  rf95.setFrequency(915.0); // Set frequency (adjust for your region)
  rf95.setTxPower(23, false); // Set transmission power
  rf95.setModeRx(); // Set the module to receive mode
}

void loop() {
  // Check if there's a received message
  if (rf95.available()) {
    uint8_t buf[RH_RF95_MAX_MESSAGE_LEN]; // Buffer for incoming message
    uint8_t len = sizeof(buf); // Length of the buffer

    if (rf95.recv(buf, &len)) { // Receive the message
      Serial.print("Received: ");
      Serial.write(buf, len); // Output the message to the serial monitor
      Serial.println(); // New line for readability
    } else {
      Serial.println("Receive failed"); // Error handling
    }
  }

  delay(100); // Small delay to avoid busy loop
}

  • This sketch initializes the RFM95 module and sets its frequency and transmission power.
  • It initializes the DS18B20 sensor to get temperature readings.
  • Every 15 minutes (900,000 milliseconds), it reads the temperature and sends it via LoRa.
  • The message is formatted to include the temperature reading in Celsius.
  • The sketch sends the message using the RadioHead library, waits for the packet to be sent, and then delays for 15 minutes before repeating the process.

Before uploading this sketch, ensure you’ve installed the RadioHead, OneWire, and DallasTemperature libraries. The frequency and transmission power settings may vary depending on your region and specific application. Make sure to use the appropriate frequency and transmission power for your hardware and regulations in your country.

The program provided will receive any LoRa message on the same frequency and with the same modulation settings (e.g., bandwidth, spreading factor, coding rate). It does not inherently restrict itself to specific senders or messages.

To ensure that this receiver program only listens for messages sent by a specific source, there are several strategies one could employ:

  1. Unique Identifiers: Modify the sender program to prepend a unique identifier to each message. Then, the receiver can check for this identifier before processing the message.
  2. Addressing: The RadioHead library has an addressing mechanism that allows you to assign unique addresses to each node. This way, the receiver only processes messages intended for its address.
  3. Encryption: If security is a concern, you can add encryption to ensure only authorized receivers can understand the messages. This would require additional libraries and complexity.

Here’s an updated version of the receiver program that looks for a specific identifier at the beginning of the message. This identifier could be a unique sequence of characters known to both the sender and receiver:

#include <RH_RF95.h> // RadioHead library for RFM95 LoRa

#define RFM95_CS 10 // Chip select pin for the RFM95
#define RFM95_RST 9 // Reset pin for the RFM95
#define RFM95_INT 2 // Interrupt pin for the RFM95

RH_RF95 rf95(RFM95_CS, RFM95_INT);

const char expectedIdentifier[] = "SENSOR01"; // Unique identifier for valid messages

void setup() {
  Serial.begin(9600); // Start serial communication for output
  pinMode(RFM95_RST, OUTPUT); // Set RFM95 reset pin as output

  // Reset the RFM95 module
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);

  // Initialize the RF95 module with frequency (adjust as needed)
  if (!rf95.init()) {
    Serial.println("RFM95 initialization failed.");
    while (1); // Stop if initialization fails
  }

  rf95.setFrequency(915.0); // Set frequency (adjust for your region)
  rf95.setTxPower(23, false); // Set transmission power
  rf95.setModeRx(); // Set the module to receive mode
}

void loop() {
  // Check if there's a received message
  if (rf95.available()) {
    uint8_t buf[RH_RF95_MAX_MESSAGE_LEN]; // Buffer for incoming message
    uint8_t len = sizeof(buf); // Length of the buffer

    if (rf95.recv(buf, &len)) { // Receive the message
      buf[len] = '\0'; // Null-terminate the received message
      
      // Check if the message starts with the expected identifier
      if (strncmp((char*)buf, expectedIdentifier, strlen(expectedIdentifier)) == 0) {
        Serial.print("Received valid message: ");
        Serial.write(buf, len); // Output the message to the serial monitor
        Serial.println(); // New line for readability
      } else {
        Serial.println("Received invalid message");
      }
    } else {
      Serial.println("Receive failed"); // Error handling
    }
  }

  delay(100); // Small delay to avoid busy loop
}

LoRa transmitter with RFM95

This program does the following:

  • Connects to the RFM95 module for LoRa communication.
  • Reads temperature from the DS18B20 sensor.
  • Sends temperature data via LoRa every 15 minutes (900,000 milliseconds).
#include <RH_RF95.h> // RadioHead library for RFM95 LoRa
#include <OneWire.h> // OneWire communication
#include <DallasTemperature.h> // DS18B20 temperature sensor

#define RFM95_CS 10 // Chip select pin for the RFM95
#define RFM95_RST 9 // Reset pin for the RFM95
#define RFM95_INT 2 // Interrupt pin for the RFM95
#define ONE_WIRE_BUS 3 // DS18B20 sensor connected to pin 3

RH_RF95 rf95(RFM95_CS, RFM95_INT);
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

void setup() {
  Serial.begin(9600); // Serial for debugging
  pinMode(RFM95_RST, OUTPUT); // Set RFM95 reset pin as output

  // Reset the RFM95 module
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);

  // Initialize the RF95 module with frequency (example: 915 MHz for USA)
  if (!rf95.init()) {
    Serial.println("RFM95 initialization failed.");
    while (1); // Stop if initialization fails
  }

  rf95.setFrequency(915.0); // Set frequency (adjust for your region)
  rf95.setTxPower(23, false); // Set transmission power

  sensors.begin(); // Initialize the temperature sensor
}

void loop() {
  // Get the temperature reading
  sensors.requestTemperatures();
  float temperature = sensors.getTempCByIndex(0); // Temperature in Celsius

  // Create the message to send
  char msg[32];
  snprintf(msg, sizeof(msg), "Temp: %.2f °C", temperature);

  // Send the message via LoRa
  rf95.send((uint8_t *)msg, strlen(msg));
  rf95.waitPacketSent(); // Wait until packet is sent

  Serial.print("Sent: ");
  Serial.println(msg);

  // Wait 15 minutes before sending the next reading
  delay(900000); // 15 minutes in milliseconds
}

  • This sketch initializes the RFM95 module and sets its frequency and transmission power.
  • It initializes the DS18B20 sensor to get temperature readings.
  • Every 15 minutes (900,000 milliseconds), it reads the temperature and sends it via LoRa.
  • The message is formatted to include the temperature reading in Celsius.
  • The sketch sends the message using the RadioHead library, waits for the packet to be sent, and then delays for 15 minutes before repeating the process.

Before uploading this sketch, ensure you’ve installed the RadioHead, OneWire, and DallasTemperature libraries. The frequency and transmission power settings may vary depending on your region and specific application. Make sure to use the appropriate frequency and transmission power for your hardware and regulations in your country.

Arduino Webserver with W5500 Shield

For nostalgia reasons an arduino Webserver using the W5500 shield:

#include <Ethernet.h> // Library for W5500-based Ethernet shield
#include <OneWire.h>  // Library for OneWire communication
#include <DallasTemperature.h> // Library for DS18B20 temperature sensor

// Pin where the DS18B20 is connected
#define ONE_WIRE_BUS 2
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

EthernetServer server(80); // Ethernet server on port 80

void setup() {
  Serial.begin(9600); // Start the serial communication for debugging

  // Set up the Ethernet shield with a fixed MAC address and IP address
  byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; // MAC address
  IPAddress ip(192, 168, 1, 177); // Local IP address (adjust as needed for your network)

  // Start the Ethernet and the server
  Ethernet.begin(mac, ip);
  server.begin();
  Serial.print("Server is at ");
  Serial.println(Ethernet.localIP());

  // Start the temperature sensor
  sensors.begin();
}

void loop() {
  // Check for incoming client connections
  EthernetClient client = server.available();
  if (client) {
    Serial.println("New client connected");

    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();

        // If the request is complete (double newline), send the response
        if (c == '\n' && currentLineIsBlank) {
          sensors.requestTemperatures(); // Request temperature
          float temperature = sensors.getTempCByIndex(0); // Get temperature in Celsius

          // Send the HTTP response headers
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          client.println("Connection: close"); // Connection will close after this response
          client.println();

          // Send the HTML content with temperature
          client.println("<!DOCTYPE HTML>");
          client.println("<html>");
          client.println("<h1>Temperature Monitor</h1>");
          client.print("<p>Temperature: ");
          client.print(temperature);
          client.println(" °C</p>");
          client.println("</html>");
          break;
        }

        if (c == '\n') {
          currentLineIsBlank = true;
        } else if (c != '\r') {
          currentLineIsBlank = false;
        }
      }
    }

    // Close the connection with the client
    client.stop();
    Serial.println("Client disconnected");
  }
}

  • This sketch initializes the Ethernet shield with a specified MAC address and a static IP address. You can adjust the IP address as needed to fit your network configuration.
  • It creates an Ethernet server on port 80 to listen for HTTP requests.
  • The sketch initializes the DS18B20 temperature sensor on the OneWire bus (connected to pin 2).
  • When a client connects, the code reads the temperature from the sensor and serves a simple HTML page showing the temperature in degrees Celsius.
  • The connection to the client is closed after sending the HTTP response.

Before uploading this sketch, ensure you have installed the Ethernet, OneWire, and DallasTemperature libraries. Adjust the IP address and MAC address as needed to suit your network configuration. If you’re using DHCP, you can remove the explicit IP address and use Ethernet.begin(mac) to get an IP address from your router.

Arduino Webserver for W5100 Shield

For Nostalgia’s sake: An Arduino Uno with a W5100 ethernetshield, serving a webpage with results of a DS18N20

#include <Ethernet.h> // Library for W5100-based Ethernet shield
#include <OneWire.h>  // Library for OneWire communication
#include <DallasTemperature.h> // Library for DS18B20 temperature sensor

// Pin where the DS18B20 is connected
#define ONE_WIRE_BUS 2
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

EthernetServer server(80); // Ethernet server on port 80

void setup() {
  // Start the serial communication for debugging
  Serial.begin(9600);

  // Set up the Ethernet shield with a fixed MAC address and IP address
  byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; // MAC address
  IPAddress ip(192, 168, 1, 177); // Local IP address (adjust as needed for your network)

  // Start the Ethernet and the server
  Ethernet.begin(mac, ip);
  server.begin();
  Serial.print("Server is at ");
  Serial.println(Ethernet.localIP());

  // Start the temperature sensor
  sensors.begin();
}

void loop() {
  // Check for incoming client connections
  EthernetClient client = server.available();
  if (client) {
    Serial.println("New client");

    // Wait for data from the client
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();

        // If the request is complete (double newline), send the response
        if (c == '\n' && currentLineIsBlank) {
          // Get the temperature reading
          sensors.requestTemperatures(); // Request temperature
          float temperature = sensors.getTempCByIndex(0); // Get temperature in Celsius

          // Send the HTTP response headers
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          client.println("Connection: close"); // The connection will close after this response
          client.println();

          // Send the HTML content
          client.println("<!DOCTYPE HTML>");
          client.println("<html>");
          client.println("<h1>Temperature Monitor</h1>");
          client.print("<p>Temperature: ");
          client.print(temperature);
          client.println(" °C</p>");
          client.println("</html>");
          break;
        }

        if (c == '\n') {
          currentLineIsBlank = true;
        } else if (c != '\r') {
          currentLineIsBlank = false;
        }
      }
    }

    // Close the connection with the client
    client.stop();
    Serial.println("Client disconnected");
  }
}

  • This sketch initializes the Ethernet shield with a MAC address and a static IP address. Adjust the IP address and MAC address to fit your network configuration.
  • It creates an Ethernet server listening on port 80 for HTTP requests.
  • The sketch initializes the DS18B20 temperature sensor on a OneWire bus (here on digital pin 2).
  • When a client connects, the code reads from the temperature sensor and serves a simple HTML page with the temperature in degrees Celsius.
  • The connection to the client is closed after sending the HTTP response.

With this sketch, the Ethernet shield serves as a basic web server that provides temperature data from the DS18B20 sensor. Before uploading, ensure the Ethernet, OneWire, and DallasTemperature libraries are installed in your Arduino IDE.

Arduino Webserver with ENC28J60

From the prehistory: an Arduino Uno with ENC28J60 Etherrnet card, serving a webpage. Just out of nostalgia if someone needs it

#include <UIPEthernet.h> // Library for ENC28J60 Ethernet shield
#include <OneWire.h>     // Library for OneWire communication
#include <DallasTemperature.h> // Library for DS18B20 temperature sensor

// Pin where the DS18B20 is connected
#define ONE_WIRE_BUS 2
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

EthernetServer server(80); // Ethernet server on port 80

void setup() {
  // Start the serial communication for debugging
  Serial.begin(9600);

  // Set up the Ethernet shield with a fixed MAC address and IP address
  byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; // MAC address
  IPAddress ip(192, 168, 1, 177); // Local IP address (adjust as needed for your network)

  // Start the Ethernet and the server
  Ethernet.begin(mac, ip);
  server.begin();
  Serial.print("Server is at ");
  Serial.println(Ethernet.localIP());

  // Start the temperature sensor
  sensors.begin();
}

void loop() {
  // Check for incoming client connections
  EthernetClient client = server.available();
  if (client) {
    Serial.println("New client");

    // Wait for data from the client
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();

        // If the request is complete (double newline), send the response
        if (c == '\n' && currentLineIsBlank) {
          // Get the temperature reading
          sensors.requestTemperatures(); // Request temperature
          float temperature = sensors.getTempCByIndex(0); // Get temperature in Celsius

          // Send the HTTP response headers
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          client.println("Connection: close"); // The connection will close after this response
          client.println();

          // Send the HTML content
          client.println("<!DOCTYPE HTML>");
          client.println("<html>");
          client.println("<h1>Temperature Monitor</h1>");
          client.print("<p>Temperature: ");
          client.print(temperature);
          client.println(" °C</p>");
          client.println("</html>");
          break;
        }

        if (c == '\n') {
          currentLineIsBlank = true;
        } else if (c != '\r') {
          currentLineIsBlank = false;
        }
      }
    }

    // Close the connection with the client
    client.stop();
    Serial.println("Client disconnected");
  }
}

  • This sketch initializes the Ethernet shield with a given MAC address and IP address. Adjust the IP address as needed for your network.
  • The OneWire library is used to communicate with the DS18B20 temperature sensor, and the DallasTemperature library is used to read the temperature in Celsius.
  • The Ethernet server listens on port 80 for incoming HTTP requests.
  • When a client connects, the sketch reads the incoming data and responds with a simple HTML page showing the temperature in degrees Celsius.
  • The sketch handles client connections by waiting for a complete HTTP request and then sending the temperature reading in the response.

To use this sketch, make sure you have installed the UIPEthernet, OneWire, and DallasTemperature libraries. You can install them via the Arduino Library Manager. Adjust the IP address and MAC address as needed to fit your network configuration.

Self updating OTA firmware for an ESP8266

Self updating Software for an ESP can easily be setup. As I hate reinventing the wheel, I found a code on Bakke-online, but sadly that did not work. The main reason seemed to be that the httpcalls he used were deprecated (his code is from 2017), but even after correcting those calls, I kept having problem, so I set out to rewrite the code. I do use his file organizing structure though. On a server I set up two files: a text file called (macnummer).version and a bin file with the new software called (macnummer).bin. The version file only has one line with the version of the new software.

What the software below does is it makes a WiFi connection, then does an http call to the version file, reads the number, compares it to the version number in the running program and if the number in the file is bigger than the number in the program, it does an update. The bin file and the version file I stored in a folder called fota (akin to what Bakke does). As I am on Linux, using an apache server, that fota directory should go into the 'var/www/html' folder.1

The program looks like this:

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>

WiFiClient client;

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

  WiFi.begin("SSID", "PW");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nConnected to Wi-Fi.");

  // Get the current firmware version
  const int currentFirmwareVersion = 1; // Update with your firmware version

  // Form the URL for checking firmware version
  String mac = "104000e7fe3f"; // or your MAC retrieval function
  String fwVersionURL = "http://192.168.1.133/fota/" + mac + ".version";

  HTTPClient httpClient;
  httpClient.begin(client, fwVersionURL); // Initialize with client and URL
  int httpCode = httpClient.GET(); // Perform HTTP GET request
  Serial.print("HTTP code: ");
  Serial.println(httpCode);

  if (httpCode == HTTP_CODE_OK) {
    String serverFirmwareVersionStr = httpClient.getString();
    int serverFirmwareVersion = serverFirmwareVersionStr.toInt();

    Serial.print("Current firmware version: ");
    Serial.println(currentFirmwareVersion);
    Serial.print("Server firmware version: ");
    Serial.println(serverFirmwareVersion);

    if (serverFirmwareVersion > currentFirmwareVersion) { // Check if an update is needed
      Serial.println("A new firmware version is available.");

      // Form the URL for the firmware binary
      String fwBinaryURL = "http://192.168.1.133/fota/" + mac + ".bin";

      // Perform the OTA update
      t_httpUpdate_return ret = ESPhttpUpdate.update(client, fwBinaryURL);

      if (ret == HTTP_UPDATE_OK) {
        Serial.println("OTA update successful.");
      } else {
        Serial.printf("OTA update failed: %s", ESPhttpUpdate.getLastErrorString().c_str());
      }
    } else {
      Serial.println("Firmware is up to date.");
    }
  } else {
    Serial.println("Failed to check firmware version.");
  }

  httpClient.end(); // Clean up HTTP client
}

void loop() {
  // Your code here
}

The program does a one time test from the Setup, but the procedure can be called from anywhere. It is ideal for updating software in applications that go into deepsleep for some time. When it does do an OTA update, you may get a crash message in the monitor, just be patient and wait for the restart. The “.bin” file is obtained by going to “Sketch-Export compiled Binary” in the arduino IDE. The compiled file can then be found in the program directory of the arduino sketch book. It should then be copied to the “var/www/html/fota” directory and renamed with the MAC number of the ESP8266 that needs updating. Only after the new code has ben placed there, should you update the version number in the (macnr).version file

A word on subnets

If your update server and your ESP8266 are on different subnets in your network, you may encounter problems in the ESP8266 being able to reach the update server and you may need to reconfigure your network. In general though if your ESP8266 is on a lower subnet (e.g. (192.168.1.xxx) than your update server (e.g. 9192.168.2.xxx) , there should not be a problem. also, when your update server is on a totally different network (say a remote internet server somewhere) you should also not expect any problems

  1. (If you are using Nginx, an HTTP request to the base IP address might map to a default directory like /usr/share/nginx/html. In Windows with IIS it is usually c:\Inetpub\wwwroot, if not: Start > Control Panel > Administrative Tools > Internet Information Services, and open up the Sites tab. Find the “Default Web Site” and right click and select Explore. That should open the directory for you.) ↩︎

Asynchronous Webserver for Raspberry Pi

An asynchronous webserver reading a BME280, for the Raspberry Pi

import asyncio
import aiohttp
import smbus2
import bme280
from aiohttp import web

async def get_sensor_data():
    # I2C bus (0 on older Raspberry Pi models, 1 on newer models)
    bus = smbus2.SMBus(1)

    # BME280 address
    address = 0x76

    # BME280 sensor setup
    calibration_params = bme280.load_calibration_params(bus, address)
    data = bme280.sample(bus, address, calibration_params)

    # Format data
    temperature = round(data.temperature, 2)
    pressure = round(data.pressure, 2)
    humidity = round(data.humidity, 2)

    return {
        "temperature": temperature,
        "pressure": pressure,
        "humidity": humidity
    }

async def sensor_data_handler(request):
    data = await get_sensor_data()
    return web.json_response(data)

async def serve_sensor_data():
    app = web.Application()
    app.router.add_get('/sensor-data', sensor_data_handler)
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '0.0.0.0', 8080)
    await site.start()

async def main():
    while True:
        await serve_sensor_data()
        await asyncio.sleep(15)

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

The program utilizes the smbus2 library for I2C communication with the sensor and the aiohttp library for asynchronous web serving.

Make sure you have the necessary libraries installed. You can install them using pip:

pip install aiohttp smbus2 bme280

This program creates an asynchronous web server using aiohttp, serving sensor data from the BME280 sensor attached to the Raspberry Pi. The sensor data is refreshed every 15 seconds. You can access the sensor data by sending a GET request to http://<raspberry_pi_ip>:8080/sensor-data.

The aiohttp library itself doesn’t provide functionality specifically for serving personalized CSS code. However, you can certainly incorporate CSS into your web server responses by serving HTML pages that link to CSS files. This allows you to personalize the appearance of your web pages.

Here’s a modified version of the previous code to include serving an HTML page with a linked CSS file:

import asyncio
import aiohttp
import smbus2
import bme280
from aiohttp import web

async def get_sensor_data():
    # Simulated sensor data for demonstration
    return {
        "temperature": 25.5,
        "pressure": 1013.25,
        "humidity": 50.0
    }

async def sensor_data_handler(request):
    data = await get_sensor_data()
    return web.json_response(data)

async def serve_sensor_page(request):
    with open('sensor_page.html', 'rb') as f:
        return web.Response(body=f.read(), content_type='text/html')

async def serve_sensor_data():
    app = web.Application()
    app.router.add_get('/sensor-data', sensor_data_handler)
    app.router.add_get('/', serve_sensor_page)
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '0.0.0.0', 8080)
    await site.start()

async def main():
    while True:
        await serve_sensor_data()
        await asyncio.sleep(15)

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

You would need to create an HTML file named sensor_page.html and include a link to your CSS file within it. For example:

<!DOCTYPE html>
<html>
<head>
    <title>Sensor Data</title>
    <link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
    <h1>Sensor Data</h1>
    <div id="sensor-data"></div>
    <script src="script.js"></script>
</body>
</html>

In this example, the CSS file is named styles.css and should be placed in the same directory as the HTML file. You can personalize the appearance of your web page by editing the CSS file.

You can create a JavaScript file (script.js) if you need to add client-side functionality to your web page, such as updating the displayed sensor data dynamically without refreshing the entire page. If you don’t need JavaScript functionality, you can omit this file.

Extracting sunrise/sunset data from Openweathermap

If you are using Openweathermap a typical API call wil get you the following jsondata:

{"cod":"200","message":0,"cnt":1,"list":[{"dt":1709089200,"main":{"temp":4.17,"feels_like":1.35,"temp_min":4.17,"temp_max":5.21,"pressure":1018,"sea_level":1018,"grnd_level":1018,"humidity":81,"temp_kf":-1.04},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"clouds":{"all":44},"wind":{"speed":3.22,"deg":226,"gust":8.12},"visibility":10000,"pop":0,"sys":{"pod":"n"},"dt_txt":"2024-02-28 03:00:00"}],"city":{"id":2759794,"name":"Amsterdam","coord":{"lat":52.374,"lon":4.8897},"country":"NL","population":2122311,"timezone":3600,"sunrise":1709101842,"sunset":1709140552}}

If you are interested in the sunrise and sunset times, those can be extracted from that call and as you can see, they are at the end of the jsondata.
let’s have a more stuctured look:

Extracting the required data from that json can be done as follows (making the api call itself is beyond the scope of this article, as I presume you have that one, when using OpenWeathermap).

// Fetch JSON data through API call
json_data = fetchJSONData(); // Assuming you have a function to fetch JSON data
if (json_data == "") {
Serial.println("Failed to fetch JSON data");
return;
}

We then use the proper setup for the json parsing:

// Parse JSON data
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, json_data);

if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
return;
}

and we extract the jsondata as follows:

// Extract timezone offset
long timezone_offset = doc["city"]["timezone"];

// Extract sunrise and sunset times
long sunrise_unix = doc["city"]["sunrise"];
long sunset_unix = doc["city"]["sunset"];

// Convert sunrise and sunset times to local time
long sunrise_local = sunrise_unix + timezone_offset;
long sunset_local = sunset_unix + timezone_offset;

// Print local sunrise and sunset times
Serial.print("Local Sunrise: ");
printTime(sunrise_local);
Serial.print("Local Sunset: ");
printTime(sunset_local);
}

As most likely you are not living on or near the Greenwich meridian, the ‘timezone’ field is interesting as well (presuming you have set that up in your api call for the city you are interested in), so we add that(or subtract if you are east) to the isolated times for sunrise and sunset. As the JSON contains the time in UNIX format we need to convert it to human readable time. For that we use the following procedure:

// Function to convert Unix timestamp to readable time
void printTime(long timestamp) {
struct tm timeinfo;
time_t tstamp = (time_t)timestamp; // Cast timestamp to time_t
gmtime_r(&tstamp, &timeinfo);
timeinfo.tm_isdst = 0; // Set daylight saving time to zero
timestamp = mktime(&timeinfo); // Convert back to timestamp
char time_str[20];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", &timeinfo);
Serial.println(time_str);
}

Several Arduino Time libraries can do that for you as well, but it is only a few lines of code. If one would be interested in the length of day, that can be obtained by the function:

unsigned int hours;
unsigned int minutes;

void secondsToHoursMinutes(unsigned long seconds) {
// Calculate total minutes
unsigned int totalMinutes = seconds / 60;

// Calculate hours and remaining minutes
hours = totalMinutes / 60;
unsigned int remainingSeconds = seconds % 60;

// Round remaining seconds to the closest whole minute
if (remainingSeconds >= 30) {
minutes = totalMinutes + 1; // Round up to the next whole minute
} else {
minutes = totalMinutes; // Remainder is less than 30 seconds, no need to round
}
}

total minutes and hours need to be defined as global unsigned int variable and the amount of seconds needed comes from subtracting the unix times for sunset and sunrise

Setting up a BLE server on ESP32

The following program reads one of the analog ports of an ESP32 and makes the read value available through BLE (Bluetooth Low Energy)

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

#define ANALOG_PIN 34 // Analog pin to read from
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

BLEServer* pServer = NULL;
BLECharacteristic* pCharacteristic = NULL;
bool deviceConnected = false;
int analogValue = 0;

class MyServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
};

void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
}
};

void setup() {
Serial.begin(115200);
pinMode(ANALOG_PIN, INPUT);

BLEDevice::init("ESP32 BLE Analog");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());

BLEService *pService = pServer->createService(SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
pCharacteristic->addDescriptor(new BLE2902());

pService->start();

BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->start();
}

void loop() {
if (deviceConnected) {
analogValue = analogRead(ANALOG_PIN);
String valueString = String(analogValue);
pCharacteristic->setValue(valueString.c_str());
pCharacteristic->notify();
Serial.println("Sent: " + valueString);
delay(10000); // Send every 10 seconds
}
}

A similar program but now for a BME280 sensor

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

#define BME_SDA 21
#define BME_SCL 22
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

BLEServer* pServer = NULL;
BLECharacteristic* pCharacteristic = NULL;
bool deviceConnected = false;
Adafruit_BME280 bme; // I2C

class MyServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
};

void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
}
};

void setup() {
Serial.begin(115200);
Wire.begin(BME_SDA, BME_SCL);

if (!bme.begin()) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}

BLEDevice::init("ESP32 BLE BME280");
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());

BLEService *pService = pServer->createService(SERVICE_UUID);
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
pCharacteristic->addDescriptor(new BLE2902());

pService->start();

BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->start();
}

void loop() {
if (deviceConnected) {
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0;

String valueString = "T: " + String(temperature) + "C, H: " + String(humidity) + "%, P: " + String(pressure) + "hPa";
pCharacteristic->setValue(valueString.c_str());
pCharacteristic->notify();
Serial.println("Sent: " + valueString);
delay(10000); // Send every 10 seconds
}
}

Make sure to choose unique UUID’s for each program. One can generate UUID’s here:

  1. UUID Generator by uuidgenerator.net: uuidgenerator.net
  2. Online UUID Generator by onlinewebtool.com: onlinewebtool.com
  3. UUIDTools by toolslick.com: toolslick.com
  4. UUID Generator by generate.plus: generate.plus
  5. UUID Generator by randomlists.com: randomlists.com