Zum Inhalt

camera.tc

ESP32 Camera — TinyC direct esp_camera driver

Source on GitHub

// ═══════════════════════════════════════════════════════════════════
// ESP32 Camera — TinyC direct esp_camera driver
// Supports multiple boards via compile-time #define
// Compile: node compile_cli.js camera.tc camera.tcb -DBOARD_DFROBOT
//      or: node compile_cli.js camera.tc camera.tcb -DBOARD_GOOUUU
//      or: node compile_cli.js camera.tc camera.tcb -DBOARD_AITHINKER
// Live view: http://<device-ip>/cam  (MJPEG stream from port 81)
// ═══════════════════════════════════════════════════════════════════
// @defines: -DBOARD_DFROBOT

// ─── Camera pixel formats ───
#define PIXFORMAT_RGB565    0
#define PIXFORMAT_YUV422    1
#define PIXFORMAT_YUV420    2
#define PIXFORMAT_GRAYSCALE 3
#define PIXFORMAT_JPEG      4
#define PIXFORMAT_RGB888    5

// ─── Frame sizes ───
#define FRAMESIZE_96X96     0
#define FRAMESIZE_QQVGA     1
#define FRAMESIZE_QCIF      2
#define FRAMESIZE_HQVGA     3
#define FRAMESIZE_240X240   4
#define FRAMESIZE_QVGA      5
#define FRAMESIZE_CIF       6
#define FRAMESIZE_HVGA      7
#define FRAMESIZE_VGA       8
#define FRAMESIZE_SVGA      9
#define FRAMESIZE_XGA       10
#define FRAMESIZE_HD        11
#define FRAMESIZE_SXGA      12
#define FRAMESIZE_UXGA      13

// ─── Sensor parameter IDs (for camControl sel=9) ───
#define CAM_VFLIP           0
#define CAM_BRIGHTNESS      1
#define CAM_SATURATION      2
#define CAM_HMIRROR         3
#define CAM_CONTRAST        4
#define CAM_FRAMESIZE       5
#define CAM_QUALITY         6
#define CAM_SHARPNESS       7
#define CAM_SPECIAL_EFFECT  8
#define CAM_WHITEBAL        9
#define CAM_AWB_GAIN        10
#define CAM_WB_MODE         11
#define CAM_EXPOSURE_CTRL   12
#define CAM_AEC2            13
#define CAM_AE_LEVEL        14
#define CAM_AEC_VALUE       15
#define CAM_GAIN_CTRL       16
#define CAM_AGC_GAIN        17
#define CAM_GAINCEILING     18
#define CAM_LENC            19
#define CAM_RAW_GMA         20

// ─── Known sensor PIDs ───
#define OV2640_PID          0x2642
#define OV3660_PID          0x3660
#define OV5640_PID          0x5640

// ═══════════════════════════════════════════════════════════════════
// Board-specific pin definitions
// Order: pwdn, reset, xclk, sda, scl, d7, d6, d5, d4, d3, d2, d1, d0, vsync, href, pclk
// ═══════════════════════════════════════════════════════════════════
#ifdef BOARD_DFROBOT
// DFRobot Firebeetle 2 ESP32-S3 (OV3660)
int campins[] = {-1, -1, 5, 8, 9, 4, 6, 7, 14, 17, 21, 18, 16, 1, 2, 15};
#endif

#ifdef BOARD_GOOUUU
// Goouuu ESP32-S3-CAM / ESP32S3_EYE (OV2640)
int campins[] = {-1, -1, 15, 4, 5, 16, 17, 18, 12, 10, 8, 9, 11, 6, 7, 13};
#endif

#ifdef BOARD_AITHINKER
// AI-Thinker ESP32-CAM (OV2640), flash LED on GPIO4
int campins[] = {32, -1, 0, 26, 27, 35, 34, 39, 36, 21, 19, 18, 5, 25, 23, 22};
#endif

int pic_count;
int frame_count;
int cam_ok;
int last_size;
int motion_val;
int motion_detect;
char buf[128];
char path[64];

// ═══════════════════════════════════════════════════════════════════
// Initialize camera with board-specific pins
// cameraInit(pins[], format, framesize, quality, xclk_freq, fb_count, grab_mode, fb_loc)
//   xclk_freq: Hz (0 = 20MHz default)
//   fb_count:  0 = auto (1 no PSRAM, 2 with PSRAM), >0 = explicit
//   grab_mode: -1 = auto, 0 = GRAB_WHEN_EMPTY, 1 = GRAB_LATEST
//   fb_loc:    -1 = auto (PSRAM if available), 1 = force DRAM
// ═══════════════════════════════════════════════════════════════════
int initCamera() {
    int ok;

    ok = cameraInit(campins, PIXFORMAT_JPEG, FRAMESIZE_VGA, 12, 0, 0, -1, -1);
    if (ok != 0) {
        print(ok);
        addLog("TCC: camera init FAILED");
        return -1;
    }
    addLog("TCC: camera initialized OK");

    int pid = camControl(8, 0, 0);
    sprintf(buf, "TCC: sensor PID = 0x%x", pid);
    addLog(buf);

    if (pid == OV3660_PID) {
        camControl(9, CAM_VFLIP, 1);
        camControl(9, CAM_BRIGHTNESS, 1);
        camControl(9, CAM_SATURATION, -2);
        addLog("TCC: OV3660 settings applied");
    }

    return 0;
}

// ═══════════════════════════════════════════════════════════════════
// Capture to PSRAM slot 1 (for live view via /tc_cam endpoint)
// ═══════════════════════════════════════════════════════════════════
int captureToSlot() {
    int size;
    size = camControl(10, 1, 0);
    if (size <= 0) {
        return -1;
    }
    frame_count = frame_count + 1;
    return size;
}

