Skip to content

webcam.tc

Webcam Security Camera Script

Source on GitHub

// Webcam Security Camera Script
// ESP32-CAM with motion detection, PIR alarm, picture saving, email alerts
// Features: Hardware PIR, software motion detector, timelapse, auto-cleanup
// Converted from Tasmota Scripter webcam script
//
// Assumes: LED flasher on GPIO relay (Power1), PIR sensor on GPIO 3
// Camera: QVGA resolution, continuous stream on port 81

// ─── Hardware config ───
#define PIR_PIN    3
#define CAM_QVGA   6


// ─── Persistent settings (survive reboot) ───
persist int limit;      // software motion alarm threshold
persist int pir;        // hardware PIR enabled
persist int ma;         // send email on alarm
persist int spir;       // software motion detector enabled
persist int rpt;        // take picture every N minutes (0=off)
persist int ufl;        // use flash LED
persist int mtl;        // merge pictures into timelapse mjpeg

// ─── Global state ───
int mot;                // current motion value
int bri;                // picture brightness
int cam_w;              // camera width
int cam_h;              // camera height
int alarm_active;       // 1 = alarm triggered, wait for motion to stop
int mailout;            // mail sent flag
int take_pic;           // button: take picture now
int del_all;            // button: delete all pictures
int stream_on;          // stream started flag
int last_hour;          // for midnight detection
int rpt_timer;          // countdown for periodic capture (seconds)
int sv_limit;   // shadow copies for change detection
int sv_pir;
int sv_ma;
int sv_spir;
int sv_rpt;
int sv_ufl;
int sv_mtl;

char buf[256];
char fnam[80];

// ─── Log message with timestamp ───
void log_msg(char msg[]) {
    char line[128];
    timeStamp(line);
    strcat(line, " - ");
    strcat(line, msg);
    fileLog("/log.txt", line, 8192);
}

// ─── Flash LED helpers ───
void flash_on() {
    if (ufl > 0) {
        tasm_power = 1;
        delay(150);
    }
}

void flash_off() {
    if (ufl > 0) {
        tasm_power = 0;
    }
}

// ─── Build timestamped filename into fnam ───
// Result: /PICS/27_2_2026-14_30_05.jpg
void build_pic_name() {
    strcpy(fnam, "/PICS/");
    sprintfAppend(fnam, "%d", tasm_day);
    strcat(fnam, "_");
    sprintfAppend(fnam, "%d", tasm_month);
    strcat(fnam, "_");
    sprintfAppend(fnam, "%d", tasm_year);
    strcat(fnam, "-");
    sprintfAppend(fnam, "%d", tasm_hour);
    strcat(fnam, "_");
    sprintfAppend(fnam, "%d", tasm_minute);
    strcat(fnam, "_");
    sprintfAppend(fnam, "%d", tasm_second);
    strcat(fnam, ".jpg");
}

// ─── Save picture to /PICS/day_month_year-hours_mins_secs.jpg ───
void save_pic() {
    flash_on();

    // Capture to RAM buffer 1
    camControl(1, 1, 0);

    build_pic_name();
    int fh = fileOpen(fnam, 'w');
    if (fh >= 0) {
        // savePic: write RAM buffer 1 to file handle
        camControl(7, 1, fh);
        fileClose(fh);
    } else {
        print("file io error\n");
    }

    flash_off();
}

// ─── Append picture to timelapse mjpeg file ───
void save_mjpeg() {
    flash_on();
    camControl(1, 1, 0);

    int fh = fileOpen("/PICS/tlapse.mjpeg", 'a');
    if (fh >= 0) {
        camControl(7, 1, fh);
        fileClose(fh);
    } else {
        print("file io error\n");
    }

    flash_off();
}

// ─── Delete all pictures in /PICS ───
void del_folder() {
    char msg[32];
    strcpy(msg, "delete all pictures");
    log_msg(msg);

    char entry[48];
    char path[80];
    int dh = fileOpenDir("/PICS");
    if (dh >= 0) {
        while (fileReadDir(dh, entry)) {
            strcpy(path, "/PICS/");
            strcat(path, entry);
            fileDelete(path);
        }
        fileClose(dh);
    }
}

