Embedded Systems

Arduino Projects

This is a collection of Arduino based projects I have been working on. Most of them are designed to tie into my broader homelab/Home assistant setup while some are standalone projects.

Project list:

Motion sensor camera
// Sketch to use an ESP32 + esp32 camera to run a motion detection algorithm and save snapshots when motion is detected

#include "esp_camera.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include "../secrets.h" 

// Target Unraid Server
const char* serverName = "http://192.168.xx.xxx:8080/";

// ========== PINS FOR ESP32-S CAM ==========
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

// Globals
WiFiClient client;
unsigned long lastTriggerTime = 0;
long lastFingerprint = 0;
const int motionThreshold = 2500;

void setup_wifi() {
  Serial.print("Connecting to WiFi");
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected! IP: " + WiFi.localIP().toString());
}

void setupCamera() {
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 10000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_VGA;
  config.jpeg_quality = 15;
  config.fb_count = 1;
  config.fb_location = CAMERA_FB_IN_DRAM;
  config.grab_mode = CAMERA_GRAB_LATEST;

  // 1. Initialize Camera
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed: 0x%x\n", err);
    return;
  }

  // 2. Adjust Sensor Settings AFTER Init
  sensor_t * s = esp_camera_sensor_get();
  if (s) {
    s->set_whitebal(s, 1);
    s->set_awb_gain(s, 1);
    s->set_exposure_ctrl(s, 0); 
    s->set_aec_value(s, 300); 
  }
  
  Serial.println("Camera Ready!");
}

// Function to compare previous frame with current frame to detect motion
bool isMotion(camera_fb_t * fb) {
  if (!fb || fb->len < 1000) return false;

  long currentFingerprint = 0;
  // Build up fingerprint from current frame buffer
  for (size_t i = 500; i < fb->len; i += 100) {
    currentFingerprint += fb->buf[i];
  }

  if (lastFingerprint == 0) {
    lastFingerprint = currentFingerprint;
    return false;
  }

  // compare new and old finger print with our threshold for motion
  long diff = abs(currentFingerprint - lastFingerprint);
  lastFingerprint = currentFingerprint;

  if (diff > motionThreshold) {
    Serial.printf("Motion Detected! Diff: %ld\n", diff);
    return true;
  }
  return false;
}

// Function to send the image to unraid server
void sendImage(camera_fb_t * fb) {
  HTTPClient http;
  http.begin(client, serverName);
  http.addHeader("Content-Type", "image/jpeg");
  
  int httpResponseCode = http.POST(fb->buf, fb->len);
  
  Serial.printf("Unraid Response: %d\n", httpResponseCode);
  http.end();
}

void setup() {
  Serial.begin(115200);
  setup_wifi();
  setupCamera();
}

void loop() {
  // get most recent frame buffer
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) return;

  // Check if motion occurred AND if we are out of the 5-second cooldown to prevent constant detections 
  if (isMotion(fb) && (millis() - lastTriggerTime > 5000)) {
    sendImage(fb); //Send image if motion was detected
    lastTriggerTime = millis();
  }

  esp_camera_fb_return(fb);
  delay(200); // 5 checks per second
}
Task Timer
// Task Timer V2 for ESP32. 
// After arduino Uno prototype, this script converts the script to run on an ESP32 board for a smaller form factor for the prototype device.
// Also includes a lot of logic and timing optimization to account for the less powerful ESP32 to prevent flicker and delays in operation.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);

// Pin Definitions for ESP32
const int xPin = 35;
const int yPin = 34;
const int swPin = 23;
int latchPin = 33;
int dataPin = 25;
int clockPin = 32;
const int digitPins[4] = { 13, 14, 27, 26 };

// Common Anode bit Map for displaying the digits
const byte digitMap[] = {
  0b00000011, 0b10011111, 0b00100101, 0b00001101, 0b10011001,
  0b01001001, 0b01000001, 0b00011111, 0b00000001, 0b00001001,
  0b10010001 // Index 10: "H"
};

// Timing and State
unsigned long previousMillis = 0;
unsigned long lastLogicUpdate = 0;
const int interval = 1000;
int menuScreen = 0;
bool loaded = false;
int taskSelected = 0;

// Global Digit Storage for 7-Seg
int d[4] = {0, 0, 0, 0};
bool globalColon = false;

struct Task {
  String taskName;
  long targetTime;
  long timeSpent;
};
Task taskArray[3];
int numTasks = 2;