// ═══════════════════════════════════════════════════════════════════
// Capture to PSRAM slot 2 and save as numbered picture file
// ═══════════════════════════════════════════════════════════════════
int takePicture() {
    int size;
    int fh;
    int written;

    size = camControl(10, 2, 0);
    if (size <= 0) {
        addLog("TCC: capture failed");
        return -1;
    }

    pic_count = pic_count + 1;
    sprintf(path, "/pic_%03d.jpg", pic_count);

    fh = fileOpen(path, 1);
    if (fh < 0) {
        return -1;
    }

    written = camControl(11, 2, fh);
    fileClose(fh);

    sprintf(buf, "TCC: saved %d bytes to %s", written, path);
    addLog(buf);

    return written;
}

// ═══════════════════════════════════════════════════════════════════
// Custom web handler: /cam — MJPEG stream page
// ═══════════════════════════════════════════════════════════════════
void WebOn() {
    int h;
    h = webHandler();
    if (h == 1) {
        webSend("<html><head><title>TinyC Camera</title>");
        webSend("<style>body{background:#000;margin:0;text-align:center}");
        webSend("img{max-width:100%;height:auto;margin-top:20px}</style></head>");
        webSend("<body><h2 style='color:#0ff'>TinyC Camera Live View</h2>");
        webSend("<img id='img'/>");
        webSend("<p style='color:#888;font-size:12px'>");
        webSend("<a id='snap' href='#' style='color:#0ff'>Snapshot</a>");
        webSend(" | <a id='strm' href='#' style='color:#0ff'>Direct stream</a></p>");
        webSend("<script>var h=location.hostname,img=document.getElementById('img');");
        webSend("document.getElementById('snap').href='/tc_cam?slot=1';");
        webSend("document.getElementById('strm').href='http://'+h+':81/cam.mjpeg';");
        webSend("if(/^((?!chrome|android).)*safari/i.test(navigator.userAgent)){");
        webSend("setInterval(function(){img.src='/tc_cam?slot=1&t='+Date.now()},200)");
        webSend("}else{img.src='http://'+h+':81/cam.mjpeg'}</script>");
        webSend("</body></html>");
    }
}

// ═══════════════════════════════════════════════════════════════════
// WebUI — control panel on Tasmota main page
// ═══════════════════════════════════════════════════════════════════
int doCapture;
int doHiRes;

void WebUI() {
    webButton(doCapture, "Take Picture");
    webButton(doHiRes, "Hi-Res Capture");
}

// ═══════════════════════════════════════════════════════════════════
// WebCall — show status on Tasmota main page
// ═══════════════════════════════════════════════════════════════════
void WebCall() {
    webSend("{s}<b style='color:cyan'>Camera</b>{m}{e}");
    sprintf(buf, "{s}Frames{m}%d{e}", frame_count);
    webSend(buf);
    sprintf(buf, "{s}Motion{m}%d{e}", motion_val);
    webSend(buf);
    sprintf(buf, "{s}Pictures saved{m}%d{e}", pic_count);
    webSend(buf);
    webSend("{s}Live view{m}<a href='/cam' target='_blank'>Open /cam</a>{e}");
}

// ═══════════════════════════════════════════════════════════════════
// TaskLoop — runs in VM task thread (required for camera capture)
// ═══════════════════════════════════════════════════════════════════
void TaskLoop() {
    if (cam_ok != 1) {
        delay(100);
        return;
    }

    // Continuous capture to PSRAM slot 1 (served via /tc_cam and MJPEG stream)
    int size = captureToSlot();

    // Simple motion detection via JPEG size delta
    if (last_size > 0 && size > 0) {
        int diff = size - last_size;
        if (diff < 0) { diff = 0 - diff; }
        motion_val = diff;
        if (diff > 400) {
            motion_detect = motion_detect + 1;
        }
    }
    if (size > 0) { last_size = size; }

    // Button: take numbered picture
    if (doCapture == 1) {
        doCapture = 0;
        takePicture();
    }

    // Button: hi-res capture (UXGA, then back to VGA)
    if (doHiRes == 1) {
        doHiRes = 0;
        camControl(9, CAM_FRAMESIZE, FRAMESIZE_UXGA);
        delay(500);
        takePicture();
        camControl(9, CAM_FRAMESIZE, FRAMESIZE_VGA);
    }

    // ~10 fps for smooth MJPEG stream
    delay(100);
}

// ═══════════════════════════════════════════════════════════════════
// OnExit — clean up camera resources
// ═══════════════════════════════════════════════════════════════════
void OnExit() {
    camControl(15, 0, 0);   // stop MJPEG stream server
    camControl(12, 0, 0);   // free all PSRAM slots (slot 0 = free all)
    camControl(13, 0, 0);   // deinit camera
    addLog("TCC: camera shut down");
}

// ═══════════════════════════════════════════════════════════════════
// MAIN
// ═══════════════════════════════════════════════════════════════════
int main() {
    pic_count = 0;
    frame_count = 0;
    doCapture = 0;
    doHiRes = 0;
    cam_ok = 0;
    last_size = 0;
    motion_val = 0;
    motion_detect = 0;

    // Register custom web endpoint: /cam
    webOn(1, "/cam");

    int ok = initCamera();
    if (ok != 0) {
        return -1;
    }
    cam_ok = 1;

    // Start MJPEG stream server on port 81
    camControl(15, 1, 0);

    // Take initial test capture to PSRAM slot 1
    int size = captureToSlot();
    sprintf(buf, "Camera ready, first frame: %d bytes, heap: %d", size, tasm_heap);
    addLog(buf);

    return 0;
}