Raushan kr.
Published © MIT

Build a Smart Timelapse Camera with UNIHIKER K10

In this guide, you’ll build a sleek, automated timelapse camera using the UNIHIKER K10 by DFRobot—perfect for capturing long-term scenes wit

BeginnerFull instructions provided1 hour137
Build a Smart Timelapse Camera with UNIHIKER K10

Things used in this project

Hardware components

UNIHIKER K10 AI Educational Tool for Kids (Image Detection, Voice Recognition, TinyML)
DFRobot UNIHIKER K10 AI Educational Tool for Kids (Image Detection, Voice Recognition, TinyML)
×1
Flash Memory Card, MicroSDHC Card
Flash Memory Card, MicroSDHC Card
×1
USB-A to B Cable
USB-A to B Cable
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

Stl.zip

Schematics

unihiker

Code

Source Code

Arduino
/******************************************************
 * UNIHIKER K10 Time-Lapse Camera
 * --------------------------------
 * Features:
 * - Stream camera to display
 * - Capture time-lapse images to SD card
 * - Selectable resolution
 * 
 * Hardware: UNIHIKER K10 (ESP32-S3)
 *******************************************************/

#include "unihiker_k10.h"

// ============================================================
// CAMERA PINS (K10 - DO NOT CHANGE)
// ============================================================
#define CAM_PWDN     -1
#define CAM_RESET    -1
#define CAM_XCLK      7
#define CAM_SIOD     47
#define CAM_SIOC     48
#define CAM_Y9        6
#define CAM_Y8       15
#define CAM_Y7       16
#define CAM_Y6       18
#define CAM_Y5        9
#define CAM_Y4       11
#define CAM_Y3       10
#define CAM_Y2        8
#define CAM_VSYNC     4
#define CAM_HREF      5
#define CAM_PCLK     17

// ============================================================
// CONFIG
// ============================================================
#define APP_VERSION     "v3.0"
#define JPG_QUALITY     50
#define EXIT_HOLD_MS    800
#define UI_REFRESH_MS   800

// ============================================================
// COLORS
// ============================================================
#define C_BG          0x0000
#define C_CARD        0x10A2
#define C_CARD_LIGHT  0x2124
#define C_BORDER      0x4228
#define C_ACCENT      0x07FF
#define C_CYAN        0x07FF
#define C_WHITE       0xFFFF
#define C_GRAY        0x8410
#define C_DARK_GRAY   0x4208
#define C_YELLOW      0xFFE0
#define C_GREEN       0x07E0
#define C_RED         0xF800
#define C_MAGENTA     0xF81F

// ============================================================
// SCREEN
// ============================================================
#define SCR_W   240
#define SCR_H   320

// ============================================================
// STATES
// ============================================================
enum State {
    S_SPLASH,
    S_MENU,
    S_RES,
    S_HRS,
    S_MIN,
    S_SEC,
    S_CONFIRM,
    S_STREAM,
    S_REC,
    S_DONE,
    S_ERR
};

// ============================================================
// RESOLUTIONS
// ============================================================
struct ResInfo {
    const char* name;
    const char* detail;
    framesize_t size;
};

const ResInfo RES[] = {
    {"QVGA",    "240x320 - Fast",     FRAMESIZE_QVGA},
    {"VGA",     "640x480 - Good",     FRAMESIZE_SVGA},
    {"SVGA",    "800x600 - Better",   FRAMESIZE_XGA},
    {"HD",      "1280x720 - HD",      FRAMESIZE_SXGA},
    {"SXGA",    "1280x1024 - Best",   FRAMESIZE_UXGA}
};
#define NUM_RES 5

// ============================================================
// GLOBALS
// ============================================================
UNIHIKER_K10 k10;
TFT_eSPI tft = TFT_eSPI();

// Buttons
volatile bool flagA = false;
volatile bool flagB = false;
uint32_t bothStart = 0;

// State
State st = S_SPLASH;
bool needDraw = true;
uint32_t stTime = 0;
uint32_t lastDraw = 0;

// Settings
int selMode = 0;
int selRes = 1;
int valH = 0, valM = 0, valS = 10;
uint32_t interval = 10000;

// Capture
uint32_t capStart = 0;
uint32_t nextCap = 0;
uint32_t imgNum = 0;
uint32_t imgBase = 0;
uint32_t totalKB = 0;
int errCnt = 0;

