381 lines
14 KiB
C++
381 lines
14 KiB
C++
/*
|
|
web_server.ino
|
|
|
|
Example code for serving a web page over a WiFi network, displaying
|
|
environment data read from the Metriful MS430.
|
|
|
|
This example is designed for the following WiFi enabled hosts:
|
|
* Arduino Nano 33 IoT
|
|
* Arduino MKR WiFi 1010
|
|
* ESP8266 boards (e.g. Wemos D1, NodeMCU)
|
|
* ESP32 boards (e.g. DOIT DevKit v1)
|
|
|
|
All environment data values are measured and displayed on a text
|
|
web page generated by the host, which acts as a simple web server.
|
|
|
|
The host can either connect to an existing WiFi network, or generate
|
|
its own for other devices to connect to (Access Point mode).
|
|
|
|
Copyright 2020 Metriful Ltd.
|
|
Licensed under the MIT License - for further details see LICENSE.txt
|
|
|
|
For code examples, datasheet and user guide, visit
|
|
https://github.com/metriful/sensor
|
|
*/
|
|
|
|
#include <Metriful_sensor.h>
|
|
#include <WiFi_functions.h>
|
|
|
|
//////////////////////////////////////////////////////////
|
|
// USER-EDITABLE SETTINGS
|
|
|
|
// Choose how often to read and update data (every 3, 100, or 300 seconds)
|
|
// The web page can be refreshed more often but the data will not change
|
|
uint8_t cycle_period = CYCLE_PERIOD_3_S;
|
|
|
|
// Choose whether to create a new WiFi network (host as Access Point),
|
|
// or connect to an existing WiFi network.
|
|
bool createWifiNetwork = false;
|
|
// If creating a WiFi network, a static (fixed) IP address ("theIP") is
|
|
// specified by the user. Otherwise, if connecting to an existing
|
|
// network, an IP address is automatically allocated and the serial
|
|
// output must be viewed at startup to see this allocated IP address.
|
|
|
|
// Provide the SSID (name) and password for the WiFi network. Depending
|
|
// on the choice of createWifiNetwork, this is either created by the
|
|
// host (Access Point mode) or already exists.
|
|
// To avoid problems, do not create a network with the same SSID name
|
|
// as an already existing network.
|
|
char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name)
|
|
char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password; must be at least 8 characters
|
|
|
|
// Choose a static IP address for the host, only used when generating
|
|
// a new WiFi network (createWifiNetwork = true). The served web
|
|
// page will be available at http://<IP address here>
|
|
IPAddress theIP(192, 168, 12, 20);
|
|
// e.g. theIP(192, 168, 12, 20) means an IP of 192.168.12.20
|
|
// and the web page will be at http://192.168.12.20
|
|
|
|
// END OF USER-EDITABLE SETTINGS
|
|
//////////////////////////////////////////////////////////
|
|
|
|
#if !defined(HAS_WIFI)
|
|
#error ("This example program has been created for specific WiFi enabled hosts only.")
|
|
#endif
|
|
|
|
WiFiServer server(80);
|
|
uint16_t refreshPeriodSeconds;
|
|
|
|
// Structs for data
|
|
AirData_t airData = {0};
|
|
AirQualityData_t airQualityData = {0};
|
|
LightData_t lightData = {0};
|
|
ParticleData_t particleData = {0};
|
|
SoundData_t soundData = {0};
|
|
|
|
// Storage for the web page text
|
|
char lineBuffer[100] = {0};
|
|
char pageBuffer[2300] = {0};
|
|
|
|
void setup() {
|
|
// Initialize the host's pins, set up the serial port and reset:
|
|
SensorHardwareSetup(I2C_ADDRESS);
|
|
|
|
if (createWifiNetwork) {
|
|
// The host generates its own WiFi network ("Access Point") with
|
|
// a chosen static IP address
|
|
if (!createWiFiAP(SSID, password, theIP)) {
|
|
Serial.println("Failed to create access point.");
|
|
while (true) {
|
|
yield();
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// The host connects to an existing Wifi network
|
|
|
|
// Wait for the serial port to start because the user must be able
|
|
// to see the printed IP address in the serial monitor
|
|
while (!Serial) {
|
|
yield();
|
|
}
|
|
|
|
// Attempt to connect to the Wifi network and obtain the IP
|
|
// address. Because the address is not known before this point,
|
|
// a serial monitor must be used to display it to the user.
|
|
connectToWiFi(SSID, password);
|
|
theIP = WiFi.localIP();
|
|
}
|
|
|
|
// Print the IP address: use this address in a browser to view the
|
|
// generated web page
|
|
Serial.print("View your page at http://");
|
|
Serial.println(theIP);
|
|
|
|
// Start the web server
|
|
server.begin();
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
|
// Select how often to auto-refresh the web page. This should be done at
|
|
// least as often as new data are obtained. A more frequent refresh is
|
|
// best for long cycle periods because the page refresh is not
|
|
// synchronized with the cycle. Users can also manually refresh the page.
|
|
if (cycle_period == CYCLE_PERIOD_3_S) {
|
|
refreshPeriodSeconds = 3;
|
|
}
|
|
else if (cycle_period == CYCLE_PERIOD_100_S) {
|
|
refreshPeriodSeconds = 30;
|
|
}
|
|
else { // CYCLE_PERIOD_300_S
|
|
refreshPeriodSeconds = 50;
|
|
}
|
|
|
|
// Apply the chosen settings to the Metriful board
|
|
uint8_t particleSensor = PARTICLE_SENSOR;
|
|
TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1);
|
|
TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1);
|
|
ready_assertion_event = false;
|
|
TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0);
|
|
}
|
|
|
|
void loop() {
|
|
|
|
// While waiting for the next data release, respond to client requests
|
|
// by serving the web page with the last available data. Initially the
|
|
// data will be all zero (until the first data readout has completed).
|
|
while (!ready_assertion_event) {
|
|
handleClientRequests();
|
|
yield();
|
|
}
|
|
ready_assertion_event = false;
|
|
|
|
// new data are now ready
|
|
|
|
/* Read data from the MS430 into the data structs.
|
|
For each category of data (air, sound, etc.) a pointer to the data struct is
|
|
passed to the ReceiveI2C() function. The received byte sequence fills the data
|
|
struct in the correct order so that each field within the struct receives
|
|
the value of an environmental quantity (temperature, sound level, etc.)
|
|
*/
|
|
|
|
// Air data
|
|
// Choose output temperature unit (C or F) in Metriful_sensor.h
|
|
ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
|
|
|
|
/* Air quality data
|
|
The initial self-calibration of the air quality data may take several
|
|
minutes to complete. During this time the accuracy parameter is zero
|
|
and the data values are not valid.
|
|
*/
|
|
ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
|
|
|
|
// Light data
|
|
ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
|
|
|
|
// Sound data
|
|
ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
|
|
|
|
/* Particle data
|
|
This requires the connection of a particulate sensor (invalid
|
|
values will be obtained if this sensor is not present).
|
|
Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h
|
|
Also note that, due to the low pass filtering used, the
|
|
particle data become valid after an initial initialization
|
|
period of approximately one minute.
|
|
*/
|
|
if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
|
|
ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
|
|
}
|
|
|
|
// Create the web page ready for client requests
|
|
assembleWebPage();
|
|
|
|
// Check WiFi is still connected
|
|
if (!createWifiNetwork) {
|
|
uint8_t wifiStatus = WiFi.status();
|
|
if (wifiStatus != WL_CONNECTED) {
|
|
// There is a problem with the WiFi connection: attempt to reconnect.
|
|
Serial.print("Wifi status: ");
|
|
Serial.println(interpret_WiFi_status(wifiStatus));
|
|
connectToWiFi(SSID, password);
|
|
theIP = WiFi.localIP();
|
|
Serial.print("View your page at http://");
|
|
Serial.println(theIP);
|
|
ready_assertion_event = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void handleClientRequests(void) {
|
|
// Check for incoming client requests
|
|
WiFiClient client = server.available();
|
|
if (client) {
|
|
bool blankLine = false;
|
|
while (client.connected()) {
|
|
if (client.available()) {
|
|
char c = client.read();
|
|
if (c == '\n') {
|
|
// Two consecutive newline characters indicates the end of the client HTTP request
|
|
if (blankLine) {
|
|
// Send the page as a response
|
|
client.print(pageBuffer);
|
|
break;
|
|
}
|
|
else {
|
|
blankLine = true;
|
|
}
|
|
}
|
|
else if (c != '\r') {
|
|
// Carriage return (\r) is disregarded for blank line detection
|
|
blankLine = false;
|
|
}
|
|
}
|
|
}
|
|
delay(10);
|
|
// Close the connection:
|
|
client.stop();
|
|
}
|
|
}
|
|
|
|
// Create a simple text web page showing the environment data in
|
|
// separate category tables, using HTML and CSS
|
|
void assembleWebPage(void) {
|
|
sprintf(pageBuffer,"HTTP/1.1 200 OK\r\n"
|
|
"Content-type: text/html\r\n"
|
|
"Connection: close\r\n"
|
|
"Refresh: %u\r\n\r\n",refreshPeriodSeconds);
|
|
|
|
strcat(pageBuffer,"<!DOCTYPE HTML><html><head>"
|
|
"<meta charset='UTF-8'>"
|
|
"<title>Metriful Sensor Demo</title>"
|
|
"<style>"
|
|
"h1{font-size: 3vw;}"
|
|
"h2{font-size: 2vw; margin-top: 4vw;}"
|
|
"a{padding: 1vw; font-size: 2vw;}"
|
|
"table,th,td{font-size: 2vw;}"
|
|
"body{padding: 0vw 2vw;}"
|
|
"th,td{padding: 0.05vw 1vw; text-align: left;}"
|
|
"#v1{text-align: right; width: 10vw;}"
|
|
"#v2{text-align: right; width: 13vw;}"
|
|
"#v3{text-align: right; width: 10vw;}"
|
|
"#v4{text-align: right; width: 10vw;}"
|
|
"#v5{text-align: right; width: 11vw;}"
|
|
"</style></head>"
|
|
"<body><h1>Indoor Environment Data</h1>");
|
|
|
|
//////////////////////////////////////
|
|
|
|
strcat(pageBuffer,"<p><h2>Air Data</h2><table>");
|
|
|
|
uint8_t T_intPart = 0;
|
|
uint8_t T_fractionalPart = 0;
|
|
bool isPositive = true;
|
|
const char * unit = getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive);
|
|
sprintf(lineBuffer,"<tr><td>Temperature</td><td id='v1'>%s%u.%u</td><td>%s</td></tr>",
|
|
isPositive?"":"-", T_intPart, T_fractionalPart, unit);
|
|
strcat(pageBuffer,lineBuffer);
|
|
|
|
sprintf(lineBuffer,"<tr><td>Pressure</td><td id='v1'>%" PRIu32 "</td><td>Pa</td></tr>", airData.P_Pa);
|
|
strcat(pageBuffer,lineBuffer);
|
|
|
|
sprintf(lineBuffer,"<tr><td>Humidity</td><td id='v1'>%u.%u</td><td>%%</td></tr>",
|
|
airData.H_pc_int, airData.H_pc_fr_1dp);
|
|
strcat(pageBuffer,lineBuffer);
|
|
|
|
sprintf(lineBuffer,"<tr><td>Gas Sensor Resistance</td>"
|
|
"<td id='v1'>%" PRIu32 "</td><td>" OHM_SYMBOL "</td></tr></table></p>",
|
|
airData.G_ohm);
|
|
strcat(pageBuffer,lineBuffer);
|
|
|
|
//////////////////////////////////////
|
|
|
|
strcat(pageBuffer,"<p><h2>Air Quality Data</h2>");
|
|
|
|
if (airQualityData.AQI_accuracy == 0) {
|
|
sprintf(lineBuffer,"<a>%s</a></p>",interpret_AQI_accuracy(airQualityData.AQI_accuracy));
|
|
strcat(pageBuffer,lineBuffer);
|
|
}
|
|
else {
|
|
sprintf(lineBuffer,"<table><tr><td>Air Quality Index</td><td id='v2'>%u.%u</td><td></td></tr>",
|
|
airQualityData.AQI_int, airQualityData.AQI_fr_1dp);
|
|
strcat(pageBuffer,lineBuffer);
|
|
|
|
sprintf(lineBuffer,"<tr><td>Air Quality Summary</td><td id='v2'>%s</td><td></td></tr>",
|
|
interpret_AQI_value(airQualityData.AQI_int));
|
|
strcat(pageBuffer,lineBuffer);
|
|
|
|
sprintf(lineBuffer,"<tr><td>Estimated CO" SUBSCRIPT_2 "</td><td id='v2'>%u.%u</td><td>ppm</td></tr>",
|
|
airQualityData.CO2e_int, airQualityData.CO2e_fr_1dp);
|
|
strcat(pageBuffer,lineBuffer);
|
|
|
|
sprintf(lineBuffer,"<tr><td>Equivalent Breath VOC</td>"
|
|
"<td id='v2'>%u.%02u</td><td>ppm</td></tr></table></p>",
|
|
airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp);
|
|
strcat(pageBuffer,lineBuffer);
|
|
}
|
|
|
|
//////////////////////////////////////
|
|
|
|
strcat(pageBuffer,"<p><h2>Sound Data</h2><table>");
|
|
|
|
sprintf(lineBuffer,"<tr><td>A-weighted Sound Pressure Level</td>"
|
|
"<td id='v3'>%u.%u</td><td>dBA</td></tr>",
|
|
soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp);
|
|
strcat(pageBuffer,lineBuffer);
|
|
|
|
for (uint8_t i=0; i<SOUND_FREQ_BANDS; i++) {
|
|
sprintf(lineBuffer,"<tr><td>Frequency Band %u (%u Hz) SPL</td>"
|
|
"<td id='v3'>%u.%u</td><td>dB</td></tr>",
|
|
i+1, sound_band_mids_Hz[i], soundData.SPL_bands_dB_int[i], soundData.SPL_bands_dB_fr_1dp[i]);
|
|
strcat(pageBuffer,lineBuffer);
|
|
}
|
|
|
|
sprintf(lineBuffer,"<tr><td>Peak Sound Amplitude</td>"
|
|
"<td id='v3'>%u.%02u</td><td>mPa</td></tr></table></p>",
|
|
soundData.peak_amp_mPa_int, soundData.peak_amp_mPa_fr_2dp);
|
|
strcat(pageBuffer,lineBuffer);
|
|
|
|
//////////////////////////////////////
|
|
|
|
strcat(pageBuffer,"<p><h2>Light Data</h2><table>");
|
|
|
|
sprintf(lineBuffer,"<tr><td>Illuminance</td><td id='v4'>%u.%02u</td><td>lux</td></tr>",
|
|
lightData.illum_lux_int, lightData.illum_lux_fr_2dp);
|
|
strcat(pageBuffer,lineBuffer);
|
|
|
|
sprintf(lineBuffer,"<tr><td>White Light Level</td><td id='v4'>%u</td><td></td></tr>"
|
|
"</table></p>", lightData.white);
|
|
strcat(pageBuffer,lineBuffer);
|
|
|
|
//////////////////////////////////////
|
|
|
|
if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
|
|
strcat(pageBuffer,"<p><h2>Air Particulate Data</h2><table>");
|
|
|
|
sprintf(lineBuffer,"<tr><td>Sensor Duty Cycle</td><td id='v5'>%u.%02u</td><td>%%</td></tr>",
|
|
particleData.duty_cycle_pc_int, particleData.duty_cycle_pc_fr_2dp);
|
|
strcat(pageBuffer,lineBuffer);
|
|
|
|
char unitsBuffer[7] = {0};
|
|
if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42) {
|
|
strcpy(unitsBuffer,"ppL");
|
|
}
|
|
else if (PARTICLE_SENSOR == PARTICLE_SENSOR_SDS011) {
|
|
strcpy(unitsBuffer,SDS011_UNIT_SYMBOL);
|
|
}
|
|
else {
|
|
strcpy(unitsBuffer,"(?)");
|
|
}
|
|
sprintf(lineBuffer,"<tr><td>Particle Concentration</td>"
|
|
"<td id='v5'>%u.%02u</td><td>%s</td></tr></table></p>",
|
|
particleData.concentration_int, particleData.concentration_fr_2dp, unitsBuffer);
|
|
strcat(pageBuffer,lineBuffer);
|
|
}
|
|
|
|
//////////////////////////////////////
|
|
|
|
strcat(pageBuffer,"</body></html>");
|
|
}
|