// ─── Delete outdated files (14 days back) ───
void del_old() {
    char msg[32];
    strcpy(msg, "delete outdated pictures");
    log_msg(msg);

    // Build cutoff date string: "day_month_year"
    char ts[24];
    timeStamp(ts);
    timeOffset(ts, -14, 1);
    // ts = "2026-02-13T00:00:00" — extract date parts
    char dyear[8];
    char dmonth[4];
    char dday[4];
    strToken(dyear, ts, '-', 1);
    strToken(dmonth, ts, '-', 2);
    // day is before 'T'
    char rest[16];
    strToken(rest, ts, '-', 3);
    strToken(dday, rest, 'T', 1);
    // Build cutoff prefix: "13_2_2026"
    char cutoff[16];
    strcpy(cutoff, dday);
    strcat(cutoff, "_");
    strcat(cutoff, dmonth);
    strcat(cutoff, "_");
    strcat(cutoff, dyear);

    // Iterate directory and delete matching files
    char entry[48];
    char path[80];
    char fdate[16];
    int dh = fileOpenDir("/PICS");
    if (dh >= 0) {
        while (fileReadDir(dh, entry)) {
            // Extract date prefix from filename (before first '-')
            strToken(fdate, entry, '-', 1);
            if (strcmp(fdate, cutoff) == 0) {
                strcpy(path, "/PICS/");
                strcat(path, entry);
                fileDelete(path);
            }
        }
        fileClose(dh);
    }
}

// ─── Send alarm email with picture ───
void send_alarm() {
    char msg[32];
    strcpy(msg, "motion alarm");
    log_msg(msg);

    // Capture and save picture
    flash_on();
    camControl(1, 1, 0);
    build_pic_name();
    int fh = fileOpen(fnam, 'w');
    if (fh >= 0) {
        camControl(7, 1, fh);
        fileClose(fh);
    }
    flash_off();

    if (ma > 0) {
        char body[64];
        strcpy(body, "Motion alarm camera 1<br>");
        mailBody(body);
        mailAttachPic(1);
        char params[128];
        strcpy(params, "[*:*:*:*:*:your@email.com:Alarm]");
        mailSend(params);
        mailout = 1;
    }
}

// ─── Every Second callback ───
void EverySecond() {
    // Start stream when WiFi connects
    if (tasm_wifi > 0 && stream_on == 0) {
        camControl(5, 1, 0);
        stream_on = 1;
    }

    // Delete old pictures at midnight (hour transition to 0)
    if (tasm_hour == 0 && last_hour != 0) {
        del_old();
    }
    last_hour = tasm_hour;

    // Read motion value and brightness
    mot = camControl(6, -1, 0);
    bri = camControl(6, -2, 0);

    // Check PIR / software motion alarm (one-shot: fires once, re-arms when motion stops)
    if ((pir > 0 && digitalRead(PIR_PIN) == 0) ||
        (spir > 0 && mot > limit)) {
        if (alarm_active == 0) {
            alarm_active = 1;
            send_alarm();
        }
    } else {
        alarm_active = 0;
    }

    // Take picture on button press
    if (take_pic > 0) {
        save_pic();
        take_pic = 0;
    }

    // Periodic picture taking (every rpt minutes)
    if (rpt > 0) {
        if (rpt_timer > 0) {
            rpt_timer = rpt_timer - 1;
        }
        if (rpt_timer == 0) {
            rpt_timer = rpt * 60;
            if (mtl > 0) {
                save_mjpeg();
            } else {
                save_pic();
            }
        }
    } else {
        rpt_timer = 0;
    }

    // Delete all pictures on button press
    if (del_all > 0) {
        del_all = 0;
        del_folder();
    }

    // Auto-save persist variables when settings change
    if (limit != sv_limit || pir != sv_pir || ma != sv_ma ||
        spir != sv_spir || rpt != sv_rpt || ufl != sv_ufl || mtl != sv_mtl) {
        sv_limit = limit; sv_pir = pir; sv_ma = ma;
        sv_spir = spir; sv_rpt = rpt; sv_ufl = ufl; sv_mtl = mtl;
        saveVars();
    }
}