// Camera
bool camOn = false;
uint8_t* jpgBuf = NULL;
size_t jpgSize = 0;

// SD
bool sdOn = false;

// Buffers
char msgErr[40] = {0};
char msgFile[20] = {0};

// Stream
bool streaming = false;

// ============================================================
// ISR
// ============================================================
void IRAM_ATTR onA() { flagA = true; }
void IRAM_ATTR onB() { flagB = true; }

// ============================================================
// BUTTONS
// ============================================================
bool pressA() {
    if (flagA) { flagA = false; return true; }
    return false;
}

bool pressB() {
    if (flagB) { flagB = false; return true; }
    return false;
}

void clearBtn() {
    flagA = false;
    flagB = false;
    bothStart = 0;
}

bool checkExit() {
    if (flagA && flagB) {
        if (bothStart == 0) bothStart = millis();
        if (millis() - bothStart >= EXIT_HOLD_MS) {
            flagA = false;
            flagB = false;
            bothStart = 0;
            return true;
        }
    } else {
        bothStart = 0;
    }
    return false;
}

// ============================================================
// CLEANUP
// ============================================================
void stopCam() {
    if (jpgBuf) { free(jpgBuf); jpgBuf = NULL; jpgSize = 0; }
    if (camOn) { esp_camera_deinit(); camOn = false; }
}

void stopStream() {
    streaming = false;
    k10.initScreen(2);
    tft.init();
    tft.setRotation(2);
    tft.setTextSize(1);
    delay(50);
}

void cleanup() {
    if (streaming) stopStream();
    stopCam();
    imgNum = 0;
    totalKB = 0;
    errCnt = 0;
    msgFile[0] = 0;
    msgErr[0] = 0;
}

void goHome() {
    cleanup();
    st = S_MENU;
    needDraw = true;
    stTime = millis();
    clearBtn();
}

void goTo(State s) {
    st = s;
    needDraw = true;
    stTime = millis();
    clearBtn();
}

// ============================================================
// DRAWING HELPERS
// ============================================================
void card(int x, int y, int w, int h, uint16_t fill, uint16_t edge, bool shadow = false) {
    if (shadow) {
        tft.fillRoundRect(x + 2, y + 2, w, h, 8, C_DARK_GRAY);
    }
    tft.fillRoundRect(x, y, w, h, 8, fill);
    tft.drawRoundRect(x, y, w, h, 8, edge);
}

void topBar(const char* txt, uint16_t accent = C_CYAN) {
    tft.fillRect(0, 0, SCR_W, 38, C_CARD);
    tft.drawFastHLine(0, 37, SCR_W, accent);
    tft.drawFastHLine(0, 38, SCR_W, accent);
    
    tft.setTextColor(C_DARK_GRAY, C_CARD);
    tft.drawString(txt, 10, 11, 4);
    tft.setTextColor(C_WHITE, C_CARD);
    tft.drawString(txt, 8, 9, 4);
}

void botBar(const char* a, const char* b, bool showExit = false) {
    int y = SCR_H - 38;
    
    tft.fillRect(0, y, SCR_W, 38, C_CARD);
    tft.drawFastHLine(0, y, SCR_W, C_BORDER);
    
    if (a && a[0]) {
        tft.fillRoundRect(8, y + 8, 22, 22, 4, C_YELLOW);
        tft.setTextColor(C_BG, C_YELLOW);
        tft.drawString("A", 14, y + 12, 2);
        tft.setTextColor(C_GRAY, C_CARD);
        tft.drawString(a, 35, y + 12, 2);
    }
    
    if (b && b[0]) {
        tft.fillRoundRect(125, y + 8, 22, 22, 4, C_GREEN);
        tft.setTextColor(C_BG, C_GREEN);
        tft.drawString("B", 131, y + 12, 2);
        tft.setTextColor(C_GRAY, C_CARD);
        tft.drawString(b, 152, y + 12, 2);
    }
    
    if (showExit) {
        tft.setTextColor(C_DARK_GRAY, C_CARD);
        tft.drawString("A+B Exit", 175, y + 28, 1);
    }
}

