webcam.tc¶
Webcam Security Camera Script
// 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;
}