219 lines
6.3 KiB
C++
219 lines
6.3 KiB
C++
/*
|
|
* esp32-cam-lcd-status.ino
|
|
*
|
|
* Simpler companion to esp32-cam-lcd-preview.ino: shows a STATUS
|
|
* dashboard on the ILI9341 instead of decoding the JPEG. Useful if
|
|
* jpg2rgb565() fails on truncated frames (a known limitation when
|
|
* the source JPEG > 9 KiB — see the project README).
|
|
*
|
|
* What you see on the TFT:
|
|
* - Frame counter (incrementing as frames arrive)
|
|
* - Last frame size in bytes
|
|
* - Average fps
|
|
* - JPEG header byte dump (first 16 bytes — visualises the SOI
|
|
* marker FF D8 FF and APP0 JFIF on each frame)
|
|
* - Histogram of byte distribution in the JPEG (shows webcam
|
|
* activity visually — bright frames vs dark frames distribute
|
|
* bytes differently)
|
|
*
|
|
* Wiring is the same as esp32-cam-lcd-preview.ino — see that file or
|
|
* the README. Libraries needed: Adafruit GFX + Adafruit ILI9341.
|
|
*/
|
|
|
|
#include "esp_camera.h"
|
|
|
|
#include <SPI.h>
|
|
#include <Adafruit_GFX.h>
|
|
#include <Adafruit_ILI9341.h>
|
|
|
|
// Camera pinout — AI-Thinker ESP32-CAM
|
|
#define PWDN_GPIO_NUM 32
|
|
#define RESET_GPIO_NUM -1
|
|
#define XCLK_GPIO_NUM 0
|
|
#define SIOD_GPIO_NUM 26
|
|
#define SIOC_GPIO_NUM 27
|
|
#define Y9_GPIO_NUM 35
|
|
#define Y8_GPIO_NUM 34
|
|
#define Y7_GPIO_NUM 39
|
|
#define Y6_GPIO_NUM 36
|
|
#define Y5_GPIO_NUM 21
|
|
#define Y4_GPIO_NUM 19
|
|
#define Y3_GPIO_NUM 18
|
|
#define Y2_GPIO_NUM 5
|
|
#define VSYNC_GPIO_NUM 25
|
|
#define HREF_GPIO_NUM 23
|
|
#define PCLK_GPIO_NUM 22
|
|
|
|
// TFT pinout
|
|
#define TFT_CS 15
|
|
#define TFT_DC 14
|
|
#define TFT_RST 2
|
|
#define TFT_MOSI 13
|
|
#define TFT_SCK 12
|
|
|
|
SPIClass tftSPI(VSPI);
|
|
Adafruit_ILI9341 tft = Adafruit_ILI9341(&tftSPI, TFT_DC, TFT_CS, TFT_RST);
|
|
|
|
uint32_t frame_count = 0;
|
|
uint32_t total_bytes = 0;
|
|
unsigned long start_ms = 0;
|
|
|
|
void draw_static() {
|
|
tft.fillScreen(ILI9341_BLACK);
|
|
|
|
// Title bar
|
|
tft.fillRect(0, 0, 320, 24, ILI9341_NAVY);
|
|
tft.setTextSize(2);
|
|
tft.setTextColor(ILI9341_WHITE);
|
|
tft.setCursor(8, 4);
|
|
tft.print("VELXIO ESP32-CAM");
|
|
|
|
// Section labels
|
|
tft.setTextSize(1);
|
|
tft.setTextColor(ILI9341_CYAN);
|
|
tft.setCursor(8, 36); tft.print("STATS");
|
|
tft.setCursor(8, 110); tft.print("JPEG HEADER (first 16 bytes)");
|
|
tft.setCursor(8, 168); tft.print("BYTE HISTOGRAM (8 buckets)");
|
|
}
|
|
|
|
void draw_stats(size_t fb_len) {
|
|
// Erase + redraw stats section
|
|
tft.fillRect(0, 50, 320, 50, ILI9341_BLACK);
|
|
tft.setTextSize(2);
|
|
tft.setTextColor(ILI9341_GREEN);
|
|
|
|
tft.setCursor(8, 54);
|
|
tft.printf("frame %5u", (unsigned)frame_count);
|
|
|
|
tft.setCursor(8, 76);
|
|
tft.printf("size %5u B", (unsigned)fb_len);
|
|
|
|
unsigned long elapsed = millis() - start_ms;
|
|
float fps = elapsed > 0 ? (1000.0f * frame_count) / (float)elapsed : 0.0f;
|
|
tft.setTextColor(ILI9341_YELLOW);
|
|
tft.setCursor(180, 54);
|
|
tft.printf("fps %4.1f", fps);
|
|
|
|
tft.setCursor(180, 76);
|
|
tft.printf("avg %4u B",
|
|
frame_count ? (unsigned)(total_bytes / frame_count) : 0);
|
|
|
|
// Pulse dot — proves the loop is running each frame
|
|
uint16_t dot = (frame_count & 1) ? ILI9341_RED : ILI9341_DARKGREY;
|
|
tft.fillCircle(304, 12, 6, dot);
|
|
}
|
|
|
|
void draw_header_dump(const uint8_t* buf, size_t len) {
|
|
// Erase + redraw 16-byte hex dump
|
|
tft.fillRect(0, 124, 320, 36, ILI9341_BLACK);
|
|
tft.setTextSize(2);
|
|
// SOI bytes (FF D8 FF) coloured green if valid
|
|
bool soi_ok = (len >= 3) && buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF;
|
|
tft.setCursor(8, 128);
|
|
for (size_t i = 0; i < 8 && i < len; i++) {
|
|
tft.setTextColor(i < 3 && soi_ok ? ILI9341_GREEN : ILI9341_WHITE);
|
|
tft.printf("%02X ", buf[i]);
|
|
}
|
|
tft.setCursor(8, 146);
|
|
for (size_t i = 8; i < 16 && i < len; i++) {
|
|
tft.setTextColor(ILI9341_WHITE);
|
|
tft.printf("%02X ", buf[i]);
|
|
}
|
|
}
|
|
|
|
void draw_histogram(const uint8_t* buf, size_t len) {
|
|
// 8 buckets: 0x00-0x1F, 0x20-0x3F, ..., 0xE0-0xFF
|
|
// Sample stride = max(1, len/2048) so this stays cheap
|
|
uint32_t bucket[8] = {0};
|
|
size_t stride = len / 2048;
|
|
if (stride < 1) stride = 1;
|
|
for (size_t i = 0; i < len; i += stride) {
|
|
bucket[buf[i] >> 5]++;
|
|
}
|
|
|
|
// Find max for scaling
|
|
uint32_t maxv = 1;
|
|
for (int i = 0; i < 8; i++) if (bucket[i] > maxv) maxv = bucket[i];
|
|
|
|
// Draw 8 bars in 304 px wide area, 60 px tall, starting at y=180
|
|
const int BAR_X = 8, BAR_Y = 184, BAR_W = 36, BAR_GAP = 2, BAR_H = 50;
|
|
tft.fillRect(0, BAR_Y, 320, BAR_H + 4, ILI9341_BLACK);
|
|
uint16_t colors[8] = {
|
|
ILI9341_NAVY, ILI9341_BLUE, ILI9341_DARKCYAN, ILI9341_CYAN,
|
|
ILI9341_GREEN, ILI9341_YELLOW, ILI9341_ORANGE, ILI9341_RED,
|
|
};
|
|
for (int i = 0; i < 8; i++) {
|
|
int h = (int)(((uint64_t)bucket[i] * BAR_H) / maxv);
|
|
int x = BAR_X + i * (BAR_W + BAR_GAP);
|
|
tft.fillRect(x, BAR_Y + (BAR_H - h), BAR_W, h, colors[i]);
|
|
}
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
delay(1000);
|
|
Serial.println();
|
|
Serial.println("=== Velxio ESP32-CAM + ILI9341 status dashboard ===");
|
|
|
|
tftSPI.begin(TFT_SCK, -1, TFT_MOSI, TFT_CS);
|
|
tft.begin();
|
|
tft.setRotation(1);
|
|
tft.fillScreen(ILI9341_NAVY);
|
|
tft.setTextSize(2);
|
|
tft.setTextColor(ILI9341_WHITE);
|
|
tft.setCursor(40, 90);
|
|
tft.print("Booting camera...");
|
|
|
|
camera_config_t cfg = {};
|
|
cfg.ledc_channel = LEDC_CHANNEL_0;
|
|
cfg.ledc_timer = LEDC_TIMER_0;
|
|
cfg.pin_d0 = Y2_GPIO_NUM;
|
|
cfg.pin_d1 = Y3_GPIO_NUM;
|
|
cfg.pin_d2 = Y4_GPIO_NUM;
|
|
cfg.pin_d3 = Y5_GPIO_NUM;
|
|
cfg.pin_d4 = Y6_GPIO_NUM;
|
|
cfg.pin_d5 = Y7_GPIO_NUM;
|
|
cfg.pin_d6 = Y8_GPIO_NUM;
|
|
cfg.pin_d7 = Y9_GPIO_NUM;
|
|
cfg.pin_xclk = XCLK_GPIO_NUM;
|
|
cfg.pin_pclk = PCLK_GPIO_NUM;
|
|
cfg.pin_vsync = VSYNC_GPIO_NUM;
|
|
cfg.pin_href = HREF_GPIO_NUM;
|
|
cfg.pin_sccb_sda = SIOD_GPIO_NUM;
|
|
cfg.pin_sccb_scl = SIOC_GPIO_NUM;
|
|
cfg.pin_pwdn = PWDN_GPIO_NUM;
|
|
cfg.pin_reset = RESET_GPIO_NUM;
|
|
cfg.xclk_freq_hz = 20000000;
|
|
cfg.pixel_format = PIXFORMAT_JPEG;
|
|
cfg.frame_size = FRAMESIZE_QVGA;
|
|
cfg.jpeg_quality = 12;
|
|
cfg.fb_count = 1;
|
|
cfg.fb_location = CAMERA_FB_IN_DRAM;
|
|
|
|
esp_err_t err = esp_camera_init(&cfg);
|
|
if (err != ESP_OK) {
|
|
tft.setTextColor(ILI9341_RED);
|
|
tft.setCursor(40, 130);
|
|
tft.print("camera_init FAIL");
|
|
while (1) delay(1000);
|
|
}
|
|
|
|
draw_static();
|
|
start_ms = millis();
|
|
}
|
|
|
|
void loop() {
|
|
camera_fb_t* fb = esp_camera_fb_get();
|
|
if (!fb) { delay(50); return; }
|
|
|
|
frame_count++;
|
|
total_bytes += fb->len;
|
|
|
|
draw_stats(fb->len);
|
|
draw_header_dump(fb->buf, fb->len);
|
|
draw_histogram(fb->buf, fb->len);
|
|
|
|
esp_camera_fb_return(fb);
|
|
delay(50);
|
|
}
|