void menuOpt(int y, const char* txt, const char* sub, bool sel) {
    uint16_t bg = sel ? C_CARD_LIGHT : C_BG;
    uint16_t edge = sel ? C_CYAN : C_BORDER;
    
    card(8, y, SCR_W - 16, sub ? 42 : 32, bg, edge, sel);
    
    int dotX = 22;
    int dotY = y + (sub ? 21 : 16);
    if (sel) {
        tft.fillCircle(dotX, dotY, 6, C_CYAN);
        tft.fillCircle(dotX, dotY, 3, C_WHITE);
    } else {
        tft.drawCircle(dotX, dotY, 5, C_BORDER);
    }
    
    tft.setTextColor(sel ? C_YELLOW : C_WHITE, bg);
    tft.drawString(txt, 38, y + 8, 2);
    
    if (sub) {
        tft.setTextColor(C_GRAY, bg);
        tft.drawString(sub, 38, y + 26, 1);
    }
}

void infoCard(int y, const char* lbl, const char* val, uint16_t col, bool highlight = false) {
    uint16_t bg = highlight ? C_CARD_LIGHT : C_CARD;
    card(8, y, SCR_W - 16, 38, bg, highlight ? col : C_BORDER);
    
    tft.setTextColor(C_GRAY, bg);
    tft.drawString(lbl, 16, y + 5, 1);
    
    tft.setTextColor(col, bg);
    tft.drawString(val, 16, y + 19, 2);
    
    if (highlight) {
        tft.fillRect(8, y, 3, 38, col);
    }
}

void progressBar(int y, float pct, uint16_t col = C_CYAN) {
    pct = constrain(pct, 0, 1);
    int w = SCR_W - 32;
    int filled = (int)(w * pct);
    
    tft.fillRoundRect(16, y, w, 10, 5, C_DARK_GRAY);
    
    if (filled > 0) {
        tft.fillRoundRect(16, y, filled, 10, 5, col);
        tft.drawFastHLine(16, y + 2, max(1, filled - 4), C_WHITE);
    }
}

void valueBox(int y, const char* lbl, int val, const char* unit, int maxVal) {
    card(8, y, SCR_W - 16, 80, C_CARD, C_CYAN, true);
    
    tft.setTextColor(C_GRAY, C_CARD);
    tft.drawString(lbl, 16, y + 8, 2);
    
    char buf[16];
    sprintf(buf, "%d", val);
    tft.setTextColor(C_YELLOW, C_CARD);
    tft.setTextDatum(MC_DATUM);
    tft.drawString(buf, SCR_W / 2, y + 45, 6);
    tft.setTextDatum(TL_DATUM);
    
    tft.setTextColor(C_GRAY, C_CARD);
    tft.drawString(unit, SCR_W / 2 + 30, y + 40, 2);
    
    sprintf(buf, "0 - %d", maxVal);
    tft.setTextColor(C_DARK_GRAY, C_CARD);
    tft.drawString(buf, 16, y + 65, 1);
}

// ============================================================
// SPLASH SCREEN
// ============================================================
void drawSplash() {
    tft.fillScreen(C_BG);
    
    int cx = SCR_W / 2;
    int iconY = 80;
    
    // Camera body with shadow
    tft.fillRoundRect(cx - 42, iconY + 2, 84, 58, 10, C_DARK_GRAY);
    tft.fillRoundRect(cx - 44, iconY, 88, 60, 12, C_CARD_LIGHT);
    tft.drawRoundRect(cx - 44, iconY, 88, 60, 12, C_CYAN);
    
    // Lens rings
    tft.fillCircle(cx, iconY + 30, 22, C_CYAN);
    tft.fillCircle(cx, iconY + 30, 18, C_CARD);
    tft.fillCircle(cx, iconY + 30, 14, C_CYAN);
    tft.fillCircle(cx, iconY + 30, 10, C_CARD_LIGHT);
    tft.fillCircle(cx, iconY + 30, 5, C_WHITE);
    
    // Flash
    tft.fillRoundRect(cx - 32, iconY + 8, 18, 12, 3, C_YELLOW);
    
    // Shutter button
    tft.fillCircle(cx + 30, iconY + 10, 6, C_RED);
    
    // App name with glow effect
    tft.setTextColor(C_CYAN, C_BG);
    tft.setTextDatum(MC_DATUM);
    tft.drawString("Time-Lapse", cx + 1, 176, 4);
    tft.setTextColor(C_WHITE, C_BG);
    tft.drawString("Time-Lapse", cx, 175, 4);
    
    tft.setTextColor(C_CYAN, C_BG);
    tft.drawString("Camera", cx + 1, 206, 4);
    tft.setTextColor(C_WHITE, C_BG);
    tft.drawString("Camera", cx, 205, 4);
    
    // Version badge
    card(cx - 30, 240, 60, 24, C_CARD, C_CYAN);
    tft.setTextColor(C_CYAN, C_CARD);
    tft.drawString(APP_VERSION, cx, 252, 2);
    
    // Loading animation
    tft.setTextColor(C_DARK_GRAY, C_BG);
    tft.drawString("Loading", cx, SCR_H - 25, 1);
    tft.setTextDatum(TL_DATUM);
    
    // Animated loading dots
    for (int i = 0; i < 3; i++) {
        delay(300);
        int dotX = cx + 25 + (i * 8);
        tft.fillCircle(dotX, SCR_H - 22, 3, C_CYAN);
    }
    
    delay(400);
}

