#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ezButton.h>
#include <TimedAction.h>
const int SHORT_PRESS_TIME = 250;
const int LONG_PRESS_TIME = 500;
//GPIO declarations
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
byte segmentClock = 6; //Yellow
byte segmentLatch = 5; //Brown
byte segmentData = 7; //Blue
byte alarmInPin = 8;
byte alarmOutPin = 9; //Green
byte timeAddPin = 10;
byte timeMinusPin = 11;
byte startStopPin = 12;
byte resetPin = 13;
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
//Button Declarations
ezButton btnAlarm(alarmInPin);
ezButton btnTimeAdd(timeAddPin);
ezButton btnTimeMinus(timeMinusPin);
ezButton btnStartStop(startStopPin);
ezButton btnReset(resetPin);
enum runningState {
STOPPED,
PREPARE,
RUNNING,
};
enum alarmState {
ACTIVE,
INACTIVE,
};
//Global Variables
int countdown = 120; //keeps the qualifier clock value. Only changes when user sets clock time
int runclk = 0; //keeps the end clock value. Decrements when the clock is running
int prepclk = 10; //keeps the 10s prep clock value. Decrements when the clock is running
int alarmCount; //number of beeps for the alarm cycle
alarmState alarmStatus = INACTIVE;
runningState runStatus = STOPPED;
//Forward Function Declarations
void updateTime();
void runAlarm();
void button_check(void);
void showNumber(float value);
void centerText(String text);
//TimedAction Declarations
TimedAction timerAction = TimedAction(1000,updateTime);
TimedAction alarmAction = TimedAction(250,runAlarm);
//GFX Display
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
Serial.println("Archery Countdown Timer");
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
pinMode(segmentClock, OUTPUT);
pinMode(segmentData, OUTPUT);
pinMode(segmentLatch, OUTPUT);
pinMode(alarmOutPin, OUTPUT);
btnAlarm.setDebounceTime(50);
btnTimeAdd.setDebounceTime(50);
btnTimeMinus.setDebounceTime(50);
btnStartStop.setDebounceTime(50);
digitalWrite(segmentClock, LOW);
digitalWrite(segmentData, LOW);
digitalWrite(segmentLatch, LOW);
display.clearDisplay(); //Adafruit splash logo is in initial buffer, Clear it.
display.setCursor(0,0);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
centerText("El Dorado Archers");
centerText("Archery Timer");
centerText("Developed by:");
centerText("Micah Ting");
display.display();
delay(2000);
showNumber(countdown);
Serial.println("State is STOPPED");
}
void loop() {
button_check();
timerAction.check();
alarmAction.check();
}
/* Process buttons in order of importance:
1. Alarm
2. Start/Stop
3. Timer adjust
Timer adjust will only function when the state machine is stopped.
*/
void button_check(void)
{
long pressDuration;
btnAlarm.loop();
btnTimeAdd.loop();
btnTimeMinus.loop();
btnStartStop.loop();
btnReset.loop();
/* Check and act on alarm button */
if (btnAlarm.isPressed()) {
btnAlarm.pressTime = millis();
btnAlarm.isPressing = true;
btnAlarm.isLongDetected = false;
}
if (btnAlarm.isReleased()) {
btnAlarm.isPressing = false;
btnAlarm.releaseTime = millis();
pressDuration = btnAlarm.releaseTime - btnAlarm.pressTime;
if (pressDuration < SHORT_PRESS_TIME)
if (!btnAlarm.isLongDetected)
{
alarmCount = 3;
runStatus = STOPPED;
}
}
if (btnAlarm.isPressing && !btnAlarm.isLongDetected) {
pressDuration = millis() - btnAlarm.pressTime;
if (pressDuration > LONG_PRESS_TIME) {
btnAlarm.isLongDetected = true;
}
}
//actively pressing and long press is true
if (btnAlarm.isPressing && btnAlarm.isLongDetected) {
digitalWrite(alarmOutPin, HIGH); //5 whistle blast and reset everything
runStatus = STOPPED;
} else if (!btnAlarm.isPressing && btnAlarm.isLongDetected) {
digitalWrite(alarmOutPin, LOW);
showNumber(countdown);
alarmCount = 0;
btnAlarm.isLongDetected = false; //btn released, alarm is stopped, reset long state
}
/* Check and act on start/stop button */
if (btnStartStop.isPressed()) {
btnStartStop.pressTime = millis();
btnStartStop.isPressing = true;
btnStartStop.isLongDetected = false;
}
if (btnStartStop.isReleased()) {
btnStartStop.isPressing = false;
btnStartStop.releaseTime = millis();
pressDuration = btnStartStop.releaseTime - btnStartStop.pressTime;
if (pressDuration < SHORT_PRESS_TIME)
{
switch (runStatus)
{
case STOPPED:
runStatus = PREPARE;
runclk = countdown; //set the running timer
prepclk = 10;
alarmCount = 2;
Serial.print("State is PREPARE. Timer is set to ");
Serial.println(runclk);
break;
case PREPARE:
runStatus = STOPPED;
Serial.println("State is STOPPED");
break;
case RUNNING:
runStatus = STOPPED;
Serial.println("State is STOPPED");
break;
}
}
}
if (runStatus == STOPPED) //only allow timer adjustments if we're in STOPPED state
{
/* Check and act on time plus button */
if (btnTimeAdd.isPressing && btnTimeAdd.isLongDetected) { //handle long press
countdown = (countdown + 10 >= 990 ? 990 : countdown + 10);
showNumber(countdown);
delay(100);
}
if (btnTimeAdd.isPressed()) {
btnTimeAdd.pressTime = millis();
btnTimeAdd.isPressing = true;
btnTimeAdd.isLongDetected = false;
}
if (btnTimeAdd.isReleased()) {
btnTimeAdd.isPressing = false;
btnTimeAdd.releaseTime = millis();
pressDuration = btnTimeAdd.releaseTime - btnTimeAdd.pressTime;
if (pressDuration < SHORT_PRESS_TIME) {
countdown = (countdown + 10 >= 990 ? 990 : countdown + 10);
showNumber(countdown);
}
}
if (btnTimeAdd.isPressing && !btnTimeAdd.isLongDetected) {
pressDuration = millis() - btnTimeAdd.pressTime;
if (pressDuration > LONG_PRESS_TIME) {
btnTimeAdd.isLongDetected = true;
}
}
/* Check and act on time minus button */
if (btnTimeMinus.isPressing && btnTimeMinus.isLongDetected) {
countdown = (countdown - 10 <= 10 ? 10 : countdown - 10);
showNumber(countdown);
delay(100);
}
if (btnTimeMinus.isPressed()) {
btnTimeMinus.pressTime = millis();
btnTimeMinus.isPressing = true;
btnTimeMinus.isLongDetected = false;
}
if (btnTimeMinus.isReleased()) {
btnTimeMinus.isPressing = false;
btnTimeMinus.releaseTime = millis();
pressDuration = btnTimeMinus.releaseTime - btnTimeMinus.pressTime;
if (pressDuration < SHORT_PRESS_TIME) {
countdown = (countdown - 10 <= 10 ? 10 : countdown - 10);
showNumber(countdown);
}
}
if (btnTimeMinus.isPressing && !btnTimeMinus.isLongDetected) {
pressDuration = millis() - btnTimeMinus.pressTime;
if (pressDuration > LONG_PRESS_TIME) {
btnTimeMinus.isLongDetected = true;
}
}
}//End runStatus == STOPPED
//Handle Reset button. Reset state, display, kill alarm, etc
if (btnReset.isPressed()) {
runStatus = STOPPED;
showNumber(countdown);
alarmCount = 0;
}
}
/* updateTime
* Uses the state machine to determine what to display
* STOPPED: Do not run anything in this timer
* PREPARE: Set the clock for 10s and decrement. When 0, move state machine to RUNNING. Sets alarm state machine
* RUNNING: Uses countdown timer and decrements down to 0. Sets alarm state machine
*/
void updateTime() {
//if we're not actively running, don't do anything
switch(runStatus)
{
case STOPPED:
return;
break;
case PREPARE:
{
showNumber(prepclk);
if (prepclk == 0) //reset PREPARE time, move to next state
{
prepclk = 10;
runStatus = RUNNING;
Serial.println("State is RUNNING");
alarmCount = 1;
break; //immediately move to RUNNING state, display time, and start countdown
}
prepclk--;
}
break;
case RUNNING:
{
showNumber(runclk);
if (runclk == 0)
{
runStatus = STOPPED;
Serial.println("State is STOPPED");
alarmCount = 3;
}
showNumber(runclk);
runclk--;
}
break;
}
}
/* runAlarm
* Controls the alarm state (on/off) in 250ms intervals
* Gate is the alarm count, set whenever an event occurs (prepare, time complete, 5 whiste)
* State toggles between active/inactive. Only decrement the count remaining when transitions from on to off
*/
void runAlarm()
{
if (alarmCount)
{
if (alarmStatus == INACTIVE)
{
digitalWrite(alarmOutPin, HIGH);
alarmStatus = ACTIVE;
} else
{
digitalWrite(alarmOutPin, LOW);
alarmStatus = INACTIVE;
alarmCount--;
}
}
}
//Takes a number and displays 2 numbers. Displays absolute value (no negatives)
void showNumber(float value)
{
int number = abs(value); //Remove negative signs and any decimals
String s_number = String(number);
display.clearDisplay();
display.setTextSize(3);
display.setCursor(0,0);
centerText(s_number);
display.setTextSize(1);
switch(runStatus) {
case STOPPED:
centerText("Stopped");
break;
case PREPARE:
centerText("Approach Line");
break;
case RUNNING:
centerText("Running");
break;
}
display.display();
//3 digits. For each digit, push the digit to the display
for (byte x = 0 ; x < 3 ; x++)
{
int remainder = number % 10;
postNumber(remainder, false);
number /= 10;
}
//Latch the current segment data
digitalWrite(segmentLatch, LOW);
digitalWrite(segmentLatch, HIGH); //Register moves storage register on the rising edge of RCK
}
//Given a number, or '-', shifts it out to the display
void postNumber(byte number, boolean decimal)
{
// - A
// / / F/B
// - G
// / / E/C
// -. D/DP
#define a 1<<0
#define b 1<<6
#define c 1<<5
#define d 1<<4
#define e 1<<3
#define f 1<<1
#define g 1<<2
#define dp 1<<7
byte segments;
switch (number)
{
case 1: segments = b | c; break;
case 2: segments = a | b | d | e | g; break;
case 3: segments = a | b | c | d | g; break;
case 4: segments = f | g | b | c; break;
case 5: segments = a | f | g | c | d; break;
case 6: segments = a | f | g | e | c | d; break;
case 7: segments = a | b | c; break;
case 8: segments = a | b | c | d | e | f | g; break;
case 9: segments = a | b | c | d | f | g; break;
case 0: segments = a | b | c | d | e | f; break;
case ' ': segments = 0; break;
case 'c': segments = g | e | d; break;
case '-': segments = g; break;
}
if (decimal) segments |= dp;
//Clock these bits out to the drivers
for (byte x = 0 ; x < 8 ; x++)
{
digitalWrite(segmentClock, LOW);
digitalWrite(segmentData, segments & 1 << (7 - x));
digitalWrite(segmentClock, HIGH); //Data transfers to the register on the rising edge of SRCK
}
}
void centerText(String text)
{
int16_t x1;
int16_t y1;
uint16_t width;
uint16_t height;
display.getTextBounds(text, 0, 0, &x1, &y1, &width, &height);
display.setCursor((SCREEN_WIDTH - width) / 2, display.getCursorY()); //only center on X axis, keep current Y axis
display.println(text);
}