// ─── Web UI: sensor values (re-rendered on every sensor refresh) ───
void WebCall() {
    sprintf(buf, "{s}Motion{m}%d{e}", mot);
    webSend(buf);
    sprintf(buf, "{s}Brightness{m}%d{e}", bri);
    webSend(buf);
    sprintf(buf, "{s}Heap{m}%d kB{e}", tasm_heap / 1000);
    webSend(buf);
}

// ─── Web Page: camera stream (rendered once on page load) ───
// Chrome: MJPEG stream on port 81 (smooth video)
// Safari: periodic snapshot refresh (workaround for multipart/x-mixed-replace bug)
void WebPage() {
    strcpy(buf, "<br><img id=\"cs\" style=\"width:");
    sprintfAppend(buf, "%d", cam_w);
    strcat(buf, "px;height:");
    sprintfAppend(buf, "%d", cam_h);
    strcat(buf, "px\">");
    webSend(buf);
    webSend("<script>var cs=document.getElementById(\"cs\");");
    webSend("var h=\"http://\"+location.hostname;");
    webSend("if(/^((?!chrome|android).)*safari/i.test(navigator.userAgent)){");
    webSend("setInterval(function(){cs.src=h+\"/snapshot.jpg?\"+Date.now()},200)");
    webSend("}else{cs.src=h+\":81/stream\"}</script>");
    webSend("<br><br><center>webcam office</center>");
}

// ─── Web UI: controls ───
void WebUI() {
    webCheckbox(pir, "Hardware PIR");
    webCheckbox(spir, "Software motion detector");
    webNumber(limit, 50, 10000, "Software alarm limit");
    webCheckbox(ma, "Send email on alarm");
    webCheckbox(ufl, "Use flash LED");
    webNumber(rpt, 0, 60, "Take picture every N minutes");
    webCheckbox(mtl, "Merge into tlapse.mjpeg file");
    webButton(take_pic, "Take a picture");
    webButton(del_all, "Delete all pictures");
}

// ─── JSON output ───
void JsonCall() {
    sprintf(buf, ",\"Webcam\":{\"Motion\":%d,\"Brightness\":%d}", mot, bri);
    responseAppend(buf);
}

// ─── Cleanup on exit ───
void OnExit() {
    if (stream_on) {
        camControl(5, 0, 0);
        stream_on = 0;
    }
}

// ─── Main: initialization ───
int main() {
    mot = 0;
    bri = 0;
    mailout = 0;
    alarm_active = 1;  // suppress alarm until first motion-clear
    rpt_timer = rpt * 60;  // first capture after full interval
    take_pic = 0;
    del_all = 0;
    stream_on = 0;
    last_hour = -1;

    // Default alarm limit if not persisted
    if (limit == 0) {
        limit = 1000;
    }

    // Initialize shadow copies for change detection
    sv_limit = limit; sv_pir = pir; sv_ma = ma;
    sv_spir = spir; sv_rpt = rpt; sv_ufl = ufl; sv_mtl = mtl;

    // Delete stale persist file if values were corrupted (old siva bug wrote ASCII not int)
    if (pir > 1 || ma > 1 || spir > 1 || ufl > 1 || mtl > 1) {
        pir = 0; ma = 0; spir = 0; ufl = 0; mtl = 0;
        saveVars();
    }

    // Initialize camera at QVGA resolution
    camControl(0, CAM_QVGA, 0);
    cam_w = camControl(3, 0, 0);
    cam_h = camControl(4, 0, 0);

    // Start motion detector, check every 1000ms
    camControl(6, 1000, 0);

    // Create pictures folder
    if (!fileExists("/PICS")) {
        fileMkdir("/PICS");
    }

    // PIR input pin
    pinMode(PIR_PIN, 0);

    char msg[16];
    strcpy(msg, "booting");
    log_msg(msg);

    // Startup sound
    audioPlay("/Startup.mp3");

    sprintf(buf, "Webcam init OK, %d", cam_w);
    print(buf);
    sprintf(buf, "x%d\n", cam_h);
    print(buf);

    return 0;
}