/*
Humidistat V10154
(c) Oliver King 2025
Here we have Humidistat with working 108Khz output
High and low temp alarms
I2C comms A4=SDA A5=SCL
OLED display I2C plus RST P9
BME280 module I2C
3 buttons > plus minus Enter <
V1.01 small changes to display menu
V1.011 Add display error message if BME not responding
V1.012 Add progress bar when holding button to make menu selection more intuitive
V1.014 Add fan output - Turns on with humidity, stays on for 5 minutes. Also turns on if temp high alarm is activated.
V1015
V10151 Add Adjustable Hysteresis to defines
V10152 New Option - turn 108Khz output off if high alarm (16MHz/(147+1) = ~108kHz).
V10153 Put images in different sketch for easier manipulation.
V10154 Small changes to display
To do
**V10155 Adds delay variables - Vapour is released after fan established, and has maximum on time of x seconds, before turning off for x seconds before re-evaluating
Add Min/Max temp memory with reset
*/
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_BME280.h>
#include <EEPROM.h>
#include "Bunia.h"
#define BUTTON_UP 2
#define BUTTON_DOWN 3
#define BUTTON_SELECT 4
#define OUTPUT_PIN 5
#define ALARM_OUTPUT 6
#define OLED_RESET 9
#define PWM_OUTPUT_PIN 10 // PWM output on pin 10
#define FAN_PIN 11 // Fan control on D11
#define EEPROM_INIT_FLAG 0x55 // Magic number to check if EEPROM was initialized
#define EEPROM_INIT_ADDR 8 // Address to store initialization flag
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define DISPLAY_REFRESH_INTERVAL 500 // milliseconds
Adafruit_BME280 bme;
#define DEBOUNCE_DELAY 150
#define LONG_PRESS_TIME 2000 // Changed from 1000 to 2000ms
#define PROGRESS_DOT_INTERVAL 500 // Add a dot every 500ms
#define PROGRESS_MAX_DOTS 4 // 2000ms / 500ms = 4 dots
// Limits
#define MIN_HUMIDITY 10
#define MAX_HUMIDITY 90
#define MIN_TEMP 10
#define MAX_TEMP 65
#define TEMP_HYSTERESIS_HIGH 1 // Hysteresis for high temperature alarm
#define TEMP_HYSTERESIS_LOW 1 // Hysteresis for low temperature alarm
#define HUMIDITY_HYSTERESIS 1 // Hysteresis for humidity control
#define OUTPUT_DELAY_MS 2000 // Delay before first activation of 108Khz
#define OUTPUT_DURATION_MS 10000 // How long to keep output on
#define OUTPUT_COOLDOWN_MS 20000 // Wait time before checking again
int currentTemp = 0; //was float/int
int currentHumidity = 0; //was float
int targetHumidity = 60;
int highAlarmTemp = 65;
int lowAlarmTemp = 10;
bool alarmEnabled = true;
bool settingMode = false;
int settingIndex = 0;
bool selectPressed = false, upPressed = false, downPressed = false;
unsigned long lastButtonPress = 0, selectPressTime = 0;
unsigned long lastDisplayUpdate = 0;
int prevTargetHumidity = targetHumidity;
int prevHighAlarmTemp = highAlarmTemp;
int prevLowAlarmTemp = lowAlarmTemp;
bool prevAlarmEnabled = alarmEnabled;
unsigned long fanTurnOffTime = 0; // Tracks when to turn fan off
const unsigned long FAN_POST_MIST_DURATION = 9000; // 5 (300000) minutes (in ms)
unsigned long mistStartTime = 0;
unsigned long mistCooldownTime = 0;
bool mistingActive = false;
bool cooldownActive = false;
//bool displayBeginReturn = display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
/*
void printFreeMemory() {
extern int __heap_start, *__brkval;
int v;
Serial.print("Free RAM: ");
Serial.println((int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval));
}
void scanI2C() {
byte error, address;
int nDevices;
Serial.println("Scanning...");
nDevices = 0;
for(address = 1; address < 127; address++ ) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0) {
Serial.print("I2C device found at 0x");
if (address<16) Serial.print("0");
Serial.print(address,HEX);
Serial.println(" !");
nDevices++;
}
}
if (nDevices == 0) Serial.println("No I2C devices found");
}*/
bool validateEEPROMSettings() {// function to validate EEPROM data
if (targetHumidity < MIN_HUMIDITY || targetHumidity > MAX_HUMIDITY) return false;
if (highAlarmTemp < MIN_TEMP || highAlarmTemp > MAX_TEMP) return false;
if (lowAlarmTemp < MIN_TEMP || lowAlarmTemp > MAX_TEMP) return false;
if (lowAlarmTemp >= highAlarmTemp) return false; // Low alarm must be < high alarm
return true;
}
void setDefaultValues() {
targetHumidity = 60; // Reasonable default humidity
highAlarmTemp = 65; // Default high temp alarm
lowAlarmTemp = 10; // Default low temp alarm
alarmEnabled = true; // Default to alarms on
// Store the default values to EEPROM
EEPROM.put(0, targetHumidity);
EEPROM.put(2, highAlarmTemp);
EEPROM.put(4, lowAlarmTemp);
EEPROM.put(6, (int)alarmEnabled);
EEPROM.write(EEPROM_INIT_ADDR, EEPROM_INIT_FLAG);
}
void setupTimer1ForPWM() {
// Configure Timer1 for 108kHz PWM on pin 10 (OC1B)
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
// Fast PWM mode with ICR1 as TOP (Mode 14)
TCCR1A = (1 << COM1B1) | (1 << WGM11); // Clear OC1B on compare, Fast PWM upper bits
TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10); // Fast PWM lower bits, no prescaler
ICR1 = 147; // TOP value for ~108kHz (16MHz/(147+1))
OCR1B = (ICR1 * 0.1); // 10% duty cycle
pinMode(PWM_OUTPUT_PIN, OUTPUT);
interrupts();
}
void disableTimer1PWM() {
// Disconnect PWM from pin but keep timer running
TCCR1A &= ~(1 << COM1B1);
digitalWrite(PWM_OUTPUT_PIN, LOW);
}
void enableTimer1PWM() {
// Reconnect PWM to pin
TCCR1A |= (1 << COM1B1);
}
void setup() {
Wire.begin();
// Wire.setClock(50000);
Serial.begin(9600);
// printFreeMemory();
// scanI2C();
delay(100);
//Serial.println("SSD1306 allocation Successful");
//bme.begin(0x76);
//if (!bme.begin(0x76)) {
// displaySensorError();
// while(1); // Don't proceed if display fails
//}
pinMode(FAN_PIN, OUTPUT);
digitalWrite(FAN_PIN, LOW); // Start with fan off
pinMode(BUTTON_UP, INPUT_PULLUP);
pinMode(BUTTON_DOWN, INPUT_PULLUP);
pinMode(BUTTON_SELECT, INPUT_PULLUP);
pinMode(OUTPUT_PIN, OUTPUT);
pinMode(ALARM_OUTPUT, OUTPUT);
/*bool displayBeginReturn = display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
// Initialize display first
if (displayBeginReturn) {
Serial.println("display.begin() returned success");
} else {
Serial.println("display.begin() returned failure. Halting.");
while (true);
}*/
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
/*if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 allocation failed");
while(1); // Don't proceed if display fails
}*/
display.clearDisplay();
display.display();
delay(200);
display.drawBitmap(0, 0, epd_bitmap_BUN, 128, 64, WHITE);
display.display();
delay(3000);
display.clearDisplay();
display.display();
delay(100);
display.drawBitmap(0, 0, epd_bitmap_ME3, 128, 64, WHITE);
display.display();
delay(1600);
display.clearDisplay();
display.display();
delay(100);
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(20, 20);
delay(100);
display.print("V10153H1");
display.display();
delay(100);
// Load settings from EEPROM with validation
byte initialized = EEPROM.read(EEPROM_INIT_ADDR);
if (initialized != EEPROM_INIT_FLAG) {
// First run - initialize EEPROM with defaults
setDefaultValues();
} else {
// Load values from EEPROM
EEPROM.get(0, targetHumidity);
EEPROM.get(2, highAlarmTemp);
EEPROM.get(4, lowAlarmTemp);
EEPROM.get(6, alarmEnabled);
// Validate loaded values
if (!validateEEPROMSettings()) {
setDefaultValues(); // Reset to defaults if invalid
}
}
// Store previous values for change detection
prevTargetHumidity = targetHumidity;
prevHighAlarmTemp = highAlarmTemp;
prevLowAlarmTemp = lowAlarmTemp;
prevAlarmEnabled = alarmEnabled;
// Initialize Timer1 for PWM (after display is initialized)
setupTimer1ForPWM();
disableTimer1PWM(); // Start with PWM disabled
// Test outputs
digitalWrite(OUTPUT_PIN, HIGH);
delay(200);
digitalWrite(ALARM_OUTPUT, HIGH);
delay(400);
digitalWrite(FAN_PIN, HIGH);
delay(600);
digitalWrite(OUTPUT_PIN, LOW);
delay(200);
digitalWrite(ALARM_OUTPUT, LOW);
delay(500);
digitalWrite(FAN_PIN, LOW);
}
void loop() {
readSensorData();
checkTemperatureAlarms();
//checkHumidity();
handleButtons();
updateDisplay();
// Fan control logic:
if (fanTurnOffTime > 0 && millis() >= fanTurnOffTime) {
if (digitalRead(ALARM_OUTPUT) == LOW) { // Only turn off if not in alarm
digitalWrite(FAN_PIN, LOW);
fanTurnOffTime = 0;
}
}
}
void readSensorData() {
if (!bme.begin(0x76)) {
displaySensorError();
while(1) {};
}
//bme.begin(0x76);
currentTemp = bme.readTemperature();
currentHumidity = bme.readHumidity();
// Check for invalid readings (NaN)
if (isnan(currentTemp) || isnan(currentHumidity)) {
displaySensorError();
currentTemp = 0;
currentHumidity = 0;
}
}
/*void checkHumidity() {
unsigned long currentTime = millis();
// Check if we need to start misting
if (!mistingActive && !cooldownActive &&
(currentHumidity < targetHumidity - HUMIDITY_HYSTERESIS)) {
mistStartTime = currentTime + OUTPUT_DELAY_MS;
mistingActive = true;
digitalWrite(FAN_PIN, HIGH); // Fan ON immediately
fanTurnOffTime = currentTime + OUTPUT_DURATION_MS + FAN_POST_MIST_DURATION;
}
// Check if it's time to turn on the mist
if (mistingActive && currentTime >= mistStartTime &&
currentTime < (mistStartTime + OUTPUT_DURATION_MS)) {
digitalWrite(OUTPUT_PIN, HIGH); // Mister ON
enableTimer1PWM(); // Enable PWM
}
// Check if misting duration is complete
if (mistingActive && currentTime >= (mistStartTime + OUTPUT_DURATION_MS)) {
digitalWrite(OUTPUT_PIN, LOW); // Mister OFF
disableTimer1PWM(); // Disable PWM
mistingActive = false;
cooldownActive = true;
mistCooldownTime = currentTime + OUTPUT_COOLDOWN_MS;
}
// Check if cooldown period is over
if (cooldownActive && currentTime >= mistCooldownTime) {
cooldownActive = false;
}
// If humidity is above target, reset all misting states
if (currentHumidity > targetHumidity + HUMIDITY_HYSTERESIS) {
digitalWrite(OUTPUT_PIN, LOW); // Mister OFF
disableTimer1PWM(); // Disable PWM
mistingActive = false;
cooldownActive = false;
}
}*/
void checkHumidity() {
if (currentHumidity < targetHumidity - HUMIDITY_HYSTERESIS) {
// Humidity too low - turn on mister and fan
digitalWrite(OUTPUT_PIN, HIGH); // Mister ON
enableTimer1PWM(); // Enable PWM
digitalWrite(FAN_PIN, HIGH); // Fan ON
fanTurnOffTime = millis() + FAN_POST_MIST_DURATION; // Set turn-off time
}
else if (currentHumidity > targetHumidity + HUMIDITY_HYSTERESIS) {
// Humidity too high - turn off mister
digitalWrite(OUTPUT_PIN, LOW); // Mister OFF
disableTimer1PWM(); // Disable PWM
}
}
void checkTemperatureAlarms() {
static bool highTempAlarmActive = false;
static bool lowTempAlarmActive = false;
if (alarmEnabled) {
// High temp alarm
if (currentTemp >= highAlarmTemp) {
digitalWrite(ALARM_OUTPUT, HIGH);
digitalWrite(FAN_PIN, HIGH);
highTempAlarmActive = true;
fanTurnOffTime = 0;
}
else if (highTempAlarmActive && currentTemp <= (highAlarmTemp - TEMP_HYSTERESIS_HIGH)) {
digitalWrite(ALARM_OUTPUT, LOW);
digitalWrite(FAN_PIN, LOW);
highTempAlarmActive = false;
}
// Low temp alarm
if (currentTemp <= lowAlarmTemp) {
digitalWrite(ALARM_OUTPUT, HIGH);
lowTempAlarmActive = true;
}
else if (lowTempAlarmActive && currentTemp > (lowAlarmTemp + TEMP_HYSTERESIS_LOW)) {
digitalWrite(ALARM_OUTPUT, LOW);
lowTempAlarmActive = false;
}
}
else {
digitalWrite(ALARM_OUTPUT, LOW);
highTempAlarmActive = false;
lowTempAlarmActive = false;
}
}
void drawProgressBar(bool enteringMenu) {
static unsigned long lastDotTime = 0;
static int dotCount = 0;
unsigned long currentTime = millis();
// Reset if not in button press
if (!selectPressed) {
dotCount = 0;
return;
}
// Add new dot at intervals
if (currentTime - lastDotTime >= PROGRESS_DOT_INTERVAL) {
lastDotTime = currentTime;
if (dotCount < PROGRESS_MAX_DOTS) {
dotCount++;
}
}
// Draw the progress
display.fillRect(0, SCREEN_HEIGHT-8, SCREEN_WIDTH, 8, SSD1306_BLACK); // Clear progress area
display.setCursor(0, SCREEN_HEIGHT-8);
// Draw dots (up to dotCount)
for (int i = 0; i < dotCount; i++) {
display.print(".");
}
// Only show "M" or "S" after all dots are filled (dotCount == 4)
if (dotCount >= PROGRESS_MAX_DOTS) {
display.setCursor(SCREEN_WIDTH-8, SCREEN_HEIGHT-8);
display.print(enteringMenu ? "M" : "S");
}
display.display();
}
void updateDisplay() {
unsigned long currentMillis = millis();
if (currentMillis - lastDisplayUpdate < DISPLAY_REFRESH_INTERVAL) {
return; // Skip update if not enough time has passed
}
lastDisplayUpdate = currentMillis;
// Rest of your existing display code...
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
if (!settingMode) {
display.setCursor(0, 0);
display.print("PHOENIX Humidistat");
display.setCursor(0, 20);
display.print("Temp: ");
display.print(currentTemp);
display.print("C");
display.setCursor(0, 30);
display.print("Humidity: ");
display.print(currentHumidity);
display.print("%");
display.setCursor(0, 40);
display.print("Target: ");
display.print(targetHumidity);
display.print("%");
display.setCursor(0, 50);
display.print("Alarm: ");
display.print(alarmEnabled ? "ON" : "OFF");
} else {
display.setCursor(0, 0);
display.print("SETTINGS");
display.setCursor(55, 0);
display.print("(");
display.print(currentHumidity);
display.print("%)");
display.setCursor(88, 0);
display.print("(");
display.print(currentTemp);
display.print("C)");
display.setCursor(0, 20);
display.print(settingIndex == 0 ? "> Humidity: " : " Humidity: ");
display.print(targetHumidity);
display.setCursor(0, 30);
display.print(settingIndex == 1 ? "> High Alarm: " : " High Alarm: ");
display.print(highAlarmTemp);
display.setCursor(0, 40);
display.print(settingIndex == 2 ? "> Low Alarm: " : " Low Alarm: ");
display.print(lowAlarmTemp);
display.setCursor(0, 50);
display.print(settingIndex == 3 ? "> Alarm: " : " Alarm: ");
display.print(alarmEnabled ? "ON" : "OFF");
}
display.display();
}
void displaySensorError() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(20, 0);
display.println("SENSOR ERROR!");
display.setCursor(0, 20);
display.println("BME280 not found");
display.setCursor(0, 30);
display.println("Check connection");
display.display();
display.setCursor(0, 40);
display.println("& restart");
display.display();
// Keep outputs off during error state
digitalWrite(OUTPUT_PIN, LOW);
digitalWrite(ALARM_OUTPUT, LOW);
disableTimer1PWM();
}
void handleButtons() {
unsigned long currentMillis = millis();
// Handle Select Button
if (digitalRead(BUTTON_SELECT) == LOW && !selectPressed) {
selectPressed = true;
selectPressTime = currentMillis;
}
else if (digitalRead(BUTTON_SELECT) == HIGH && selectPressed) {
selectPressed = false;
drawProgressBar(settingMode); // Clear progress bar
if (currentMillis - selectPressTime >= LONG_PRESS_TIME) {
settingMode = !settingMode;
if (!settingMode) {
// Save to EEPROM if values changed
if (targetHumidity != prevTargetHumidity) {
EEPROM.put(0, targetHumidity);
prevTargetHumidity = targetHumidity;
}
if (highAlarmTemp != prevHighAlarmTemp) {
EEPROM.put(2, highAlarmTemp);
prevHighAlarmTemp = highAlarmTemp;
}
if (lowAlarmTemp != prevLowAlarmTemp) {
EEPROM.put(4, lowAlarmTemp);
prevLowAlarmTemp = lowAlarmTemp;
}
if (alarmEnabled != prevAlarmEnabled) {
EEPROM.put(6, (int)alarmEnabled);
prevAlarmEnabled = alarmEnabled;
}
}
} else if (settingMode) {
settingIndex = (settingIndex + 1) % 4;
}
}
// Show progress during long press
if (selectPressed && (currentMillis - selectPressTime > 500)) {
drawProgressBar(!settingMode); // Show progress toward next state
}
// Handle Up Button (Increase Value)
if (digitalRead(BUTTON_UP) == LOW && !upPressed && (currentMillis - lastButtonPress > DEBOUNCE_DELAY)) {
upPressed = true;
lastButtonPress = currentMillis;
if (settingMode) {
if (settingIndex == 0) targetHumidity = constrain(targetHumidity + 1, MIN_HUMIDITY, MAX_HUMIDITY);
else if (settingIndex == 1) highAlarmTemp = constrain(highAlarmTemp + 1, MIN_TEMP, MAX_TEMP);
else if (settingIndex == 2) lowAlarmTemp = constrain(lowAlarmTemp + 1, MIN_TEMP, MAX_TEMP);
else if (settingIndex == 3) alarmEnabled = !alarmEnabled;
}
}
else if (digitalRead(BUTTON_UP) == HIGH) {
upPressed = false;
}
// Handle Down Button (Decrease Value)
if (digitalRead(BUTTON_DOWN) == LOW && !downPressed && (currentMillis - lastButtonPress > DEBOUNCE_DELAY)) {
downPressed = true;
lastButtonPress = currentMillis;
if (settingMode) {
if (settingIndex == 0) targetHumidity = constrain(targetHumidity - 1, MIN_HUMIDITY, MAX_HUMIDITY);
else if (settingIndex == 1) highAlarmTemp = constrain(highAlarmTemp - 1, MIN_TEMP, MAX_TEMP);
else if (settingIndex == 2) lowAlarmTemp = constrain(lowAlarmTemp - 1, MIN_TEMP, MAX_TEMP);
else if (settingIndex == 3) alarmEnabled = !alarmEnabled;
}
}
else if (digitalRead(BUTTON_DOWN) == HIGH) {
downPressed = false;
}
}Loading
ssd1306
ssd1306