// ============================================================
// SCREENS
// ============================================================
void scrMenu() {
    tft.fillScreen(C_BG);
    topBar("Mode Select");
    botBar("Change", "Select", false);
    
    menuOpt(60, "Live Stream", "Preview camera on screen", selMode == 0);
    menuOpt(115, "Time-Lapse", "Capture to SD card", selMode == 1);
    
    // Info card
    card(8, 175, SCR_W - 16, 80, C_CARD, C_BORDER);
    tft.setTextColor(C_CYAN, C_CARD);
    tft.drawString("Info", 16, 182, 2);
    tft.setTextColor(C_GRAY, C_CARD);
    
    if (selMode == 0) {
        tft.drawString("Live preview on screen", 16, 210, 1);
        tft.drawString("No images saved", 16, 225, 1);
        tft.drawString("Press A+B to exit stream", 16, 240, 1);
    } else {
        tft.drawString("Capture at intervals", 16, 210, 1);
        tft.drawString("Save JPEG to SD card", 16, 225, 1);
        tft.drawString("Press A+B to stop recording", 16, 240, 1);
    }
}

void scrRes() {
    tft.fillScreen(C_BG);
    topBar("Resolution");
    botBar("Change", "Select", true);
    
    for (int i = 0; i < NUM_RES; i++) {
        menuOpt(45 + i * 45, RES[i].name, RES[i].detail, i == selRes);
    }
}

void scrTime(const char* title, const char* lbl, int val, int mx) {
    tft.fillScreen(C_BG);
    topBar(title);
    botBar("+1", "OK", true);
    
    valueBox(70, lbl, val, "", mx);
    
    card(8, 170, SCR_W - 16, 55, C_CARD, C_BORDER);
    tft.setTextColor(C_GRAY, C_CARD);
    tft.drawString("Interval Preview", 16, 178, 1);
    
    char buf[20];
    sprintf(buf, "%02d:%02d:%02d", valH, valM, valS);
    tft.setTextColor(C_CYAN, C_CARD);
    tft.drawString(buf, 16, 195, 4);
}

void scrConfirm() {
    tft.fillScreen(C_BG);
    topBar("Confirm", C_GREEN);
    botBar("Back", "Start!", true);
    
    char buf[32];
    
    infoCard(48, "Resolution", RES[selRes].name, C_CYAN, true);
    
    sprintf(buf, "%02d:%02d:%02d", valH, valM, valS);
    infoCard(92, "Interval", buf, C_CYAN, true);
    
    infoCard(136, "SD Card", sdOn ? "Ready" : "NOT FOUND!", sdOn ? C_GREEN : C_RED, !sdOn);
    
    sprintf(buf, "img%05lu.jpg", imgBase + 1);
    infoCard(180, "First Image", buf, C_GRAY, false);
    
    if (!sdOn) {
        card(8, 230, SCR_W - 16, 35, C_RED, C_RED);
        tft.setTextColor(C_WHITE, C_RED);
        tft.setTextDatum(MC_DATUM);
        tft.drawString("Insert SD card!", SCR_W/2, 247, 2);
        tft.setTextDatum(TL_DATUM);
    }
}

