Two things on my motorbike really annoyed me; (1) the grip heaters had two settings: "too hot" and "too cold" and (2) I like to be able to see the time. So I mounted a small box with an LCD screen & a smaller OLED display visible through the clear lid - and inside I put an Arduino Pro Mini + a relay + a real time clock module + a power converter. Two buttons allow me to control the temperature (initial heat up + 16 heat levels) (and control the backlight) (info on the LCD screen) whilst the RTC time is displayed on the OLED display.
I've leaned heavily on the knowledge of others to accomplish my Arduino objectives and I wanted to try to give a little back.
As a programmer I (probably) suck, but I can say in my defence that (a) the following code works and (b) it contains the solutions to a surprising number of things I found challenging; things that I had to work out "what worked" and "what didn't" - and most importantly gain an understanding of "why".
I need to be clear that the vast majority of techniques that I've used were thought up by people far better at this than I am (some of their original comments are left in place); for their contributions I'm forever grateful - THANK YOU.
I've put the entire project code below in the hope that there's enough there for others to find solutions to some of the same issues I faced - and if there are any fellow motorcycle riders out there with heated grips that suck (like mine) then this may be all you need to get your fingers toasty warm (and give you the current time on a separate OLED display) ![]()
The code contains solutions to the following things that I struggled with:
- Using an LCD display with the I2C protocol
- Using an OLED display with the I2C protocol
- Getting the current time from a RTC (ie "all 3 at once")
- Using interrupts to handle switch activations - including debouncing (what a journey that was!)
- Handling different switch press durations to control different modes
Happy to receive any constructive criticism (and yes - I fully appreciate that there are a zillion ways to code anything).
Most importanly of all, I hope that someday someone finds a real-world solution to a problem they're having here (that's why I posted the entire project code -- so someone can see how it all hangs together).
Many thanks to all those who helped me bring this project to completion; my fingers thank you!
/* This program controls the heating of my motorbike handlebars by switching a relay off and on.
*
* It powers up in "auto" mode where it powers the heaters at full power for 4 1/2 min to get things warmed up before switching to "manual" mode, starting at 6/16 power.
*
* It powers up in "day" mode where the LCD backlight stays on.
*
* Any momentary switch activity during "auto" mode immediately switches the unit to "manual" mode.
*
* Any switch held down for more than 1 second toggles day/night mode (for LCD backlight control).
*
* In night mode the LCD backlight remains off until a button is pressed - at which point it turns the backlight on for 3 seconds.
*
* The current time is read from an RTC module & displayed on the OLED display.
*
* The easiest way to set the reset the time if occasional adjustments are needed for drift or daylight savings changes is to simply remove the backup battery - power-up the unit at midnight - then reinsert the battery (the date isn't used).
*
* Be aware that there's a very common RTC module with a serious fault in that it tries to charge the 2032 battery (and tries to overcharge a rechargeable 2032 battery if powered at 5V) - be sure to remove the 201 ohm resister to disable the charging
* circuit if using one of these.
*
* The main loop completes in about 60 mS. I did it this way originally so that key presses could be detected and the LCD screen updated in a timely manner, and although I've subsequently re-written the sketch to (rightly or wrongly) use interrupts
* to handle button presses, I've stuck with this 60mS loop speed approach. Although it would be possible to turn the heaters off and on using fast loop speeds it would wear out the relay - so every "heat state period" actually consists 10 passes through the
* main loop for that state - and in the end I found it easier to just treat these values as being 10 times the size inside the program that we think of them as when using the unit eg setting "******" on the display would turn on the heaters for 6 periods out of 16 but internally
* the program uses "60" instead of "6"" for the "on time". It's also the reason (for example) increasing the heater power setting actually adds "10" not "1" to that variable. If I hadn't done it this way the heaters would still have been fine
* but the LCD screen updates would be very sluggish because they'd only get to do their thing about once a second. So every cycle of "on time" plus "off time" (always a total of 16 time periods x 10) completes in about 10 seconds - which works well for the grip heaters
* and also to control my electric blanket!
*/
#include <Wire.h> // I2C library
#include <LiquidCrystal_I2C.h> // LCD library
#include <Adafruit_SSD1306.h> // OLED library
#include "RTClib.h" // RTC library
const int SCREEN_WIDTH = 128; // OLED display width, in pixels
const int SCREEN_HEIGHT = 32; // OLED display height, in pixels
const int OLED_RESET = 4; // Reset pin # (or -1 if sharing Arduino reset pin)
const int SCREEN_ADDRESS = 0x3C; // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
const int downSwitch = 2; // Connected to down switch
const int upSwitch = 3; // Connected to up switch
const int heaterControl = 4; // Connected to relay
const int powerLevels = 16; // 16 Power levels + 0 (off)
int Power = 6 * 10; // Define heater power variable and set to 60 out of 160 (37.5% duty cycle)
volatile int modeTimer = 270 * 10; // Run in auto mode for 2700 x 1/10th sec (4 min & 30 seconds). When we get to Zero (or below) we drop into manual mode (and stay there)
volatile int loopCount = powerLevels * 10; // 16 heat levels (plus off) x 1/10th sec per iteration
int onTime; // Initial on time (in 60mS increments) (value set later)
int offTime; // Initial off time (in 60mS increments) (value set later)
int displayPower = Power / 10; // Initial power level displayed on the LCD
volatile unsigned long lightOffTime = 0; // Backlight timer turn-off time (set by the ISRs)
volatile bool nightMode = 0; // Backlight on if day mode (0); backlight off if night mode (1) (Set by the ISRs)
const int releaseTime = 500; // Key releaseTime buffer / key release period - in milliseconds. A momentary key press needs to be less than this in duration to be counted for a heat level change
const int lcdTimeout = 3 * 1000; // How long to leave the backlight on for after a key press at night (in seconds) (converted to milliseconds)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // Define interfaces
RTC_DS3231 rtc; //
LiquidCrystal_I2C lcd = LiquidCrystal_I2C(0x27, 16, 2); //
// SETUP CODE
void setup() // Setup code starts here (runs once)
{
attachInterrupt(digitalPinToInterrupt(downSwitch), downSwitchISR, LOW); // Attach ISRs to handle switch activation interrupts
attachInterrupt(digitalPinToInterrupt(upSwitch), upSwitchISR, LOW);
pinMode(heaterControl, OUTPUT); // Set heater relay control pin as output
pinMode(upSwitch, INPUT_PULLUP); // Set up switch pin as input
pinMode(downSwitch, INPUT_PULLUP); // Set down switch pin as input
Serial.begin(57600); // Initialise serial interface
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS); // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
display.setTextSize(5); // Big numbers
display.setTextColor(SSD1306_WHITE); // White text
lcd.init(); // Initialise LCD
lcd.backlight(); // Backlight on
lcd.print("Auto Mode *"); // Display "Auto Mode *"
lcd.setCursor(0,1); // Reposition cursor
lcd.print("Grips Warming Up"); // Indicate full power
}
// MAIN CODE
void loop() // Main code starts here
{
// Auto Mode Section
while (modeTimer > 0) // While in auto mode just count down with heaters on & update OLED clock
{
updateOLED(); // Show time on OLED
digitalWrite(heaterControl, HIGH); // Turn heater on
backlightControl(); // Ensure backlight is in correct mode - as night mode can be turned on even while in (and without affecting) auto mode
delay (100); // Wait 100 milliseconds (1/10th second)
modeTimer--; // Decrement the timer count
} // And go around the loop until timer is at or below zero (either counted down or set by an ISR)
// Manual mode starts here
onTime = Power; // Set on_time & off_time counters (in 60mS increments)
offTime = (powerLevels * 10) - Power; // Off Time is 160 - On Time so the two always add up to 160 (16 states x 10 passes through the loop per state)
lcd.clear(); // Clear the screen
lcd.print("Manual Mode"); // Now in manual mode
updateDisplay(); // Update bottom line of LCD display
while (loopCount > 0) // Process 10x 60mS states (ie "just under 1 second") 16 times (one for each of the possible heat levels)
{
updateOLED(); // Update time display
if (onTime > 0) // Let's deal with the time-on portion:
{
digitalWrite(heaterControl, HIGH); // Turn on the relay
onTime--; // Decrement the counter by approx 60mS
lcd.setCursor(15,0); // Position cursor to top right
if (nightMode == 1) // If we're in night mode ...
{
lcd.print("N"); // ... then print an "N" to indicate heater is on and we're in night mode
}
else // Otherwise must be in day mode ...
{
lcd.print("*"); // ... so just print the regular "*" in the top right-hand corner
}
}
else // Now let's deal with the time-off portion:
{
if (offTime > 0) // for as long as time off hasn't reached zero:
{
digitalWrite(heaterControl, LOW); // Turn off the relay
offTime--; // Decrement the counter by approx 60mS
lcd.setCursor(15,0); // Position cursor to top right
lcd.print(" "); // Clear the "*" heater on indicator
}
}
backlightControl(); // Try to turn off the LCD backlight if not needed (this will only succeed in night mode)
delay(50); // Wait 50 milliseconds - this sets the overall speed of the timing loop in normal operation - approx 10 sec for complete set of 160 iterations
loopCount--; // Decrement the loop count
} // End of heat cycle loop
loopCount = powerLevels * 10; // 160 iterations done ... so reset the counter and get back into the loop
} // End of Main Code Loop
// Subroutines
void updateDisplay() // Update the LCD display
{
lcd.setCursor(0,1); // Cursor to bottom left
lcd.print(" "); // Clear bottom row
lcd.setCursor(0,1); // Move cursor back to beginning of bottom row
for (displayPower = Power / 10; displayPower > 0; displayPower--) // Display correct number of stars on LCD
{
lcd.print("*"); // - Print a "*" and advance the cursor
} //
}
void backlightControl() // Turn off backlight if night mode is true
{
if (nightMode == 1) // If in night mode
{
if (millis() < lightOffTime) // and we haven't reached light off time then ...
{
lcd.backlight(); // turn backlight on
}
else // Otherwise we must have reached lights off time
{
lcd.noBacklight(); // so turn backlight off
}
}
if (nightMode == 0) // If not in night mode
{
lcd.backlight(); // Turn on backlight
}
}
void updateOLED() // Write current time to OLED display
{
DateTime now = rtc.now(); // Get the current time from the RTC
display.clearDisplay(); // Clear the display so we don't get artifacts
display.setCursor(0,0); // Start at top-left corner
if (now.hour() < 10) // Leading "0" required before hour?
{
display.println("0"); // If so then write it to the OLED buffer and ...
display.setCursor(32,0); // Move the cursor for the next digit
}
display.println(now.hour(), DEC); // Write the current hour to the OLED buffer
display.setCursor(66,0); // Setup for correct minutes position
if (now.minute() < 10) // Leading "0" required before minute?
{
display.println("0"); // If so then write it to the OLED buffer
display.setCursor(98,0); // and move the cursor for the next digit
}
display.println(now.minute(), DEC); // Write the current minute to the buffer
display.display(); // Display the buffer contents on the OLED
}
// Interrupt Service Routines
void downSwitchISR() // This gets run whenever the normally-high pin 3 (up switch) is grounded by the switch closing
{
static unsigned long lastInterruptTime = 0; // Setup for key bounce handling
unsigned long interruptTime = millis(); //
if (interruptTime - lastInterruptTime > 150) // If it's been at least 150 mS since last interrupt then proceed
{
lightOffTime = millis() + lcdTimeout; // Work out what time the backlight needs to be turned off
for (int16_t delayInISR = releaseTime; delayInISR > 0; delayInISR--) // Can't use delay() in ISRs - so this loops 1000 microsecond delays the specified number of times
{
delayMicroseconds(1000); //
}
if (digitalRead(downSwitch) == HIGH) // If the switch has been released after our 1/4 second wait it was just a momentary press - so process as heat level change
{
modeTimer = 0; // Signal end of auto mode (may or may not have been in that mode at this time)
if (Power > 0) // Increase heat level - but only if not already at maximum
{
Power -= 10; // Increase heater power
loopCount = 0; // Reset the timing loop so it takes effect straight away
}
}
else // Switch must be down for more than 1/4 second - so a toggle night mode might be required
{
for (int16_t delayInISR = 1000; delayInISR > 0; delayInISR--) // Wait 1 second
{
delayMicroseconds(1000); //
}
if (digitalRead(downSwitch) == LOW) // Switch down for 1 second or more?
{
nightMode = !nightMode; // Toggle night mode flag
}
}
}
lastInterruptTime = interruptTime; // Document last interrupt time for debounce code
}
void upSwitchISR() // This gets run whenever the normally-high pin 3 (up switch) is grounded by the switch closing
{
static unsigned long lastInterruptTime = 0; // Setup for key bounce handling
unsigned long interruptTime = millis(); //
if (interruptTime - lastInterruptTime > 150) // If it's been at least 150 mS since last interrupt then proceed
{
lightOffTime = millis() + lcdTimeout; // Work out what time the backlight needs to be turned off
for (int16_t delayInISR = releaseTime; delayInISR > 0; delayInISR--) // Can't use delay() in ISRs - so this loops 1000 microsecond delays the specified number of times
{
delayMicroseconds(1000); //
}
if (digitalRead(upSwitch) == HIGH) // If the switch has been released after our 1/4 second wait it was just a momentary press - so process as heat level change
{
modeTimer = 0; // Signal end of auto mode (may or may not have been in that mode at this time)
if (Power < powerLevels * 10) // Increase heat level - but only if not already at maximum
{
Power += 10; // Increase heater power
loopCount = 0; // Reset the timing loop so it takes effect straight away
}
}
else // Switch must be down for more than 1/4 second - so a toggle night mode might be required
{
for (int16_t delayInISR = 1000; delayInISR > 0; delayInISR--) // Wait 1 seconds
{
delayMicroseconds(1000); //
}
if (digitalRead(upSwitch) == LOW) // Switch down for 1 second or more?
{
nightMode = !nightMode; // Toggle night mode flag
}
}
}
lastInterruptTime = interruptTime; // Document last interrupt time for debounce code
}