#include <M5Unified.h>
#include <M5GFX.h>
#include <bsec2.h>
namespace {
M5Canvas canvas(&M5.Display);
Bsec2 envSensor;
bool sensorReady = false;
struct SensorValues {
int64_t timestampMs = 0;
String iaq = "--";
String accuracy = "--";
String temperature = "--";
String pressure = "--";
String humidity = "--";
String gas = "--";
String stabilized = "--";
String runIn = "--";
};
SensorValues values;
constexpr uint16_t kBackground = TFT_BLACK;
constexpr uint16_t kHeader = TFT_DARKCYAN;
constexpr uint16_t kLabel = TFT_LIGHTGREY;
constexpr uint16_t kValue = TFT_WHITE;
constexpr int32_t kMargin = 10;
constexpr int32_t kFirstRowY = 51;
constexpr int32_t kRowHeight = 21;
void showStatusPage(const char* heading, const String& message,
uint16_t accent = kHeader) {
canvas.fillSprite(kBackground);
canvas.fillRect(0, 0, canvas.width(), 34, accent);
canvas.setFont(&fonts::FreeMonoBold9pt7b);
canvas.setTextDatum(textdatum_t::middle_left);
canvas.setTextColor(TFT_WHITE, accent);
canvas.drawString(heading, kMargin, 17);
canvas.setTextDatum(textdatum_t::top_left);
canvas.setTextColor(kValue, kBackground);
canvas.setCursor(kMargin, 52);
canvas.setTextWrap(true);
canvas.println(message);
canvas.pushSprite(0, 0);
}
void drawRow(int32_t row, const char* label, const String& value,
uint16_t valueColor = kValue) {
const int32_t y = kFirstRowY + row * kRowHeight;
canvas.setTextDatum(textdatum_t::top_left);
canvas.setTextColor(kLabel, kBackground);
canvas.drawString(label, kMargin, y);
canvas.setTextDatum(textdatum_t::top_right);
canvas.setTextColor(valueColor, kBackground);
canvas.drawString(value, canvas.width() - kMargin, y);
}
void drawDashboard() {
canvas.fillSprite(kBackground);
canvas.setTextWrap(false);
canvas.fillRect(0, 0, canvas.width(), 31, kHeader);
canvas.setFont(&fonts::FreeMonoBold9pt7b);
canvas.setTextDatum(textdatum_t::middle_left);
canvas.setTextColor(TFT_WHITE, kHeader);
canvas.drawString("Unit ENV-Pro", kMargin, 15);
canvas.setTextDatum(textdatum_t::top_left);
canvas.setTextColor(TFT_SILVER, kBackground);
canvas.setCursor(kMargin, 35);
canvas.printf("Timestamp: %lld ms",
static_cast<long long>(values.timestampMs));
canvas.setFont(&fonts::FreeMonoBold9pt7b);
drawRow(0, "IAQ", values.iaq);
drawRow(1, "IAQ accuracy", values.accuracy);
drawRow(2, "Temperature", values.temperature);
drawRow(3, "Pressure", values.pressure);
drawRow(4, "Humidity", values.humidity);
drawRow(5, "Gas resistance", values.gas);
drawRow(6, "Stabilization", values.stabilized,
values.stabilized == "Ready" ? TFT_GREEN : TFT_YELLOW);
drawRow(7, "Run-in", values.runIn,
values.runIn == "Complete" ? TFT_GREEN : TFT_YELLOW);
canvas.pushSprite(0, 0);
}
bool hasFatalStatus(const Bsec2& bsec) {
return bsec.status < BSEC_OK || bsec.sensor.status < BME68X_OK;
}
void checkBsecStatus(const Bsec2& bsec) {
String message;
uint16_t accent = TFT_ORANGE;
if (bsec.status < BSEC_OK) {
message += "BSEC error: " + String(bsec.status) + "\n";
accent = TFT_RED;
} else if (bsec.status > BSEC_OK) {
message += "BSEC warning: " + String(bsec.status) + "\n";
}
if (bsec.sensor.status < BME68X_OK) {
message += "BME68X error: " + String(bsec.sensor.status) + "\n";
accent = TFT_RED;
} else if (bsec.sensor.status > BME68X_OK) {
message += "BME68X warning: " + String(bsec.sensor.status) + "\n";
}
if (message.isEmpty()) {
message = "The sensor operation did not complete.";
accent = TFT_RED;
}
showStatusPage(accent == TFT_RED ? "Sensor error" : "Sensor warning",
message, accent);
}
void newDataCallback(const bme68xData data, const bsecOutputs outputs,
Bsec2 bsec) {
(void)data;
(void)bsec;
if (outputs.nOutputs == 0) {
return;
}
values.timestampMs = outputs.output[0].time_stamp / INT64_C(1000000);
for (uint8_t i = 0; i < outputs.nOutputs; ++i) {
const bsecData& output = outputs.output[i];
switch (output.sensor_id) {
case BSEC_OUTPUT_IAQ:
values.iaq = String(output.signal, 2);
values.accuracy = String(output.accuracy) + " / 3";
break;
case BSEC_OUTPUT_RAW_TEMPERATURE:
values.temperature = String(output.signal, 2) + " C";
break;
case BSEC_OUTPUT_RAW_PRESSURE:
values.pressure = String(output.signal, 2) + " Pa";
break;
case BSEC_OUTPUT_RAW_HUMIDITY:
values.humidity = String(output.signal, 2) + " %";
break;
case BSEC_OUTPUT_RAW_GAS:
values.gas = String(output.signal, 0) + " Ohm";
break;
case BSEC_OUTPUT_STABILIZATION_STATUS:
values.stabilized = output.signal > 0.0f ? "Ready" : "Warming up";
break;
case BSEC_OUTPUT_RUN_IN_STATUS:
values.runIn = output.signal > 0.0f ? "Complete" : "Running";
break;
default:
break;
}
}
drawDashboard();
}
}
void setup() {
auto config = M5.config();
M5.begin(config);
canvas.setColorDepth(8);
if (canvas.createSprite(M5.Display.width(), M5.Display.height()) == nullptr) {
M5.Display.fillScreen(TFT_BLACK);
M5.Display.setTextColor(TFT_RED, TFT_BLACK);
M5.Display.setCursor(10, 10);
M5.Display.println("M5Canvas allocation failed");
return;
}
showStatusPage("Unit ENV-Pro", "Initializing sensor...");
Wire.begin(21, 22);
if (!envSensor.begin(BME68X_I2C_ADDR_HIGH, Wire)) {
checkBsecStatus(envSensor);
if (hasFatalStatus(envSensor)) {
return;
}
}
bsecSensor sensorList[] = {
BSEC_OUTPUT_IAQ,
BSEC_OUTPUT_RAW_TEMPERATURE,
BSEC_OUTPUT_RAW_PRESSURE,
BSEC_OUTPUT_RAW_HUMIDITY,
BSEC_OUTPUT_RAW_GAS,
BSEC_OUTPUT_STABILIZATION_STATUS,
BSEC_OUTPUT_RUN_IN_STATUS,
};
if (!envSensor.updateSubscription(sensorList, ARRAY_LEN(sensorList),
BSEC_SAMPLE_RATE_LP)) {
checkBsecStatus(envSensor);
if (hasFatalStatus(envSensor)) {
return;
}
}
envSensor.attachCallback(newDataCallback);
sensorReady = true;
showStatusPage("Unit ENV-Pro", "Waiting for sensor data...");
}
void loop() {
M5.update();
if (sensorReady && !envSensor.run()) {
checkBsecStatus(envSensor);
if (hasFatalStatus(envSensor)) {
sensorReady = false;
}
}
}