void scrRec() {
    tft.fillScreen(C_BG);
    topBar("Recording", C_RED);
    
    tft.fillCircle(SCR_W - 25, 19, 8, C_RED);
    tft.fillCircle(SCR_W - 25, 19, 4, C_WHITE);
    
    tft.fillRect(0, SCR_H - 30, SCR_W, 30, C_CARD);
    tft.setTextColor(C_YELLOW, C_CARD);
    tft.setTextDatum(MC_DATUM);
    tft.drawString("Hold A+B to Stop", SCR_W/2, SCR_H - 15, 2);
    tft.setTextDatum(TL_DATUM);
}

void updateRec() {
    uint32_t el = (millis() - capStart) / 1000;
    int h = el / 3600;
    int m = (el % 3600) / 60;
    int s = el % 60;
    
    int32_t rem = (nextCap - millis()) / 1000;
    if (rem < 0) rem = 0;
    
    float pct = 1.0f - (float)(nextCap - millis()) / interval;
    
    char buf[24];
    
    sprintf(buf, "%02d:%02d:%02d", h, m, s);
    infoCard(48, "Elapsed", buf, C_CYAN, true);
    
    sprintf(buf, "%lu", imgNum);
    infoCard(92, "Images Captured", buf, C_GREEN, true);
    
    sprintf(buf, "%ld sec", rem);
    infoCard(136, "Next Capture", buf, C_YELLOW, false);
    
    progressBar(180, pct, C_CYAN);
    
    if (msgFile[0]) {
        infoCard(195, "Last File", msgFile, C_GRAY, false);
    }
    
    if (totalKB > 1024) {
        sprintf(buf, "%.1f MB", (float)totalKB / 1024);
    } else {
        sprintf(buf, "%lu KB", totalKB);
    }
    infoCard(239, "Total Size", buf, C_GRAY, false);
    
    // Blink record indicator
    static uint32_t lastBlink = 0;
    static bool vis = true;
    if (millis() - lastBlink > 500) {
        vis = !vis;
        lastBlink = millis();
    }
    tft.fillCircle(SCR_W - 25, 19, 8, vis ? C_RED : C_CARD);
    if (vis) tft.fillCircle(SCR_W - 25, 19, 4, C_WHITE);
}

void scrDone() {
    tft.fillScreen(C_BG);
    topBar("Complete", C_GREEN);
    botBar("", "Home", false);
    
    int cx = SCR_W / 2;
    tft.fillCircle(cx, 85, 30, C_GREEN);
    tft.setTextColor(C_BG, C_GREEN);
    tft.setTextDatum(MC_DATUM);
    tft.drawString("OK", cx, 85, 4);
    tft.setTextDatum(TL_DATUM);
    
    char buf[24];
    sprintf(buf, "%lu images", imgNum);
    infoCard(130, "Captured", buf, C_CYAN, true);
    
    if (totalKB > 1024) {
        sprintf(buf, "%.1f MB", (float)totalKB / 1024);
    } else {
        sprintf(buf, "%lu KB", totalKB);
    }
    infoCard(174, "Total Size", buf, C_GRAY, false);
}

void scrErr() {
    tft.fillScreen(C_BG);
    topBar("Error", C_RED);
    botBar("", "Retry", false);
    
    int cx = SCR_W / 2;
    tft.fillCircle(cx, 85, 30, C_RED);
    tft.setTextColor(C_WHITE, C_RED);
    tft.setTextDatum(MC_DATUM);
    tft.drawString("!", cx, 85, 4);
    tft.setTextDatum(TL_DATUM);
    
    card(8, 130, SCR_W - 16, 60, C_CARD, C_RED);
    tft.setTextColor(C_WHITE, C_CARD);
    tft.drawString(msgErr, 16, 155, 2);
}

// ============================================================
// SD & CAMERA FUNCTIONS
// ============================================================
bool sdInit() {
    if (!SD.begin()) { strcpy(msgErr, "SD mount failed"); return false; }
    if (SD.cardType() == CARD_NONE) { strcpy(msgErr, "No SD card"); return false; }
    return true;
}

