制造商零件编号 ABX00087
ARDUINO UNO R4 WIFI
Arduino
License: See Original Project Air Quality Carbon Dioxide (CO2) Environmental Gas Arduino
As a Home Assistant user, one of the things I like to track in my home is air quality. I wrote previously about this, using the Sensirion SCD30 carbon dioxide sensor to monitor CO2 levels and automate ventilation. However, CO2 is only one aspect of air quality – there are other factors to track as well, such as dust (aka particulate), volatile organic compounds (VOCs), and more. Recently, Sensirion released their new SEN6x series of sensors, which combines sensors for all of this, and more, into a single unit, perfect for home automation!
The SEN6x series of sensors communicate via I2C, making it easy to work with. While most of my connected homebrew sensor devices are created using ESPHome, as of this time, this sensor is not yet officially supported, so I decided to create my own using Arduino, MQTT, and Sensirion’s Arduino library for the SEN66 sensor.
While working on this project, I came across the popular HALO project, which is a smart air quality monitor for 3D printing areas. Inspired by this project, I realized I could achieve similar results with a simpler BOM (bill of materials) by adding the DFRobot MiCS-4514 Gravity board to my build. This would be perfect for my home lab, where I work on my electronics projects, since they often involve soldering, 3D printing, or the use of adhesives or other chemicals. Plus, with the boards and sensor selected, this project is completely solder-free.
I also found a great resource for adding Home Assistant discovery to DIY MQTT devices – this allows the server to automatically discover the device and set up the device in Home Assistant, which is a great alternative to having to manually set up the device and all its entities in Home Assistant’s configuration.YAML file!
Follow along to create your own smart air quality monitor designed to oversee your 3D printer workspace. This project measures particles, gases, and overall air quality, with the data published via MQTT to your Home Assistant server using native auto-discovery. This feature allows you to monitor your environment without additional integration steps, providing opportunities to automate your air quality control.
Features
Uses WiFi and MQTT to communicate with Home Assistant
Tracks PM1.0, PM2.5, PM4.0, PM10, Temperature, Humidity, VOC, NOx, CO2, and multiple gas types
Automatically integrates into Home Assistant (HA) via MQTT discovery
Built-in watchdog timer to ensure system reliability
Parts List
Male-to-male jumper wires or male headers (for adapting Gravity cable to Uno header)
Wiring Instructions
1. SEN66 Sensor:
Using the ACES 51452-006H0H0-001 to JST SHR-04V-S cable included in the SEK-SEN66 eval kit, plug the cable into the sensor and the Uno R4 WiFi's onboard Qwiic port.
2. MiCS-4514 Sensor (SEN0377):
Connect the included Gravity cable to the SEN0377 board.
Use male-to-male jumper wires or male headers to connect to the Uno's I2C headers:
SDA to SDA
SCL to SCL
VCC to 5V
GND to GND
Software Setup
1. Libraries Required (Install via Library Manager or GitHub):
SensirionI2cSen66: https://github.com/Sensirion/arduino-i2c-sen66
DFRobot_MICS: https://github.com/Sensirion/arduino-i2c-sen66
MQTT by 256dpi: https://github.com/256dpi/arduino-mqtt
ArduinoJson: https://github.com/bblanchon/ArduinoJson
WDT (Watchdog): This is included in the Arduino board package for the Arduino Uno R4 boards
2. Create arduino_secrets.h File
This file holds your WiFi and MQTT credentials securely. Place it in the same directory as your .ino file.
#define SECRET_SSID "YourWiFiSSID"
#define SECRET_WIFI_PASSWORD "YourWiFiPassword"
#define SECRET_MQTT_BROKER_ADDRESS "192.168.1.X" // Your HA IP if running Mosquitto on your HA server. Note: Local domain names (e.g., "Computer.local" on OSX) are not supported by Arduino. You need to set the IP address directly.
#define SECRET_MQTT_USERNAME "mqtt_user"
#define SECRET_MQTT_PASSWORD "mqtt_pass"
#define SECRET_MQTT_CLIENT_ID "YourDeviceName"
3. MQTT Broker Setup in HA
Ensure the Mosquitto broker is installed via the Home Assistant add-ons store, properly configured, and running. No manual MQTT sensor configuration is needed due to the project’s use of Home Assistant auto-discovery.
Uploading and Running
1. Plug your Uno R4 WiFi into your PC.
2. Open the project in the Arduino IDE.
3. Ensure the correct board (Uno R4 WiFi) and port are selected.
4. Upload the code found at the end of this project.
5. Open Serial Monitor for debug output (baud: 115200). If you don’t see anything in the Serial Monitor, ensure you have enabled debugging in the configuration section of the code.
Home Assistant Integration
Once the device is online and MQTT is working, you should see sensors appear in Home Assistant automatically. No YAML editing or MQTT sensor creation is required.
Calibration Tips (MiCS Sensor)
The MiCS-4514 requires a 3-minute warm-up on power-up. During this warmup period, you will see the Arduino’s onboard user LED blink on and off once a second.
Run the board for 24–48 hours for the best baseline calibration.
For environments with fluctuating air quality, allow longer stabilization periods before trusting readings.
Troubleshooting
WiFi Issues:
Ensure SSID/password are correct.
Retry limit is built-in; check debug output via Serial Monitor.
MQTT Issues:
Confirm MQTT broker is reachable from Uno (check IP/firewall).
Use MQTT Explorer or similar to verify topic publishing.
Sensor Issues:
Ensure correct I2C wiring.
Check address conflicts on the I2C bus.
HA Discovery Fails:
Restart MQTT and HA server.
Ensure auto_discovery flag in the code is set to true.
Code Concepts Overview
State Machine
The code uses a clean finite state machine (FSM) for reliability. Each critical step (WiFi connection, MQTT setup, sensor initialization, discovery, and normal operation) is managed sequentially. You can learn more about this approach to coding here: How to Program an Arduino Finite State Machine.
Watchdog Timer
A watchdog timer resets the board if any part of the FSM hangs. The Uno R4 has a max watchdog interval of ~5.5 seconds, so the refreshWatchdog() function is used in long loops and retries. Note that due to the time required for the board to make the initial connection to the MQTT broker and the library’s blocking function during this process, the watchdog timer is not started until after the WiFi and MQTT connections are made.
Debug Macro
The code uses a macro for serial debug messages that can easily be disabled once debugging is complete, allowing for more efficient, streamlined code execution. You can learn more about this here: How to Streamline Your Arduino Code: Avoid Using Serial Print Commands.
Next Steps
While this project is functionally complete, I still plan to utilize the Arduino board’s LED matrix for a local snapshot view of air quality. Additionally, I will be designing and printing an enclosure for this project.
Final Thoughts
This project offers a flexible and HA-friendly, extensive air quality monitor that can integrate with your home automation setup.
Project Code:
#include <Arduino.h>
#include <SensirionI2cSen66.h> // For SEN66 air quality sensor: https://github.com/Sensirion/arduino-i2c-sen66
#include <Wire.h> // I2C communication
#include <MQTT.h> // Gilberto Conti's MQTT Library: https://github.com/256dpi/arduino-mqtt
#include "arduino_secrets.h" // WiFi/MQTT credentials stored separately
#include <ArduinoJson.h> // JSON handling (for Home Assistant discovery)
#include <WiFi.h> // WiFi Library (auto-selects appropriate functions for the board being used)
#include "DFRobot_MICS.h" // DFRobot MiCS gas sensor support Library: https://github.com/DFRobot/DFRobot_MICS
#include <WDT.h> // Watchdog Timer library
//---------------- Configuration ----------------
bool auto_discovery = true; // Enable/disable Home Assistant auto-discovery (Courtesy of https://resinchemtech.blogspot.com/2023/12/mqtt-auto-discovery.html)
const long interval = 5000; // Sensor polling interval (milliseconds)
#define DEBUG 0 // Enable debug output (set to 1 to enable, 0 to disable)
//-----------------------------------------------
// Device state machine
enum DeviceState {
STATE_INIT,
STATE_WIFI_CONNECT,
STATE_MQTT_CONNECT,
STATE_WDT_START,
STATE_DISCOVERY,
STATE_SENSOR_SETUP,
STATE_RUNNING,
STATE_ERROR
};
DeviceState currentState = STATE_INIT;
//------------- Debug Macros ---------------
#if DEBUG
#define outputDebug(x) Serial.print(x)
#define outputDebugLine(x) Serial.println(x)
#define outputDebugF(...) { char debugbuf[80]; snprintf(_debug_buf, sizeof(_debug_buf), __VA_ARGS__); Serial.print(_debug_buf); }
#define outputDebugLineF(...) { char debugbuf[80]; snprintf(_debug_buf, sizeof(_debug_buf), __VA_ARGS__); Serial.println(_debug_buf); }
#else
#define outputDebug(x)
#define outputDebugLine(x)
#define outputDebugF(...)
#define outputDebugLineF(...)
#endif
// Constants
#define NO_ERROR 0
#define MAX_RETRY_ATTEMPTS 5
#define CALIBRATION_TIME 3 // MiCS-4514 calibration time in minutes (default: 3 minutes)
// Sensor validation ranges
#define PM_MIN 0.0
#define PM_MAX 1000.0
#define HUMIDITY_MIN 0.0
#define HUMIDITY_MAX 100.0
#define TEMP_MIN -10.0
#define TEMP_MAX 40.0
#define VOC_MIN 0.0
#define VOC_MAX 500.0
#define NOX_MIN 0.0
#define NOX_MAX 500.0
#define CO2_MIN 0
#define CO2_MAX 40000
#define CO_MIN 0.0
#define CO_MAX 1000.0
#define CH4_MIN 0.0
#define CH4_MAX 25000.0
#define C2H5OH_MIN 0.0
#define C2H5OH_MAX 500.0
#define H2_MIN 0.0
#define H2_MAX 1000.0
#define NH3_MIN 0.0
#define NH3_MAX 500.0
#define NO2_MIN 0.0
#define NO2_MAX 10.0
// MiCS gas sensor I2C address
#define Mics_I2C_ADDRESS MICS_ADDRESS_0
DFRobot_MICS_I2C mics(&Wire, Mics_I2C_ADDRESS);
// Global instances
WiFiClient wifiClient;
MQTTClient client(512); // MQTT buffer size
SensirionI2cSen66 sensor; // SEN66 sensor instance
// Sensor metadata arrays
const char* sensorName[] = {
"PM1.0", "PM2.5", "PM4.0", "PM10", "Humidity", "Temperature",
"VOC Index", "NOx Index", "Methane", "Ethanol", "Hydrogen",
"Ammonia", "Carbon Monoxide", "Nitrogen Dioxide", "CO2"
};
const char* deviceClass[] = { // https://www.home-assistant.io/integrations/sensor#device-class
"pm1", "pm25", "", "pm10", "humidity", "temperature",
"", "", "", "", "",
"", "carbon_monoxide", "nitrogen_dioxide", "carbon_dioxide"
};
const char* UofM[] = {
"µg/m³", "µg/m³", "µg/m³", "µg/m³", "%", "°C",
"", "", "ppm", "ppm", "ppm",
"ppm", "ppm", "ppm", "ppm"
};
const float minRanges[] = {
PM_MIN, PM_MIN, PM_MIN, PM_MIN, HUMIDITY_MIN, TEMP_MIN,
VOC_MIN, NOX_MIN, CH4_MIN, C2H5OH_MIN, H2_MIN,
NH3_MIN, CO_MIN, NO2_MIN
};
const float maxRanges[] = {
PM_MAX, PM_MAX, PM_MAX, PM_MAX, HUMIDITY_MAX, TEMP_MAX,
VOC_MAX, NOX_MAX, CH4_MAX, C2H5OH_MAX, H2_MAX,
NH3_MAX, CO_MAX, NO2_MAX
};
// MiCS gas sensor codes
const uint8_t MiCSgases[] = {0x01, 0x02, 0x03, 0x06, 0x08, 0x0A};
// Time and state tracking
unsigned long previousMillis = 0;
static char devUniqueID[30]; // Unique MQTT discovery device ID
static int16_t error; // Sensor error tracking
uint8_t errorCount = 0;
byte mac[6]; // MAC address for unique ID
const long wdtInterval = 5592; // Watchdog interval (Uno R4 max is 5592)
bool isWatchdogActive = false; // Flag for watchdog status
// Sensor value buffers
float SensorVal[14]; // All values except CO2
uint16_t CO2sensorVal; // CO2 requires uint16_t
// Credentials (stored separately)
const char ssid[] = SECRET_SSID;
const char pass[] = SECRET_WIFI_PASSWORD;
const char broker[] = SECRET_MQTT_BROKER_ADDRESS;
const char mqttClientId[] = SECRET_MQTT_CLIENT_ID;
const char mqttUsername[] = SECRET_MQTT_USERNAME;
const char mqttPassword[] = SECRET_MQTT_PASSWORD;
//--------------- WiFi Setup ---------------
/**
* Connect to WiFi with retries and backoff
* @return true if connected, false otherwise
*/
bool connectWiFi() {
outputDebugLine("Connecting to WiFi...");
WiFi.disconnect(); // Disconnect if currently connected to avoid hanging
delay(100);
int retryCount = 0;
while (WiFi.status() != WL_CONNECTED && retryCount < MAX_RETRY_ATTEMPTS) {
WiFi.begin(ssid, pass);
unsigned long connectTimeout = millis() + 10000; // Wait up to 10 seconds for connection
while (WiFi.status() != WL_CONNECTED && millis() < connectTimeout) {
outputDebug(".");
delay(500);
refreshWatchdog();
}
if (WiFi.status() != WL_CONNECTED) {
retryCount++;
int backoffTime = min(1000 * retryCount, 5000); // Exponential backoff capped at 5 seconds
outputDebugF("WiFi connection attempt %d failed. Retrying in %d ms...\n", retryCount, backoffTime);
delay(backoffTime);
refreshWatchdog();
}
}
if (WiFi.status() == WL_CONNECTED) {
outputDebugF("WiFi connected. IP: %s, RSSI: %d dBm\n",
WiFi.localIP().toString().c_str(), WiFi.RSSI());
WiFi.macAddress(mac);
return true;
} else {
outputDebugLine("WiFi connection failed after multiple attempts.");
return false;
}
}
//--------------- MQTT Setup ---------------
/**
* Connect to the MQTT broker with retry
* @return true if connected, false otherwise
*/
bool connectMQTT() {
client.setKeepAlive(60); // Ping the broker every 60 seconds
client.onMessage(messageReceived);
client.begin(broker, wifiClient); // Initialize the MQTT client with the WiFi connection
outputDebug("Connecting to MQTT...");
int retryCount = 0;
refreshWatchdog();
while (!client.connect(mqttClientId, mqttUsername, mqttPassword) &&
retryCount < MAX_RETRY_ATTEMPTS) {
outputDebug(".");
retryCount++;
int backoffTime = min(1000 * retryCount, 5000); // Exponential backoff capped at 5 seconds
delay(backoffTime);
refreshWatchdog();
}
if (client.connected()) {
outputDebugLine("MQTT connected.");
return true;
} else {
outputDebugLine("MQTT connection failed after multiple attempts.");
return false;
}
}
//--------- Connection Management ----------
/**
* Checks connection status of WiFi and MQTT
* and attempts to reconnect if needed
* @return true if connected, false otherwise
*/
bool checkConnections() {
static unsigned long lastReconnectAttempt = 0;
bool reconnected = false;
// Check WiFi connection
if (WiFi.status() != WL_CONNECTED || WiFi.RSSI() < -80) {
outputDebugLine("WiFi connection lost or poor signal. Reconnecting...");
if (connectWiFi()) {
reconnected = true;
} else {
return false;
}
}
// Check MQTT connection
if (!client.connected()) {
outputDebugLine("MQTT connection lost. Reconnecting...");
if (connectMQTT()) {
reconnected = true;
} else {
return false;
}
}
// If we reconnected, reinitiate discovery
if (reconnected) {
haDiscovery();
}
client.loop(); // Process any incoming messages
return true;
}
//--------- MQTT Message Callback ----------
/**
* Handles incoming MQTT messages
*/
void messageReceived(String &topic, String &payload) {
outputDebugF("Incoming: %s - %s\n", topic.c_str(), payload.c_str());
// Parse commands if needed
if (topic.endsWith("/command")) {
if (payload == "reset") {
outputDebugLine("Reset command received. Restarting device...");
delay(100);
NVIC_SystemReset();
}
}
}
//----------- Unique ID Creation -----------
/**
* Generates the unique device ID for MQTT discovery topics
*/
void createDiscoveryUniqueID() {
strcpy(devUniqueID, mqttClientId);
for (int i = 0; i < 3; i++) {
sprintf(&devUniqueID[strlen(devUniqueID)], "%02X", mac[5 - i]);
}
outputDebugF("Generated Unique ID: %s\n", devUniqueID);
}
//------- Home Assistant Discovery ---------
/**
* Publishes MQTT discovery config payloads for Home Assistant auto-discovery
*/
void haDiscovery() {
char disc_topic[128];
char buffer[512];
for (int i = 0; i < 15; i++) {
snprintf(disc_topic, sizeof(disc_topic), "homeassistant/sensor/%s%d/config", devUniqueID, i);
if (auto_discovery) {
outputDebugF("Adding %s Sensor\n", sensorName[i]);
// Use StaticJsonDocument instead of DynamicJsonDocument to avoid heap fragmentation
StaticJsonDocument<512> doc;
doc["name"] = sensorName[i];
doc["unique_id"] = String(devUniqueID) + String(i);
if (strlen(deviceClass[i]) > 0) {
doc["device_class"] = deviceClass[i];
}
// State topic
char stateTopic[80];
snprintf(stateTopic, sizeof(stateTopic), "%s/%s", devUniqueID, sensorName[i]);
doc["state_topic"] = stateTopic;
doc["unit_of_measurement"] = UofM[i];
// Add the device object
JsonObject device = doc.createNestedObject("device");
device["identifiers"] = devUniqueID;
device["name"] = mqttClientId;
device["manufacturer"] = "DigiKey";
device["model"] = "Arduino Uno R4 + SEN66";
// Serialize and publish
size_t len = serializeJson(doc, buffer);
if (!client.publish(disc_topic, buffer, len)) {
outputDebugF("Failed to publish discovery for %s\n", sensorName[i]);
}
} else {
client.publish(disc_topic, ""); // Clear discovery
}
delay(50); // Small delay between discovery messages
refreshWatchdog();
}
outputDebugLine("Device discovery complete.");
}
//------------ Sensor Reading ---------------
/**
* Reads air quality data from the SEN66 and MiCS sensors.
* @return true if all data successfully read and sensor
* readings are in correct range, false if an error occurred.
*/
bool readSensorData() {
bool success = true;
// Read SEN66 sensor values
error = sensor.readMeasuredValues(
SensorVal[0], SensorVal[1], SensorVal[2], SensorVal[3],
SensorVal[4], SensorVal[5], SensorVal[6], SensorVal[7],
CO2sensorVal
);
if (error != NO_ERROR) {
outputDebugLine("Error reading sensor values.");
success = false;
}
// Read MiCS-4514 sensor values
for (int i = 8; i < 14; i++) {
int j = (i-8);
SensorVal[i] = mics.getGasData(MiCSgases[j]);
}
// Validate Sesnor readings
for (int i = 0; i < 14; i++) {
if (SensorVal[i] < minRanges[i] || SensorVal[i] > maxRanges[i]) {
outputDebugF("Invalid %s reading: %.2f (outside range)\n", sensorName[i], SensorVal[i]);
success = false;
}
}
// Validate CO2 reading
if (CO2sensorVal < CO2_MIN || CO2sensorVal > CO2_MAX) {
outputDebugF("Invalid CO2 reading: %d (outside range)\n", CO2sensorVal);
success = false;
}
return success;
}
//---------- MQTT Publishing ---------------
/**
* Publishes sensor readings to MQTT topics
*/
void publishSensorData() {
char topic[80];
char valueStr[20];
// Publish float values (SEN66 + MiCS gases)
for (int i = 0; i < 14; i++) {
if (!isnan(SensorVal[i])) {
dtostrf(SensorVal[i], 1, 2, valueStr); // Format value string
snprintf(topic, sizeof(topic), "%s/%s", devUniqueID, sensorName[i]); // Format topic string
outputDebugF("%s: %s %s ", sensorName[i], valueStr, UofM[i]); // Debug output
if (!client.publish(topic, valueStr)) {
outputDebugF("Failed to publish %s\n", sensorName[i]);
}
}
delay(10); // Avoid flooding the broker
refreshWatchdog();
}
// Publish CO2 separately (uint16_t)
snprintf(topic, sizeof(topic), "%s/%s", devUniqueID, sensorName[14]);
snprintf(valueStr, sizeof(valueStr), "%d", CO2sensorVal);
outputDebugF("%s: %s %s\n", sensorName[14], valueStr, UofM[14]);
if (!client.publish(topic, valueStr)) {
outputDebugLine("Failed to publish CO2 data");
}
}
//--------- Watchdog Handling --------------
/**
* Initializes the watchdog timer
*/
void setupWatchdog() {
if (!isWatchdogActive) {
outputDebugLine("Starting watchdog...");
WDT.begin(wdtInterval);
isWatchdogActive = true;
}
}
/**
* Refreshes (resets) the watchdog timer
* to prevent unwanted device reset
*/
void refreshWatchdog() {
if (isWatchdogActive) {
WDT.refresh();
}
}
//-------- MiCS-4514 Initialization --------
/**
* Initializes the MiCS sensor
* @return true if successful, false if sensor fails to initialize
*/
bool initializeMiCS() {
int retryCount = 0;
// Attempt to connect to MiCS sensor
while (!mics.begin() && retryCount < MAX_RETRY_ATTEMPTS) {
outputDebugLine("NO MiCS-4514 detected!");
retryCount++;
delay(1000);
refreshWatchdog();
}
if (retryCount >= MAX_RETRY_ATTEMPTS) {
outputDebugLine("Failed to connect to MiCS-4514 after multiple attempts");
return false;
}
outputDebugLine("MiCS-4514 connected successfully!");
// Wake up the sensor if it's in sleep mode
uint8_t mode = mics.getPowerState();
if (mode == SLEEP_MODE) {
mics.wakeUpMode();
outputDebugLine("Wake up MiCS-4514 success!");
} else {
outputDebugLine("The MiCS-4514 is already awake");
}
/**
* Do not touch the sensor probe when preheating the sensor.
* Place the sensor in clean air.
* The default calibration time is 3 minutes.
*/
outputDebugLine("Please wait until the warm-up time is over!");
while(!mics.warmUpTime(CALIBRATION_TIME)){
// Perform warm-up with visual feedback
for (int minute = 1; minute <= CALIBRATION_TIME; minute++) {
outputDebugF("Warm-up: %d of %d minutes\n", minute, CALIBRATION_TIME);
// 60 second loop with LED blinks and WDT refreshes
for (int second = 0; second < 60; second++) {
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // Blink LED during warm-up
delay(500);
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // Blink LED during warm-up
delay(500);
refreshWatchdog();
}
}
}
digitalWrite(LED_BUILTIN, LOW); // Turn off LED after warm-up
return true;
}
//---------- SEN66 Initialization ----------
/**
* Initializes the SEN66 sensor
* @return true if successful, false if sensor fails to initialize
*/
bool initializeSEN66() {
Wire1.begin();
sensor.begin(Wire1, SEN66_I2C_ADDR_6B);
if (sensor.deviceReset() != NO_ERROR || sensor.startContinuousMeasurement() != NO_ERROR) {
outputDebugLine("SEN66 initialization failed.");
return false;
}
delay(100); // Give the sensor time to finish start up before taking any readings
refreshWatchdog();
outputDebugLine("SEN66 initialized successfully");
return true;
}
//------- State Machine Processing ---------
void processStateMachine() {
static unsigned long stateEnterTime = 0;
refreshWatchdog();
// Record time of state entry for timeout purposes
if (currentState != STATE_RUNNING) {
unsigned long now = millis();
if (stateEnterTime == 0) {
stateEnterTime = now;
} else if (now - stateEnterTime > 210000) { // 3.5 minute allowance for sensor initialization and connection
outputDebugLine("State timeout - restarting setup process");
currentState = STATE_INIT;
stateEnterTime = now;
}
}
// Process current state
switch (currentState) {
case STATE_INIT: {
outputDebugLine("State: INIT");
stateEnterTime = millis();
currentState = STATE_WIFI_CONNECT;
break;
}
case STATE_SENSOR_SETUP: { // Initialize sensors
outputDebugLine("State: SENSOR_SETUP");
if (!initializeSEN66() || !initializeMiCS()) {
outputDebugLine("Sensor initialization failed");
errorCount++;
if (errorCount > 3) {
currentState = STATE_ERROR;
}
delay(5000); // Wait before retry
refreshWatchdog();
} else {
errorCount = 0;
refreshWatchdog();
currentState = STATE_RUNNING;
stateEnterTime = 0; // Clear timeout when entering running state
}
break;
}
case STATE_WIFI_CONNECT: {
outputDebugLine("State: WIFI_CONNECT");
refreshWatchdog();
if (connectWiFi()) {
outputDebugLine("Wifi connection successful");
errorCount = 0;
currentState = STATE_MQTT_CONNECT;
} else {
outputDebugLine("WiFi connection failed");
errorCount++;
if (errorCount > 3) {
currentState = STATE_ERROR;
}
refreshWatchdog();
delay(1000); // Wait before retry
}
break;
}
case STATE_MQTT_CONNECT: {
outputDebugLine("State: MQTT_CONNECT");
createDiscoveryUniqueID();
if (connectMQTT()) {
errorCount = 0;
refreshWatchdog();
currentState = STATE_WDT_START;
} else {
outputDebugLine("MQTT connection failed");
errorCount++;
if (errorCount > 3) {
currentState = STATE_ERROR;
}
refreshWatchdog();
delay(1000); // Wait before retry
}
break;
}
case STATE_WDT_START: { // Initialize watchdog timer - this must be done after MQTT client connection due to library function being a long, blocking process
outputDebugLine("State: WDT START");
setupWatchdog();
currentState = STATE_DISCOVERY;
break;
}
case STATE_DISCOVERY: {
outputDebugLine("State: DISCOVERY");
haDiscovery();
errorCount = 0;
currentState = STATE_SENSOR_SETUP;
break;
}
case STATE_RUNNING: { // Main operational state
if (!checkConnections()) {
outputDebugLine("Connection check failed");
errorCount++;
if (errorCount > 3) {
currentState = STATE_WIFI_CONNECT;
stateEnterTime = millis();
}
delay(1000); // Wait before retry
refreshWatchdog();
break;
}
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
digitalWrite(LED_BUILTIN, HIGH); // Visual heartbeat
if (readSensorData()) {
publishSensorData();
errorCount = 0; // Reset error count on success
} else {
outputDebugLine("Sensor read failed");
errorCount++;
delay(500);
if (errorCount > 5) {
currentState = STATE_SENSOR_SETUP;
stateEnterTime = millis();
}
}
digitalWrite(LED_BUILTIN, LOW);
}
break;
}
case STATE_ERROR: { // Try to recover by resetting everything
outputDebugLine("State: ERROR - Performing recovery");
errorCount = 0;
currentState = STATE_INIT;
stateEnterTime = millis();
break;
}
}
}
//------------------ Setup -----------------
void setup() {
// Initialize onboard LED
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
// Initialize Serial for debugging
if (DEBUG) {
Serial.begin(115200);
while (!Serial && millis() < 3000); // Wait for serial port with timeout
delay(5000); // Wait a few seconds for PC to display serial connection
}
outputDebugLine("\n\n===== Environmental Monitor Starting =====");
currentState = STATE_INIT;
}
//----------------- Main Loop --------------
void loop() {
processStateMachine();
}