Welcome!

By registering with us, you'll be able to discuss, share and private message with other members of our community.

SignUp Now!

Motorcycle Timer Device for “Dead Red” law

Iskweldog

New member
Joined
Nov 7, 2024
Messages
2
I’m an avid motorcyclist based in Illinois, and I recently came across your work at HackMakeMod. I’m reaching out because I have a specific need that I think your expertise could help with.

In Illinois, there’s a law allowing motorcyclists to proceed through a red light if they’ve been stopped for over 120 seconds, due to the common issue of traffic signals failing to detect motorcycles. To help with this, I’ve been looking for a small, straightforward device: a timer that could count to 120 seconds and be easily mounted to my motorcycle’s handlebars.

Ideally, I’d like this device to have a single button to initiate the countdown, with a design that works even when wearing bulky gloves. A power source via USB or battery would be perfect, along with an on/off switch.

Given your background in creating practical, DIY-friendly electronics, I thought you might be able to help design or suggest a solution. Let me know if this is something you’d be interested in discussing!
 
This sounds like a great project, especially with Illinois’ red-light law for motorcyclists. A simple handlebar-mounted timer with a big glove-friendly button, USB or battery power, and an on/off switch...

Here’s how I’d approach it:​

Parts:
  • Microcontroller (like an Arduino Nano or ESP32) for the countdown.
  • Big Button that’s easy to press with gloves.
  • Display to show the timer.
  • Power Supply – either rechargeable battery or USB.

Design Considerations:
  • Weatherproofing for outdoor durability, like a small, sealed case.
  • Mounting with a clamp or maybe a custom 3D-printed bracket.

Coding:
  • We’d set up simple code to start a 120-second countdown with a button press, resetting after the time runs out.

Quick Questions to Fine-Tune This:​

  1. Are you wanting something you can build yourself?
  2. What skills are you interested in using or learning? 3D printing, soldering, coding?
  3. Power preference: would USB work, or would you rather run it on 3 AAA batteries?
  4. how big do you want it?
  5. Could you share a pic of where you’d want it mounted?
Once I have these details, I can help with specific part suggestions and assembly steps.
 
Q: Are you wanting something you can build yourself?
A: Yes, something I can build or anyone with access to basic electronics skills can build for themselves.

Q: What skills are you interested in using or learning? 3D printing, soldering, coding?
A: I have basic 3D printing skills and soldering skills. I need help with electronics design and coding.

Q: Power preference: would USB work, or would you rather run it on 3 AAA batteries?
A: I think USB works best since most modern bikes have a USB port to charge phones, navigation devices, etc. Additionally, this would allow it to turn on/off with the motorcycle.

Q: How big do you want it?
A: Size is less relevant as long as it fits on the handlebars and doesn't interfere with the motorcycle's operation.

Q: Could you share a pic of where you’d want it mounted?
A: Every motorcycle is different but for my bike, a 2024 Triumph Speet Triple 765 RS, I'm thinking I want it clamped on the left handlebar, which I've measured to be 22mm.
street-triple-765-rs-cockpit.jpg
 
I wrote some code and made a wiring diagram. I tested everything and it works great! Let me know if you need anything else!
StopLightTimer-Diagram-01.png


This code controls a 120-second countdown timer displayed on an OLED screen, designed for use on a motorcycle. Here’s a summary of its functionality:
  1. Button-Controlled Countdown: When the button is pressed, it starts or restarts a 120-second countdown, showing the time remaining in large digits on the OLED display.
  2. Idle Animation: While waiting, the display shows a "breathing" circle animation that grows and shrinks to indicate the timer is ready to use.
  3. Countdown Completion: Once the countdown reaches zero, the code displays a flashing "GO!" logo (provided as a bitmap image) for 10 seconds, alerting the rider that the waiting time has passed.
  4. Loop Structure: The code continuously checks for button presses, updates the countdown every second, and handles the flashing "GO!" animation if the countdown has finished.
  5. Debouncing and Flash Timing: Debounce logic ensures reliable button presses, and timing intervals manage the countdown and flashing animations accurately.

C++:
// 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();
}
 
Last edited:
Back
Top