In this step, we upload the firmware that controls the timelapse camera. The code provides a simple on-screen menu to select camera mode, resolution, and capture interval. It also handles image capture, file naming, and saving photos to the microSD card automatically.
The complete code is provided below for reference.
/******************************************************
* UNIHIKER K10 Time-Lapse Camera
* --------------------------------
* Features:
* - Stream camera to display
* - Capture time-lapse images to SD card
* - Selectable resolution
*
* Hardware: UNIHIKER K10 (ESP32-S3)
* By Rau7han
*******************************************************/
#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 APP_AUTHOR "Rau7han"
#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);
}
// ============================================================
// BEAUTIFUL SPLASH SCREEN WITH "By Rau7han"
// ============================================================
void drawSplash() {
tft.fillScreen(C_BG);
int cx = SCR_W / 2;
int iconY = 55;
// 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, 146, 4);
tft.setTextColor(C_WHITE, C_BG);
tft.drawString("Time-Lapse", cx, 145, 4);
tft.setTextColor(C_CYAN, C_BG);
tft.drawString("Camera", cx + 1, 176, 4);
tft.setTextColor(C_WHITE, C_BG);
tft.drawString("Camera", cx, 175, 4);
// Version badge
card(cx - 30, 200, 60, 24, C_CARD, C_CYAN);
tft.setTextColor(C_CYAN, C_CARD);
tft.drawString(APP_VERSION, cx, 212, 2);
// ========== AUTHOR SECTION ==========
int authY = 245;
// Decorative line with center dot
tft.drawFastHLine(40, authY - 10, SCR_W - 80, C_BORDER);
tft.fillCircle(cx, authY - 10, 3, C_CYAN);
// "Created by" text
tft.setTextColor(C_GRAY, C_BG);
tft.drawString("Created by", cx, authY + 5, 2);
// Author name - PROMINENT with shadow effect
tft.setTextColor(C_MAGENTA, C_BG);
tft.drawString(APP_AUTHOR, cx + 1, authY + 31, 4);
tft.setTextColor(C_YELLOW, C_BG);
tft.drawString(APP_AUTHOR, cx, authY + 30, 4);
// Bottom decorative line
tft.drawFastHLine(40, authY + 55, SCR_W - 80, C_BORDER);
// ========== 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 (Only 2 menu options: Stream & Time-Lapse)
// ============================================================
void scrMenu() {
tft.fillScreen(C_BG);
topBar("Mode Select");
botBar("Change", "Select", false);
// Only 2 options
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(); } // Only 2 options
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 by %s\n", APP_VERSION, APP_AUTHOR);
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);
}
After uploading the code, restart the board. You should see the menu appear on the screen, allowing you to start live preview or begin timelapse recording.