// HackMakeMod StopLight Timer
// 120-second countdown timer for motorcyclists at traffic lights that don’t detect bikes.
// After pressing a button, the timer counts down, flashing "GO!" when 120 seconds have passed
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Fonts/FreeSansBold24pt7b.h> // Include thicker font
// Define OLED display dimensions
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
// Initialize the OLED display using I2C
// SDA: Connect to D2 (GPIO4)
// SCL: Connect to D1 (GPIO5)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// Button pin (you can change this to any available GPIO pin)
const int buttonPin = D7; // GPIO13
// Countdown variables
const int countdownSeconds = 120; // Counts down from 120 seconds
int remainingSeconds = countdownSeconds;
bool countdownActive = false;
// "GO!" display variables
bool goActive = false;
unsigned long goStartTime = 0;
unsigned long goFlashInterval = 200; // Flash every 200ms
unsigned long lastGoFlashTime = 0;
bool goVisible = true; // To toggle the visibility of "GO!"
// Timing variables
unsigned long previousMillis = 0;
const long interval = 1000; // 1 second
// Breathing circle variables
int circleRadius = 15;
int circleRadiusMin = 10;
int circleRadiusMax = 20;
int circleRadiusStep = 1;
bool circleGrowing = true;
unsigned long previousBreathMillis = 0;
const long breathInterval = 50; // Update every 50ms
// Button state variables
bool buttonState = HIGH; // Current state of the button
bool lastButtonState = HIGH; // Previous state of the button
unsigned long lastDebounceTime = 0; // The last time the button state changed
const unsigned long debounceDelay = 50; // Debounce time in milliseconds
void setup() {
// Initialize serial communication (optional)
Serial.begin(115200);
// Initialize I2C with custom pins
Wire.begin(0, 2); // SDA on GPIO 0, SCL on GPIO 2
pinMode(D5, OUTPUT); // Set GPIO pin D5 as an output
digitalWrite(D5, LOW); // Set D5 to LOW, simulating GND for the button
// Initialize button input with internal pull-up resistor
pinMode(buttonPin, INPUT_PULLUP);
// Initialize the OLED display
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x64
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Don't proceed, loop forever
}
// Set maximum brightness
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(0xFF);
// Clear the buffer
display.clearDisplay();
display.display();
}
void loop() {
// Read the button state
int reading = digitalRead(buttonPin);
// Check for button state change (debounce)
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
// If the button state has been stable for debounceDelay, take it as the actual state
if ((millis() - lastDebounceTime) > debounceDelay) {
// If the button state has changed
if (reading != buttonState) {
buttonState = reading;
// If the button is pressed (LOW because of internal pull-up)
if (buttonState == LOW) {
// Start/restart the countdown
countdownActive = true;
goActive = false; // Stop "GO!" display if it's active
remainingSeconds = countdownSeconds;
previousMillis = millis();
displayCountdown();
}
}
}
// Save the current reading for the next loop
lastButtonState = reading;
if (countdownActive) {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
if (remainingSeconds > 1) {
remainingSeconds--;
displayCountdown();
} else {
countdownActive = false;
goActive = true;
goStartTime = millis();
lastGoFlashTime = millis();
goVisible = true;
}
}
} else if (goActive) {
// Handle "GO!" flashing
unsigned long currentMillis = millis();
if (currentMillis - goStartTime < 10000) { // Display "GO!" for 10 seconds
if (currentMillis - lastGoFlashTime >= goFlashInterval) {
lastGoFlashTime = currentMillis;
goVisible = !goVisible; // Toggle visibility
display.clearDisplay();
if (goVisible) {
// Display "GO!"
display.setFont(&FreeSansBold24pt7b); // Set to thicker font
display.setTextSize(1); // Adjust text size for this font
display.setTextColor(SSD1306_WHITE);
// Calculate position to center the text
String goStr = "GO!";
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(goStr, 0, 0, &x1, &y1, &w, &h);
int x = (SCREEN_WIDTH - w) / 2;
int y = (SCREEN_HEIGHT - h) / 2 + 35;
display.setCursor(x, y);
display.print(goStr);
}
display.display();
}
} else {
// "GO!" display time is over
goActive = false;
display.clearDisplay();
display.display();
}
} else {
// Display breathing circle
unsigned long currentBreathMillis = millis();
if (currentBreathMillis - previousBreathMillis >= breathInterval) {
previousBreathMillis = currentBreathMillis;
updateBreathingCircle();
}
}
}
// Function to display the countdown timer
void displayCountdown() {
display.clearDisplay();
// Set text properties
display.setFont(&FreeSansBold24pt7b); // Set to thicker font
display.setTextSize(1); // Adjust text size for this font
display.setTextColor(SSD1306_WHITE);
// Calculate position to center the text
String numStr = String(remainingSeconds);
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(numStr, 0, 0, &x1, &y1, &w, &h);
int x = (SCREEN_WIDTH - w) / 2;
int y = (SCREEN_HEIGHT - h) / 2 + 35;
display.setCursor(x, y);
display.print(numStr);
display.display();
}
// Function to update the breathing circle
void updateBreathingCircle() {
// Update circle radius
if (circleGrowing) {
circleRadius += circleRadiusStep;
if (circleRadius >= circleRadiusMax) {
circleRadius = circleRadiusMax;
circleGrowing = false;
}
} else {
circleRadius -= circleRadiusStep;
if (circleRadius <= circleRadiusMin) {
circleRadius = circleRadiusMin;
circleGrowing = true;
}
}
// Clear display
display.clearDisplay();
// Draw circle at center
display.fillCircle(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, circleRadius, SSD1306_WHITE);
// Update display
display.display();
}