// Debounce variables
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 200; 

void setup() {
  Serial.begin(115200); // ESP32 standard is 115200
  Wire.setClock(400000); // Speed up I2C for the LCD
  
  lcd.init();
  lcd.backlight();

  pinMode(swPin, INPUT_PULLUP);
  pinMode(latchPin, OUTPUT);
  pinMode(dataPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  for (int i = 0; i < 4; i++) {
    pinMode(digitPins[i], OUTPUT);
    digitalWrite(digitPins[i], HIGH);
  }

  taskArray[0] = {"Task Timer", 3600000, 0};
  taskArray[1] = {"Edit Photos", 3600000, 0};
  taskArray[2] = {"Website", 3600000, 0};
}

void loop() {
  unsigned long currentMillis = millis();

  // 1. REFRESH 7-SEGMENT (One digit per loop for maximum speed)
  long remaining = (taskArray[taskSelected].targetTime - taskArray[taskSelected].timeSpent) / 1000;
  if (remaining < 0) remaining = 0;
  updateDigitBuffer((int)remaining);
  refreshSevenSegment();

  // 2. RUN LOGIC (Every 50ms to keep I2C bus clear)
  if (currentMillis - lastLogicUpdate > 50) {
    lastLogicUpdate = currentMillis;
    handleInputsAndMenu();
  }

  yield(); // Required for ESP32 background stability
}

// Pre-calculates the digits so the refresh function is lightning fast
void updateDigitBuffer(int seconds) {
  if (seconds > 3600) {
    d[0] = seconds / 3600;
    d[1] = 10; // "H"
    int mins = (seconds % 3600) / 60;
    d[2] = mins / 10;
    d[3] = mins % 10;
    globalColon = false;
  } else {
    int mins = seconds / 60;
    int secs = seconds % 60;
    d[0] = mins / 10;
    d[1] = mins % 10;
    d[2] = secs / 10;
    d[3] = secs % 10;
    globalColon = true;
  }
}

// Cycles through one digit each time it is called
void refreshSevenSegment() {
  static int currentDigit = 0;
  
  byte data = digitMap[d[currentDigit]];
  // Apply colon to the second digit if required
  if (currentDigit == 1 && globalColon) data &= 0b11111110;

  digitalWrite(latchPin, LOW);
  shiftOut(dataPin, clockPin, MSBFIRST, ~data);
  digitalWrite(latchPin, HIGH);

  digitalWrite(digitPins[currentDigit], LOW); // ON
  delayMicroseconds(600);                     // Brightness control TODO: tie value to potentiometer knob
  digitalWrite(digitPins[currentDigit], HIGH); // OFF

  currentDigit = (currentDigit + 1) % 4;
}

void handleInputsAndMenu() {
  // ESP32 ADC is 12-bit (0-4095)
  int xValue = analogRead(xPin);
  int yValue = analogRead(yPin);
  
  // The SW pin on joystick is usually active LOW (0 when pressed)
  bool swState = digitalRead(swPin); 
  bool buttonPressed = debounce(swState);

  // Determine Joystick Direction with 4095 scaling
  // Center is roughly 2048
  int joy = 0; // CENTER
  if (yValue < 1000) joy = 1;      // UP
  else if (yValue > 3000) joy = 3; // DOWN
  else if (xValue < 1000) joy = 4; // LEFT
  else if (xValue > 3000) joy = 2; // RIGHT

  // --- MENU NAVIGATION ---
  // We use a "lastJoy" check to ensure we only move ONCE per tilt
  static int lastJoy = 0;
  if (joy != lastJoy && joy != 0) {
    if (menuScreen == 1) { // SETTASK
       if (joy == 2) { // RIGHT
         taskSelected = (taskSelected + 1) % 3;
         loaded = false;
       } else if (joy == 4) { // LEFT
         taskSelected = (taskSelected - 1 + 3) % 3;
         loaded = false;
       }
    } 
    else if (menuScreen == 2) { // SETTIMER
       if (joy == 1) { // UP
         taskArray[taskSelected].targetTime += 60000;
         loaded = false;
       } else if (joy == 3) { // DOWN
         if (taskArray[taskSelected].targetTime > 60000) {
            taskArray[taskSelected].targetTime -= 60000;
            loaded = false;
         }
       }
    }
    else if (menuScreen == 0) { // WELCOME
       menuScreen = 1;
       loaded = false;
    }
  }
  lastJoy = joy;

  // --- BUTTON NAVIGATION ---
  if (buttonPressed) {
    if (menuScreen == 1) menuScreen = 2;      // From Task to Timer
    else if (menuScreen == 2) menuScreen = 3; // From Timer to Running
    else if (menuScreen == 3) menuScreen = 4; // From Running to Paused
    else if (menuScreen == 4) menuScreen = 3; // From Paused to Running
    loaded = false;
  }

  // --- TIMER TICKING ---
  if (menuScreen == 3) { // RUNNING
    if (millis() - previousMillis >= interval) {
      previousMillis = millis();
      taskArray[taskSelected].timeSpent += 1000;
      
      int currentSecs = (taskArray[taskSelected].timeSpent / 1000) % 60;

      // ONLY update LCD when seconds hit zero (every minute)
      // or if we just started (loaded == false)
      if (currentSecs == 0) {
        updateLCDOnlyNumbers(); 
      }
    }
  }

  // Reload screen if menu changed
  if (!loaded) {
    renderFullLCD();
  }
}

void renderFullLCD() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(taskArray[taskSelected].taskName);
  updateLCDOnlyNumbers();
  loaded = true;
}

