FluxBench.AIShared project · read-only

Time-Lapse Camera Rig

ESP32 DevKit (WROOM-32)·Arduino C++·Updated Jul 27, 2026·by FluxBench TeamIncludes Lab + Schematic

An interrupt-driven time-lapse controller that uses the DS3231’s SQW/INT pin the way battery-powered rigs do: alarm 1 is armed in DS3231_A1_PerSecond mode, the RTC pulls the INT line (wired to GPIO 27) LOW on every match, and attachInterrupt(..., FALLING) delivers the tick — no polling loop asking the RTC if it fired. Press the bench button (GPIO 14) to start rolling: every 5 seconds the rig “takes a frame”, flashing the white LED on GPIO 4 for 150 ms and logging “SHOT n @ HH:MM:SS” with the RTC’s own timestamp. The OLED shows ROLLING/PAUSED, the live RTC clock, the frame tally and a countdown to the next shot; press the button again to pause.

sketch.ino
// Time-Lapse Camera Rig - the DS3231's SQW/INT pin fires the shutter
// ESP32 DevKit v1. DS3231 @0x68 + SSD1306 @0x3C on I2C (SDA=21, SCL=22),
// DS3231 SQW/INT pin wired to GPIO 27, start/pause button on GPIO 14,
// white "shutter flash" LED on GPIO 4.
//
// Unlike a polled alarm, this sketch never asks the RTC "did you fire?" in a
// tight loop. The DS3231 pulls its SQW/INT line LOW whenever alarm 1 matches,
// and attachInterrupt(..., FALLING) wakes the code - the pattern you use when
// the MCU sleeps between frames. Alarm 1 is armed in DS3231_A1_PerSecond mode,
// so the ISR ticks once per second; every 5th tick takes a "shot".

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <RTClib.h>

#define INT_PIN 27     // DS3231 SQW/INT line (open drain, active LOW)
#define BUTTON_PIN 14  // start / pause the rig
#define FLASH_PIN 4    // white LED = camera flash

#define SHOT_EVERY 5   // seconds between frames

RTC_DS3231 rtc;
Adafruit_SSD1306 display(128, 64, &Wire, -1);

volatile bool tick = false;    // set by the ISR, consumed in loop()
void IRAM_ATTR onRtcAlarm() {
  tick = true;
}

bool rolling = false;
int shots = 0;
int countdown = SHOT_EVERY;
int lastBtn = 1;
unsigned long lastPress = 0;
unsigned long flashOffAt = 0;

void takeShot() {
  shots = shots + 1;
  digitalWrite(FLASH_PIN, HIGH);   // fire the flash for 150 ms
  flashOffAt = millis() + 150;
  DateTime now = rtc.now();
  char stamp[20];
  sprintf(stamp, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
  Serial.print("SHOT ");
  Serial.print(shots);
  Serial.print(" @ ");
  Serial.println(stamp);
}

void drawScreen() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(2);
  display.setCursor(0, 0);
  if (rolling) display.println("ROLLING");
  else display.println("PAUSED");
  display.setTextSize(1);
  DateTime now = rtc.now();
  char stamp[20];
  sprintf(stamp, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
  display.setCursor(0, 20);
  display.print("RTC ");
  display.println(stamp);
  display.setCursor(0, 34);
  display.print("Frames: ");
  display.println(shots);
  display.setCursor(0, 48);
  if (rolling) {
    display.print("Next shot in ");
    display.print(countdown);
    display.print("s");
  } else {
    display.print("Button = start");
  }
  display.display();
}

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(FLASH_PIN, OUTPUT);
  pinMode(INT_PIN, INPUT_PULLUP);   // open-drain INT line needs a pull-up
  Wire.begin();
  rtc.begin();
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);

  // Route alarm 1 to the INT/SQW pin and let it match once per second
  rtc.writeSqwPinMode(DS3231_OFF);  // INT mode, not square wave
  rtc.disableAlarm(2);
  rtc.clearAlarm(1);
  rtc.setAlarm1(rtc.now(), DS3231_A1_PerSecond);
  attachInterrupt(digitalPinToInterrupt(INT_PIN), onRtcAlarm, FALLING);

  Serial.println("TIMELAPSE READY - press the button to roll");
  drawScreen();
}

void loop() {
  // button toggles rolling / paused
  int btn = digitalRead(BUTTON_PIN);
  if (btn == LOW && lastBtn == HIGH && millis() - lastPress > 200) {
    lastPress = millis();
    rolling = !rolling;
    countdown = SHOT_EVERY;
    if (rolling) Serial.println("ROLLING");
    else Serial.println("PAUSED");
    drawScreen();
  }
  lastBtn = btn;

  // one tick per second, delivered by the DS3231 over the INT line
  if (tick) {
    tick = false;
    rtc.clearAlarm(1);   // release the INT line so it can fall again
    if (rolling) {
      countdown = countdown - 1;
      if (countdown <= 0) {
        takeShot();
        countdown = SHOT_EVERY;
      }
      drawScreen();
    }
  }

  if (flashOffAt > 0 && millis() >= flashOffAt) {
    digitalWrite(FLASH_PIN, LOW);
    flashOffAt = 0;
  }
  delay(20);
}

Built with FluxBench.AI — the AI workbench for embedded projects.