4
\$\begingroup\$

Description

I have created a darkroom timer, which is used for enlarging and developing film.

It has four separate timers, and can toggle mains via a relay to turn on the enlarger for a specified amount of time. Something like a mix between a triple timer for development and an enlarger timer.

For this project, I used an Arduino Uno, with the following hardware configuration:

See CAD render of hardware.

Included libraries

Code

#include <SevSeg.h>
#include <EEPROM.h>

const byte SAFELIGHT_PIN = 1;
const byte BUTTON_ARRAY_PIN = A0;
const byte ENCODER_A_PIN = A1;
const byte ENCODER_B_PIN = A2;
const byte RELAY_PIN = A3;
const byte SAFELIGHT_BUTTON_PIN = A4;
const byte BUZZER_PIN = A5;

// Changable if EEPROM ever wears out at these addresses
const int EEPROM_ADDRESSES[] = { 0, 1, 2, 3 };

const byte NUM_DIGITS = 4;
const byte DIGIT_PINS[] = { 5, 4, 3, 2 };
const byte SEGMENT_PINS[] = { 11, 13, 9, 7, 6, 12, 10, 8 };

SevSeg screen;

int buttonArrayValue;
unsigned long lastButtonArrayTime;
unsigned long lastTimerUpdate;
unsigned long timerUpdate;
boolean lastEncoderAValue;
boolean encoderAValue;
boolean encoderBValue;

byte activeTimer = 0;
int timers[] = {
  EEPROM.read(EEPROM_ADDRESSES[0]),
  EEPROM.read(EEPROM_ADDRESSES[1]),
  EEPROM.read(EEPROM_ADDRESSES[2]),
  EEPROM.read(EEPROM_ADDRESSES[3])
};

boolean timerRunning = false;
boolean safelightOn = false;

void setup() {
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(RELAY_PIN, OUTPUT);
  pinMode(ENCODER_A_PIN, INPUT);
  pinMode(ENCODER_B_PIN, INPUT);
  pinMode(SAFELIGHT_BUTTON_PIN, INPUT);

  // Type, digits, segments, digit pins, segment pins, resistors on pins, update with delay, leading zeroes.
  screen.begin(COMMON_CATHODE, NUM_DIGITS, DIGIT_PINS, SEGMENT_PINS, true, false, true);
  screen.setBrightness(10);

  lastEncoderAValue = digitalRead(ENCODER_A_PIN);
}

void loop() {
  // Read rotary encoder
  encoderAValue = digitalRead(ENCODER_A_PIN);
  encoderBValue = digitalRead(ENCODER_B_PIN);
  if (encoderAValue != lastEncoderAValue  && encoderAValue == true && timerRunning == false) {
    if (encoderBValue != encoderAValue) {
      timers[activeTimer]++;
    } else {
      timers[activeTimer]--;
    }
    // Minimum timer is 0, maximum timer is 99 minutes, 99 seconds = 6039 seconds
    timers[activeTimer] = min(max(0, timers[activeTimer]), 6039);
  }
  lastEncoderAValue = encoderAValue;

  // Button array
  buttonArrayValue = analogRead(BUTTON_ARRAY_PIN);
  if (millis() - lastButtonArrayTime > 100 && timerRunning == false) {
    if (buttonPressed(buttonArrayValue, 516)) {
      startTimer();
    } else if (buttonPressed(buttonArrayValue, 344)) {
      startTimer();
      digitalWrite(RELAY_PIN, HIGH);
    } else if (buttonPressed(buttonArrayValue, 257)) {
      activeTimer = 0;
    } else if (buttonPressed(buttonArrayValue, 205)) {
      activeTimer = 1;
    } else if (buttonPressed(buttonArrayValue, 171)) {
      activeTimer = 2;
    } else if (buttonPressed(buttonArrayValue, 146)) {
      activeTimer = 3;
    }
    lastButtonArrayTime = millis();
  }

  timerUpdate = millis() - lastTimerUpdate;

  // Clear timer
  if (timerUpdate > 1000 && timerRunning == true && timers[activeTimer] <= 0) {
    timers[activeTimer] = EEPROM.read(EEPROM_ADDRESSES[activeTimer]);
    tone(BUZZER_PIN, 880, 1000);
    timerRunning = false;
    digitalWrite(RELAY_PIN, LOW);
  }
  
  // Update timer every second
  if (timerUpdate > 1000 && timerRunning == true) {
    timers[activeTimer]--;
    // Beep every second, with a higher beep every 30 seconds.
    if (timers[activeTimer] % 30 == 0) {
      tone(BUZZER_PIN, 880, 250);
    } else {
      tone(BUZZER_PIN, 440, 50);
    }
    lastTimerUpdate += timerUpdate; // Use the timerUpdate so average time is correct
  }

  // Update displays (safelight and screen).
  safelightOn = digitalRead(SAFELIGHT_BUTTON_PIN);

  if (safelightOn == true) {
    digitalWrite(SAFELIGHT_PIN, HIGH);
    screen.setNumber(secondsToMinutes(timers[activeTimer]), 3 - activeTimer);
  } else {
    digitalWrite(SAFELIGHT_PIN, LOW);
    screen.blank();
  }
  screen.refreshDisplay();
}

// Calculates if a number is within a margin of +/- 15. If so, it returns true.
// This is used for the analogue buttons to see if they were pressed.
boolean buttonPressed (int buttonArrayValue, int target) {
  if (abs(buttonArrayValue - target) < 15) {
    return true;
  } else {
    return false;
  }
}

// Returns a seconds value in MMSS format.
int secondsToMinutes (int seconds) {
  if (seconds < 60) {
    return seconds;
  } else {
    return (floor(seconds / 60) * 100) + (seconds % 60);
  }
}

// Stores the current time if it is new, and starts the timer.
void startTimer () {
  EEPROM.update(EEPROM_ADDRESSES[activeTimer], timers[activeTimer]);
  timerRunning = true;
}

Notes

  • I used all-capitals for const variables, however should I use #define? I didn't run into memory allocation problems.
  • Everything is within the main loop() function. I tried splitting some parts off (such as reading the rotary encoder), but it didn't work - the encoder 'read' massively large numbers instead for some reason.
  • I try to avoid writing to EEPROM where possible, since it expires. Reading is apparently fine.
  • I have preferred if (someVariable == true) { rather than if (someVariable) { - I thought it was more readable with other Boolean operators such as &&.
  • I would like a review of readability, idiomaticity, and efficiency. I am a novice with Arduino/C++ type code.
\$\endgroup\$
  • \$\begingroup\$ I'm not sure if this is too waffly. If so, I can cut out the description at the top. \$\endgroup\$ – marcellothearcane Aug 10 at 7:34
  • 1
    \$\begingroup\$ I don't see any problems with this question. Welcome to Code Review! I prefer too much description over not enough description, but I don't think you have too much. When in doubt, there's the FAQ on asking questions. \$\endgroup\$ – Mast Aug 10 at 7:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.