void updateLCDOnlyNumbers() {
  lcd.setCursor(0, 1);
  lcd.print(taskArray[taskSelected].timeSpent / 60000);
  lcd.print("min / ");
  lcd.print(taskArray[taskSelected].targetTime / 60000);
  lcd.print("min   "); // Spaces clear old digits
}

bool debounce(int reading) {
  if (reading == LOW && (millis() - lastDebounceTime > debounceDelay)) {
    lastDebounceTime = millis();
    return true;
  }
  return false;
}
Temp and Humidity Sensors
// Greenhouse Temperature and humidity sensor
// Sketch to get data from dht22 sensor, connect to wifi, and send the info to home assistant through MQTT broker

#include "../secrets.h"
#include <WiFi.h>
#include <DHT.h>
#include <PubSubClient.h>
#include <ArduinoOTA.h>


// MQTT Settings
const int mqtt_port = 1883;
const char* mqtt_topic = "home/esp32/temperature";

//  DHT Setup 
#define DHTPIN 14          // GPIO14 = DHT22 temp sensor
#define DHTTYPE DHT22      // DHT 22 (AM2302)

DHT dht(DHTPIN, DHTTYPE);

// MQTT Topics for our two bits of info we are sending
const char* temperature_topic = "home/esp32/temperature/greenhouse";
const char* humidity_topic = "home/esp32/humidity/greenhouse";


WiFiClient espClient;
PubSubClient client(espClient);

void setup_wifi() {
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(WIFI_SSID);

  WiFi.mode(WIFI_STA); //Station mode
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  int retries = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    retries++;
    if (retries > 20) { // try for 10 seconds
      Serial.println("Failed to connect to WiFi!");
      return;
    }
  }

  Serial.println("");
  Serial.println("WiFi connected!");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void reconnect() {
  // Loop until reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("ESP32Client", MQTT_USER, MQTT_PASSWORD)) {
      Serial.println("connected");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}


void setup() {
  Serial.begin(115200);
  dht.begin();
  setup_wifi();
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  // OTA Setup
  ArduinoOTA.setHostname(GREENHOUSE_OTA_HOSTNAME);
  ArduinoOTA.setPassword(GREENHOUSE_OTA_PASSWORD); 

  ArduinoOTA.onStart([]() {
    Serial.println("Start updating...");
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");
  });
  
  ArduinoOTA.begin();
  Serial.println("OTA Ready"); 

  client.setServer(MQTT_SERVER, 1883);
}

void loop() {
  ArduinoOTA.handle(); 
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  // Read DHT22 valuesj 
  float humidity = dht.readHumidity();
  float temperature = dht.readTemperature(true); // Celsius by default

  if (isnan(humidity) || isnan(temperature)) {
    Serial.println("Failed to read from DHT sensor!");
    return;
  }

  // Debug prints
  Serial.print("Temperature: ");
  Serial.print(temperature);
  Serial.print(" °F  |  Humidity: ");
  Serial.print(humidity);
  Serial.println(" %");

  // Publish to MQTT
  client.publish(temperature_topic, String(temperature).c_str(), true);
  client.publish(humidity_topic, String(humidity).c_str(), true);

  delay(30000); // Read and publish every 30 seconds
}
Doorbell
// Doorbell imitation device
// Once 'doorbell' is pressed on breadboard, publish to MQTT so Home assistant can play doorbell sounds on my computer