uint32_t sdScan() {
    uint32_t mx = 0;
    File root = SD.open("/");
    if (!root) return 0;
    
    while (true) {
        File f = root.openNextFile();
        if (!f) break;
        const char* n = f.name();
        int len = strlen(n);
        if (len >= 12 && n[0] == 'i' && n[1] == 'm' && n[2] == 'g') {
            if (n[len-4] == '.' && n[len-3] == 'j' && n[len-2] == 'p' && n[len-1] == 'g') {
                char num[6] = {0};
                for (int i = 0; i < 5; i++) num[i] = n[3 + i];
                uint32_t v = atoi(num);
                if (v > mx) mx = v;
            }
        }
        f.close();
    }
    root.close();
    return mx;
}

bool sdSave(uint8_t* buf, size_t len, uint32_t num) {
    char path[24];
    sprintf(path, "/img%05lu.jpg", num);
    
    File f = SD.open(path, FILE_WRITE);
    if (!f) { strcpy(msgErr, "File create failed"); return false; }
    
    size_t w = f.write(buf, len);
    f.close();
    
    if (w != len) { strcpy(msgErr, "Write failed"); return false; }
    
    for (int i = 0; i < 18; i++) msgFile[i] = path[i + 1];
    msgFile[18] = 0;
    totalKB += len / 1024;
    return true;
}

bool camInit(framesize_t sz) {
    camera_config_t c;
    c.ledc_channel = LEDC_CHANNEL_0;
    c.ledc_timer = LEDC_TIMER_0;
    c.pin_d0 = CAM_Y2; c.pin_d1 = CAM_Y3; c.pin_d2 = CAM_Y4; c.pin_d3 = CAM_Y5;
    c.pin_d4 = CAM_Y6; c.pin_d5 = CAM_Y7; c.pin_d6 = CAM_Y8; c.pin_d7 = CAM_Y9;
    c.pin_xclk = CAM_XCLK; c.pin_pclk = CAM_PCLK;
    c.pin_vsync = CAM_VSYNC; c.pin_href = CAM_HREF;
    c.pin_sscb_sda = CAM_SIOD; c.pin_sscb_scl = CAM_SIOC;
    c.pin_pwdn = CAM_PWDN; c.pin_reset = CAM_RESET;
    c.xclk_freq_hz = 10000000;
    c.pixel_format = PIXFORMAT_RGB565;
    c.frame_size = sz;
    c.grab_mode = CAMERA_GRAB_LATEST;
    c.fb_count = 2;
    
    if (esp_camera_init(&c) != ESP_OK) {
        strcpy(msgErr, "Camera init failed");
        return false;
    }
    
    sensor_t* s = esp_camera_sensor_get();
    if (s) s->set_hmirror(s, 1);
    
    camOn = true;
    return true;
}

bool camCapture() {
    camera_fb_t* fb = esp_camera_fb_get();
    if (!fb) { errCnt++; return false; }
    
    bool ok = fmt2jpg(fb->buf, fb->len, fb->width, fb->height, fb->format, JPG_QUALITY, &jpgBuf, &jpgSize);
    esp_camera_fb_return(fb);
    
    if (!ok) { errCnt++; return false; }
    errCnt = 0;
    return true;
}

void camFreeJpg() {
    if (jpgBuf) { free(jpgBuf); jpgBuf = NULL; jpgSize = 0; }
}

// ============================================================
// STATE HANDLERS
// ============================================================
void hSplash() {
    if (needDraw) { drawSplash(); needDraw = false; }
    if (millis() - stTime > 2500 || pressA() || pressB()) {
        goTo(S_MENU);
    }
}

void hMenu() {
    if (needDraw) { scrMenu(); needDraw = false; }
    if (pressA()) { selMode = (selMode + 1) % 2; scrMenu(); }
    if (pressB()) {
        if (selMode == 0) goTo(S_STREAM);
        else goTo(S_RES);
    }
}

void hRes() {
    if (checkExit()) { goHome(); return; }
    if (needDraw) { scrRes(); needDraw = false; }
    if (pressA()) { selRes = (selRes + 1) % NUM_RES; scrRes(); }
    if (pressB()) { goTo(S_HRS); }
}

void hHrs() {
    if (checkExit()) { goHome(); return; }
    if (needDraw) { scrTime("Hours", "Hours", valH, 24); needDraw = false; }
    if (pressA()) { valH = (valH + 1) % 25; scrTime("Hours", "Hours", valH, 24); }
    if (pressB()) { goTo(S_MIN); }
}

