main.cpp
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <MQTT.h>
#include <Wire.h>
#include <Adafruit_BME680.h>
#include <Adafruit_NeoPixel.h>
#include <ArduinoJson.h>
 
//////////////// EDIT THESE ////////////////
const char* WIFI_SSID     = "YOUR_SSID";
const char* WIFI_PASSWORD = "YOUR_PASSWORD";
const char* MQTT_HOST     = "test.mosquitto.org";   // or your lab broker
const uint16_t MQTT_PORT  = 1883;
const char* BASE_TOPIC    = "iot/studentname";         // change per student
////////////////////////////////////////////
 
#define SDA_PIN 21
#define SCL_PIN 22
#define NEOPIXEL_PIN 3
 
WiFiClient net;
MQTTClient mqtt(2048);
Adafruit_BME680 bme;
Adafruit_NeoPixel pixel(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
 
unsigned long nextConnTry = 0;
uint32_t backoffMs = 1000;
unsigned long lastHeartbeat = 0;
unsigned long lastSensorPub = 0;
 
void ensureWiFi() {
  if (WiFi.status() == WL_CONNECTED) return;
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("WiFi connecting");
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.printf("\nWiFi OK, IP: %s, RSSI: %d dBm\n", WiFi.localIP().toString().c_str(), WiFi.RSSI());
}
 
void setLED(uint8_t r, uint8_t g, uint8_t b, uint8_t brightness) {
  pixel.setBrightness(brightness);
  pixel.setPixelColor(0, pixel.Color(r, g, b));
  pixel.show();
}
 
void handleLEDJson(const String& payload) {
  JsonDocument doc;
  DeserializationError err = deserializeJson(doc, payload);
  if (err) { Serial.printf("LED JSON parse error: %s\n", err.c_str()); return; }
  uint8_t r = doc["r"].isNull() ? 0 : doc["r"].as<uint8_t>();
  uint8_t g = doc["g"].isNull() ? 0 : doc["g"].as<uint8_t>();
  uint8_t b = doc["b"].isNull() ? 0 : doc["b"].as<uint8_t>();
  uint8_t br = doc["brightness"].isNull() ? 100 : doc["brightness"].as<uint8_t>();
  setLED(r,g,b,br);
 
  JsonDocument ack;
  ack["ok"] = true; ack["r"] = r; ack["g"] = g; ack["b"] = b; ack["brightness"] = br;
  String out; serializeJson(ack, out);
  mqtt.publish((String(BASE_TOPIC)+"/led/ack").c_str(), out.c_str(), false, 1);
}
 
void messageReceived(String &topic, String &payload) {
  Serial.printf("MSG [%s]: %s\n", topic.c_str(), payload.c_str());
  if (topic == String(BASE_TOPIC) + "/led") handleLEDJson(payload);
}
 
bool connectMQTT() {
  String cid = String("sparrow-c6-qos1-") + String((uint32_t)ESP.getEfuseMac(), HEX);
  mqtt.setCleanSession(true);
  mqtt.setKeepAlive(30);
  String willTopic = String(BASE_TOPIC) + "/status";
  mqtt.setWill(willTopic.c_str(), "{\"status\":\"offline\"}", true, 1);
 
  Serial.printf("MQTT connecting to %s:%u as %s ...\n", MQTT_HOST, MQTT_PORT, cid.c_str());
  bool ok = mqtt.connect(cid.c_str(), nullptr, nullptr);
  if (!ok) { Serial.printf("MQTT connect failed, error=%d\n", mqtt.lastError()); return false; }
 
  mqtt.subscribe((String(BASE_TOPIC)+"/led").c_str(), 1);
  mqtt.publish(willTopic.c_str(), "{\"status\":\"online\"}", true, 1);
  Serial.println("MQTT connected");
  return true;
}
 
void ensureMQTT() {
  if (mqtt.connected()) return;
  if (millis() < nextConnTry) return;
  if (!connectMQTT()) {
    backoffMs = min<uint32_t>(backoffMs * 2, 30000);
    nextConnTry = millis() + backoffMs;
    Serial.printf("Retry in %lu ms\n", (unsigned long)backoffMs);
  } else {
    backoffMs = 1000;
  }
}
 
bool readBME(float& tC, float& hPct, float& pHpa, float& gas) {
  if (!bme.performReading()) return false;
  tC = bme.temperature;
  hPct = bme.humidity;
  pHpa = bme.pressure / 100.0;
  gas = bme.gas_resistance;
  return true;
}
 
void setup() {
  Serial.begin(115200);
  pixel.begin();
  setLED(0,0,0,10);
 
  Wire.begin(SDA_PIN, SCL_PIN);
  if (!bme.begin(0x76)) {
    Serial.println("BME688/BME680 not found at 0x76!");
  } else {
    bme.setTemperatureOversampling(BME680_OS_8X);
    bme.setHumidityOversampling(BME680_OS_2X);
    bme.setPressureOversampling(BME680_OS_4X);
    bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
    bme.setGasHeater(0, 0);
  }
 
  mqtt.begin(MQTT_HOST, MQTT_PORT, net);
  mqtt.onMessage(messageReceived);
}
 
void loop() {
  ensureWiFi();
  ensureMQTT();
  mqtt.loop();
 
  if (mqtt.connected() && millis() - lastHeartbeat > 15000) {
    lastHeartbeat = millis();
    JsonDocument hb;
    hb["uptime_s"] = (uint32_t)(millis()/1000);
    hb["ip"] = WiFi.localIP().toString();
    hb["rssi"] = WiFi.RSSI();
    String out; serializeJson(hb, out);
    mqtt.publish((String(BASE_TOPIC)+"/heartbeat").c_str(), out.c_str(), true, 1);
    Serial.printf("Heartbeat -> %s\n", out.c_str());
  }
 
  if (mqtt.connected() && millis() - lastSensorPub > 10000) {
    lastSensorPub = millis();
    float tC, hPct, pHpa, gas;
    if (readBME(tC, hPct, pHpa, gas)) {
      JsonDocument s;
      s["ts"] = (uint32_t)(millis()/1000);
      s["temp_c"] = tC;
      s["hum_pct"] = hPct;
      s["press_hpa"] = pHpa;
      s["gas_ohm"] = gas;
      String out; serializeJson(s, out);
      mqtt.publish((String(BASE_TOPIC)+"/bme688").c_str(), out.c_str(), false, 1);
      Serial.printf("Sensor -> %s\n", out.c_str());
    } else {
      Serial.println("BME read failed");
    }
  }
}