#include "../../secrets.h"
#include <WiFi.h>
#include <DHT.h>
#include <PubSubClient.h>
#include <ArduinoOTA.h>

// constants
const int buttonPin = 33;  
const int ledPin = 25; 

// --- MQTT Settings ---
const int mqtt_port = 1883;
const char* mqtt_topic = "home/doorbell";

WiFiClient espClient;
PubSubClient client(espClient);

int buttonState = 0; 
int lastButtonState = HIGH;

void setup_wifi() {
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(WIFI_SSID);

  WiFi.mode(WIFI_STA); //Station mode
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  int retries = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    retries++;
    if (retries > 20) { // After 10 seconds give up
      Serial.println("Failed to connect to WiFi!");
      retries = 0;
      return;
    }
  }

  Serial.println("");
  Serial.println("WiFi connected!");
  Serial.println("IP address: "); 
  Serial.println(WiFi.localIP());
}

void reconnect() {
  // Loop until reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("ESP32Client", MQTT_USER, MQTT_PASSWORD)) {
      Serial.println("connected");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}


void setup_OTA() {
  // OTA Setup      
  ArduinoOTA.setHostname(DOORBELL_OTA_HOSTNAME);
  ArduinoOTA.setPassword(DOORBELL_OTA_PASSWORD); 

  ArduinoOTA.onStart([]() {
    Serial.println("Start updating...");
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");
  });
  ArduinoOTA.begin();
  Serial.println("OTA Ready");
}



void setup() {
  Serial.begin(115200);
   // initialize the LED pin as an output:
  pinMode(ledPin, OUTPUT);
  // initialize the pushbutton pin as an input:
  pinMode(buttonPin, INPUT_PULLUP);

  setup_wifi(); //Call wifi setup
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  setup_OTA(); //Call OTA setup

  client.setServer(MQTT_SERVER, 1883); //Set MQTT server
 
}

void loop() {
  ArduinoOTA.handle();

  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  // Read the state 
  buttonState = digitalRead(buttonPin);

  // Detect a press, LOW = Pressed 
  if (buttonState == LOW && lastButtonState == HIGH) {
    Serial.println("Button Pressed");
    digitalWrite(ledPin, HIGH);

    //Publish to MQTT for Homeassistant to hear
    client.publish(mqtt_topic, "pressed");
  }
  else if (buttonState == HIGH && lastButtonState == LOW) {
    // Button released
    digitalWrite(ledPin, LOW);
    client.publish(mqtt_topic, "unpressed");
  }

  // Save the current state for next loop
  lastButtonState = buttonState;
}
Desk Dashboard
// Hank Desk Dash V1
// beginning code for a desk dashboard to control my smarthome and view real time info
// With an ESP32, an oled display, temp/humidity sensor, and basic controls.
// Connects to wifi and displays info on screen and sends commands back to Home assistant through MQTT

#include "../secrets.h"
#include <WiFi.h>
#include <DHT.h>
#include <PubSubClient.h>
#include <ArduinoOTA.h>
#include <SPI.h>
#include <U8g2lib.h>



//  MQTT Settings 
const int mqtt_port = 1883; // default port
const char* mqtt_topic = "home/esp32/temperature";

//  DHT Setup 
#define DHTPIN 14          // GPIO14 DHT22 is 
#define DHTTYPE DHT22      // DHT 22 (AM2302)

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32

// SPI OLED pin definitions
#define OLED_DC   17
#define OLED_CS   5
#define OLED_RESET 16

// Constructor for SH1106 128x64 SPI
U8G2_SH1106_128X64_NONAME_F_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ OLED_CS, /* dc=*/ OLED_DC, /* reset=*/ OLED_RESET);


DHT dht(DHTPIN, DHTTYPE);

//  MQTT Topics 
const char* temperature_topic = "home/esp32/temperature/hanksdesk";
const char* humidity_topic = "home/esp32/humidity/hanksdesk";


WiFiClient espClient; 
PubSubClient client(espClient);

