// ************ Overengineered Fan Control ****************
// 2024 / WW
#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
#include <EEPROM.h>
// Display
#define DISPLAY_I2C_ADDRESS 0x3C // I2C Addresse
#define DISPLAY_WIDTH 128 // Breite in Pixeln
#define DISPLAY_HEIGHT 32 // Höhe in Pixeln
Adafruit_SSD1306 display(DISPLAY_WIDTH, DISPLAY_HEIGHT, &Wire, -1);
#define VER "V0.9/WW"
// Global Variables
char input = '0'; // Serial input character
int PWM = 0; // Lüfter PWM
int inc = 10; // PWM increment
bool SerialOut = false; // Serial output
bool IgnoreFallPlus = false;
bool IgnoreFallMinus = false;
bool SettingsActive = false;
volatile bool ScreenOn = true;
int SettingsItem = 10; // Code for setting menue structure: base Menu first item 10, second 11, 12... sublevel 20, 21, 22...
volatile bool KeyPlusRise = false;
volatile bool KeyPlusFall = false;
volatile unsigned long tStampPR;
volatile unsigned long tStampPF;
volatile unsigned long tStampMR;
volatile unsigned long tStampMF;
volatile bool KeyMinusRise = false;
volatile bool KeyMinusFall = false;
bool longP = false;
bool longM = false;
volatile unsigned long LastIRplus;
volatile unsigned long LastIRminus;
volatile int LastStatePlus = LOW;
volatile int LastStateMinus = LOW;
unsigned long LastKeyPress;
unsigned long DTimeout = 5; // Display timeout in seconds
const unsigned long DebounceTime = 30; // Debounce time lockout in milliseconds
const unsigned long LongpressTime = 1200; // Time for long keypress in milliseconds
// Function Prototypes
void SerialMonitor ();
void ISRPLUS();
void ISRMINUS();
char KeyPress();
void DisplayMainScreen();
void DisplaySettings();
void DisplayTimeout();
void DPrintC(String, int);
void setup()
{
// Set Timer 1 to 25 khz PWM
noInterrupts();
TCCR1A =
1 << COM1B1 |
1 << WGM11 |
1 << WGM10;
TCCR1B =
1 << WGM13 |
1 << CS10;
DDRB =
1 << DDB2;
OCR1A = 320;
OCR1B = 0;
interrupts();
pinMode(2, INPUT_PULLUP); // PLUS Pushbutton
pinMode(3, INPUT_PULLUP); // MINUS Pushbutton
attachInterrupt(digitalPinToInterrupt(2), ISRPLUS, CHANGE);
attachInterrupt(digitalPinToInterrupt(3), ISRMINUS, CHANGE);
Serial.begin(9600);
// Startmessages on Serial
Serial.println();
Serial.print("---------- Overengineered Fan Control ");
Serial.print(VER);
Serial.println(" ----------");
// Display connection test
Wire.begin();
Wire.beginTransmission(DISPLAY_I2C_ADDRESS);
if (Wire.endTransmission() > 0) {
Serial.println(F("OLED display not found"));
} else {
Serial.println(F("OLED display found"));
}
Serial.println(F("S for Serial Monitoring"));
Wire.end();
display.begin();
// Startscreen
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(22, 0);
display.print(F("Overengineered"));
display.setCursor(34, 10);
display.print(F("Fan Control"));
display.setCursor(45, 25);
display.print(VER);
display.drawFastHLine(0,21,128, WHITE);
display.display();
delay(2000);
}
void loop()
{
if (Serial.available() > 0) {
input = Serial.read();
} else {
input = KeyPress(); // Read button states
}
if (!SettingsActive && ScreenOn){
switch (input) {
case '+':
PWM = PWM + inc;
break;
case '-':
PWM = PWM - inc;
break;
case '1':
inc = 1;
break;
case '2':
inc = 10;
break;
case 'L':
PWM = 0;
break;
case 'H':
PWM = 100;
break;
case 'S':
SerialOut = !SerialOut;
break;
case 'M':
SettingsActive = true;
break;
default:
break;
}
}
// Clamp to 0..100%
if (PWM > 100){
PWM = 100;
}
if (PWM < 0){
PWM = 0;
}
OCR1B = map(PWM,0,100,0,320); // Project PWM to timer register values and set fan PWM
if (SettingsActive ==true) {
DisplaySettings();
}
else {
DisplayMainScreen();
DisplayTimeout();
}
display.display();
// Serial Output
if ((SerialOut == true) && (input != '0')) {
SerialMonitor();
}
input = '0';
delay(100);
}
// Serial monitor
void SerialMonitor()
{
// Serial.print("\r");
// Serial.print(" ");
/// Serial.print("\r");
Serial.print("PWM: ");
Serial.print(PWM);
Serial.println("%");
}
// Key interrupt PLUS
void ISRPLUS()
{
if (millis() - LastIRplus > DebounceTime) {
if ((digitalRead(2) == LOW) && (LastStatePlus == LOW)) {
KeyPlusRise = true;
tStampPR = millis();
LastStatePlus = HIGH;
}
else {
KeyPlusFall = true;
tStampPF = millis();
LastStatePlus = LOW;
}
LastIRplus = millis();
}
}
// Key interrupt MINUS
void ISRMINUS()
{
if (millis() - LastIRminus > DebounceTime) {
if ((digitalRead(3) == LOW) && (LastStateMinus == LOW)) {
KeyMinusRise = true;
tStampMR = millis();
LastStateMinus = HIGH;
}
else {
KeyMinusFall = true;
tStampMF = millis();
LastStateMinus = LOW;
}
LastIRminus = millis();
}
}
// Keypress Handling
char KeyPress()
{
if (KeyPlusFall && !IgnoreFallPlus) {
KeyPlusFall = false;
return '+';
}
if (KeyPlusFall && IgnoreFallPlus) {
IgnoreFallPlus = false;
KeyPlusFall = false;
}
if ((digitalRead(2) == LOW) && (KeyPlusRise == true)) {
if (millis() - tStampPR > LongpressTime) {
IgnoreFallPlus = true;
longP = true;
KeyPlusRise = false;
}
}
if (KeyMinusFall && !IgnoreFallMinus) {
KeyMinusFall = false;
return '-';
}
if (KeyMinusFall && IgnoreFallMinus) {
IgnoreFallMinus = false;
KeyMinusFall = false;
}
if ((digitalRead(3) == LOW) && (KeyMinusRise == true)) {
if (millis() - tStampMR > LongpressTime) {
IgnoreFallMinus = true;
longM = true;
KeyMinusRise = false;
}
}
if ((digitalRead(2) == HIGH) && longM) {
longM = false;
return 'L';
}
if (longP && (digitalRead(3) == HIGH)) {
longP = false;
return 'H';
}
if(longP && longM) {
longM = false;
longP = false;
return 'M';
}
return '0';
}
// Main Screen
void DisplayMainScreen()
{
display.clearDisplay();
display.setCursor(10, 12);
display.print(PWM);
display.print(" %");
}
// Settings Screen
void DisplaySettings()
{
const int Ty1 = 1; //Y position of headline
const int Ty2 = 16; // Y position of second line
String TextLine1 = "Settings";
String TextLine2;
display.clearDisplay();
display.drawFastHLine(0,10,128,WHITE);
switch (SettingsItem) {
case 10:
TextLine2 = "Sleep Timer";
switch (input) {
case '+':
SettingsItem++;
break;
case '-':
SettingsItem = 20;
break;
}
break;
case 20:
TextLine1 = "Sleep Timer";
if (DTimeout == 0) {
TextLine2 = "OFF";
} else {
TextLine2 = String(DTimeout) + " sec";
}
switch (input) {
case '+':
DTimeout += 5;
if (DTimeout > 30) {DTimeout = 0;}
break;
case '-':
SettingsItem = 10;
break;
}
break;
case 11:
TextLine2 = "Display Time Out";
switch (input) {
case '+':
SettingsItem++;
break;
}
break;
case 12:
TextLine2 = "Fan Settings";
switch (input) {
case '+':
SettingsItem++;
break;
}
break;
case 13:
TextLine2 = "Info";
switch (input) {
case '+':
SettingsItem++;
break;
}
break;
case 14:
TextLine2 = "< - Exit";
switch (input) {
case '-':
SettingsActive = false;
SettingsItem = 10;
break;
case '+':
SettingsItem = 10;
break;
}
break;
}
DPrintC(TextLine1, Ty1);
DPrintC(TextLine2, Ty2);
}
// Display timeout / Powersave
void DisplayTimeout()
{
if (LastIRplus > LastIRminus) {
LastKeyPress = LastIRplus;
}
else {
LastKeyPress = LastIRminus;
}
if (((millis() - LastKeyPress) > DTimeout * 1000) && (DTimeout != 0)) {
ScreenOn = false;
display.clearDisplay();
IgnoreFallMinus = true;
IgnoreFallPlus = true;
} else {
ScreenOn = true;
}
}
// Print text x-centered at y coordinate
void DPrintC(String strg, int y)
{
int x = (DISPLAY_WIDTH - (strg.length()) * 6) / 2;
display.setCursor(x, y);
display.print(strg);
}