void hMin() {
    if (checkExit()) { goHome(); return; }
    if (needDraw) { scrTime("Minutes", "Minutes", valM, 59); needDraw = false; }
    if (pressA()) { valM = (valM + 1) % 60; scrTime("Minutes", "Minutes", valM, 59); }
    if (pressB()) { goTo(S_SEC); }
}

void hSec() {
    if (checkExit()) { goHome(); return; }
    if (needDraw) { scrTime("Seconds", "Seconds", valS, 59); needDraw = false; }
    if (pressA()) { valS = (valS + 1) % 60; scrTime("Seconds", "Seconds", valS, 59); }
    if (pressB()) {
        interval = 1000UL * (3600UL * valH + 60UL * valM + valS);
        if (interval < 1000) { interval = 1000; valS = 1; valM = 0; valH = 0; }
        sdOn = sdInit();
        if (sdOn) imgBase = sdScan();
        goTo(S_CONFIRM);
    }
}

void hConfirm() {
    if (checkExit()) { goHome(); return; }
    if (needDraw) { scrConfirm(); needDraw = false; }
    if (pressA()) { goTo(S_MENU); }
    if (pressB()) {
        if (!sdOn) {
            sdOn = sdInit();
            if (sdOn) imgBase = sdScan();
            scrConfirm();
            if (!sdOn) { strcpy(msgErr, "SD not ready"); goTo(S_ERR); }
        } else {
            if (camInit(RES[selRes].size)) {
                imgNum = 0; totalKB = 0; errCnt = 0; msgFile[0] = 0;
                capStart = millis(); nextCap = capStart;
                goTo(S_REC);
            } else { goTo(S_ERR); }
        }
    }
}

void hStream() {
    if (checkExit()) { stopStream(); goHome(); return; }
    if (needDraw) { streaming = true; k10.initBgCamerImage(); k10.setBgCamerImage(); needDraw = false; }
    flagA = false; flagB = false;
}

void hRec() {
    if (checkExit()) { stopCam(); goTo(S_DONE); return; }
    if (needDraw) { scrRec(); updateRec(); needDraw = false; }
    
    uint32_t now = millis();
    if (now >= nextCap) {
        if (camCapture()) {
            uint32_t n = imgBase + imgNum + 1;
            if (sdSave(jpgBuf, jpgSize, n)) imgNum++;
            else errCnt++;
            camFreeJpg();
        }
        nextCap += interval;
        if (nextCap < now) nextCap = now + interval;
        if (errCnt >= 5) { strcpy(msgErr, "Too many errors"); stopCam(); goTo(S_ERR); return; }
    }
    if (now - lastDraw >= UI_REFRESH_MS) { updateRec(); lastDraw = now; }
    flagA = false; flagB = false;
}

void hDone() {
    if (needDraw) { scrDone(); needDraw = false; }
    if (pressB()) { goTo(S_MENU); }
}

void hErr() {
    if (needDraw) { scrErr(); needDraw = false; }
    if (pressB()) { msgErr[0] = 0; errCnt = 0; goTo(S_MENU); }
}

// ============================================================
// MAIN
// ============================================================
void run() {
    switch (st) {
        case S_SPLASH:   hSplash(); break;
        case S_MENU:     hMenu(); break;
        case S_RES:      hRes(); break;
        case S_HRS:      hHrs(); break;
        case S_MIN:      hMin(); break;
        case S_SEC:      hSec(); break;
        case S_CONFIRM:  hConfirm(); break;
        case S_STREAM:   hStream(); break;
        case S_REC:      hRec(); break;
        case S_DONE:     hDone(); break;
        case S_ERR:      hErr(); break;
    }
}

void setup() {
    Serial.begin(115200);
    Serial.println("\n=============================");
    Serial.println("  K10 Time-Lapse Camera");
    Serial.printf("  %s\n", APP_VERSION);
    Serial.println("=============================\n");
    
    k10.begin();
    k10.initScreen(2);
    tft.init();
    tft.setRotation(2);
    tft.setTextSize(1);
    tft.fillScreen(C_BG);
    
    k10.buttonA->setPressedCallback(onA);
    k10.buttonB->setPressedCallback(onB);
    clearBtn();
    
    Serial.println("Ready!\n");
}

void loop() {
    run();
    delay(10);
}

Credits

Raushan kr.
34 projects • 162 followers
Maker | Developer | Content Creator

Comments