void setup_wifi() {
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(WIFI_SSID);

  WiFi.mode(WIFI_STA); // Station mode
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  int retries = 0;
  while (WiFi.status() != WL_CONNECTED) {
    // Show "Connecting..." screen with dot animation
    u8g2.clearBuffer();
    u8g2.setFont(u8g2_font_ncenB08_tr);
    u8g2.drawStr(0, 12, "Connecting to WiFi");
    u8g2.drawStr(0, 28, WIFI_SSID);

    // Animate dots based on retry count
    char dots[4] = "   ";
    for (int i = 0; i < (retries % 4); i++) dots[i] = '.';
    u8g2.drawStr(0, 44, dots);

    u8g2.sendBuffer();

    delay(500);
    Serial.print(".");
    retries++;
    if (retries > 20) { // After 10 seconds give up
      Serial.println("Failed to connect to WiFi!");

      u8g2.clearBuffer();
      u8g2.setFont(u8g2_font_ncenB08_tr);
      u8g2.drawStr(0, 12, "WiFi Failed!");
      u8g2.drawStr(0, 28, "Check credentials");
      u8g2.sendBuffer();

      return;
    }
  }

  Serial.println("");
  Serial.println("WiFi connected!");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  // Show "Connected!" screen with IP address
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_ncenB08_tr);
  u8g2.drawStr(0, 12, "WiFi Connected!");
  u8g2.drawStr(0, 28, WIFI_SSID);

  // Display IP address
  char ipStr[20];
  WiFi.localIP().toString().toCharArray(ipStr, sizeof(ipStr));
  u8g2.drawStr(0, 44, ipStr);

  // Show current temp/humidity on connected screen if sensor is ready
  float h = dht.readHumidity();
  float t = dht.readTemperature(true); // Fahrenheit
  if (!isnan(h) && !isnan(t)) {
    char sensorStr[32];
    snprintf(sensorStr, sizeof(sensorStr), "%.1fF  %.1f%%RH", t, h);
    u8g2.drawStr(0, 58, sensorStr);
  }

  u8g2.sendBuffer();
  delay(2000); // Hold the connected screen for 2 seconds
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("ESP32Client", MQTT_USER, MQTT_PASSWORD)) {
      Serial.println("connected");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

void setup_OTA() {
  // OTA Setup      
  ArduinoOTA.setHostname(HANKDESKDASH_OTA_HOSTNAME);
  ArduinoOTA.setPassword(HANKDESKDASH_OTA_PASSWORD); 

  ArduinoOTA.onStart([]() {
    Serial.println("Start updating...");
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");
  });
  ArduinoOTA.begin();
  Serial.println("OTA Ready"); 
}


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

  // Init display first so we can give feedback on wifi status
  u8g2.begin();

  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_ncenB08_tr);
  u8g2.drawStr(0, 12, "Hank's Desk");
  u8g2.drawStr(0, 28, "Starting up...");
  u8g2.sendBuffer();

  dht.begin();     
  delay(2000);      

  setup_wifi(); //Call wifi setup
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  setup_OTA(); //Call OTA setup

  client.setServer(MQTT_SERVER, 1883); //Set MQTT server
}

void loop() {
  ArduinoOTA.handle();
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  // Read DHT22 values
  float humidity = dht.readHumidity();
  float temperature = dht.readTemperature(true); // Fahrenheit

  if (isnan(humidity) || isnan(temperature)) {
    Serial.println("Failed to read from DHT sensor!");

    u8g2.clearBuffer();
    u8g2.setFont(u8g2_font_ncenB08_tr);
    u8g2.drawStr(0, 12, "Sensor Error!");
    u8g2.drawStr(0, 28, "DHT22 read failed");
    u8g2.sendBuffer();

    return;
  }

  // Debug prints
  // Serial.print("Temperature: ");
  // Serial.print(temperature);
  // Serial.print(" °F  |  Humidity: ");
  // Serial.print(humidity);
  // Serial.println(" %");

  // Publish to MQTT
  client.publish(temperature_topic, String(temperature).c_str(), true);
  client.publish(humidity_topic, String(humidity).c_str(), true);

  // Update display with current readings
  char tempStr[16];
  char humStr[16];
  char ipStr[20];
  snprintf(tempStr, sizeof(tempStr), "Temp: %.1f F", temperature);
  snprintf(humStr, sizeof(humStr), "Hum:  %.1f %%", humidity);
  WiFi.localIP().toString().toCharArray(ipStr, sizeof(ipStr));

  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_ncenB08_tr);
  u8g2.drawStr(0, 12, "Hank's Desk");
  u8g2.drawStr(0, 28, tempStr);
  u8g2.drawStr(0, 44, humStr);
  u8g2.drawStr(0, 58, ipStr);
  u8g2.sendBuffer();

  delay(30000); // Read and publish every 30 seconds
}