commit b313720c81f26e471ba0f1cad7db9bba61bd931f Author: ti_mo Date: Mon Dec 22 02:32:18 2025 +0100 Initial push diff --git a/cam_to_serial/cam_to_serial.ino b/cam_to_serial/cam_to_serial.ino new file mode 100644 index 0000000..0275b5d --- /dev/null +++ b/cam_to_serial/cam_to_serial.ino @@ -0,0 +1,118 @@ +#include "esp_camera.h" +#define CAMERA_MODEL_AI_THINKER +#include "camera_pins.h" +#include + +void init_camera(){ + camera_config_t config; + config.ledc_channel = LEDC_CHANNEL_0; + config.ledc_timer = LEDC_TIMER_0; + config.pin_d0 = Y2_GPIO_NUM; + config.pin_d1 = Y3_GPIO_NUM; + config.pin_d2 = Y4_GPIO_NUM; + config.pin_d3 = Y5_GPIO_NUM; + config.pin_d4 = Y6_GPIO_NUM; + config.pin_d5 = Y7_GPIO_NUM; + config.pin_d6 = Y8_GPIO_NUM; + config.pin_d7 = Y9_GPIO_NUM; + config.pin_xclk = XCLK_GPIO_NUM; + config.pin_pclk = PCLK_GPIO_NUM; + config.pin_vsync = VSYNC_GPIO_NUM; + config.pin_href = HREF_GPIO_NUM; + config.pin_sccb_sda = SIOD_GPIO_NUM; + config.pin_sccb_scl = SIOC_GPIO_NUM; + config.pin_pwdn = PWDN_GPIO_NUM; + config.pin_reset = RESET_GPIO_NUM; + config.xclk_freq_hz = 20000000; + config.frame_size = FRAMESIZE_UXGA; + // config.pixel_format = PIXFORMAT_JPEG; // for streaming + config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition + config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; + config.fb_location = CAMERA_FB_IN_PSRAM; + config.jpeg_quality = 12; + config.fb_count = 1; + + // if PSRAM IC present, init with UXGA resolution and higher JPEG quality + // for larger pre-allocated frame buffer. + if (config.pixel_format == PIXFORMAT_JPEG) { + if (psramFound()) { + config.jpeg_quality = 10; + config.fb_count = 2; + config.grab_mode = CAMERA_GRAB_LATEST; + } else { + // Limit the frame size when PSRAM is not available + config.frame_size = FRAMESIZE_WQXGA; + config.fb_location = CAMERA_FB_IN_DRAM; + } + } else { + // Best option for face detection/recognition + config.frame_size = /*FRAMESIZE_QVGA;*/ FRAMESIZE_HVGA; +#if CONFIG_IDF_TARGET_ESP32S3 + config.fb_count = 2; +#endif + } + +#if defined(CAMERA_MODEL_ESP_EYE) + pinMode(13, INPUT_PULLUP); + pinMode(14, INPUT_PULLUP); +#endif + + // camera init + esp_err_t err = esp_camera_init(&config); + if (err != ESP_OK) { + Serial.printf("Camera init failed with error 0x%x", err); + return; + } + + sensor_t *s = esp_camera_sensor_get(); + // initial sensors are flipped vertically and colors are a bit saturated + if (s->id.PID == OV3660_PID) { + s->set_vflip(s, 1); // flip it back + s->set_brightness(s, 1); // up the brightness just a bit + s->set_saturation(s, -2); // lower the saturation + } + // drop down frame size for higher initial frame rate + if (config.pixel_format == PIXFORMAT_JPEG) { + s->set_framesize(s, FRAMESIZE_QVGA); + } + +#if defined(CAMERA_MODEL_M5STACK_WIDE) || defined(CAMERA_MODEL_M5STACK_ESP32CAM) + s->set_vflip(s, 1); + s->set_hmirror(s, 1); +#endif + +#if defined(CAMERA_MODEL_ESP32S3_EYE) + s->set_vflip(s, 1); +#endif +} + +void setup() { + Serial.begin(921600); + Serial.setDebugOutput(true); + Serial.println(); + + init_camera(); +} + +void loop() { + static int64_t last_frame = 0; + + camera_fb_t *framebuffer = esp_camera_fb_get(); + if(!framebuffer) return; + + uint8_t *image_buffer = NULL; + size_t image_buffer_len = 0; + + bool is_converted = frame2jpg(framebuffer, 80, &image_buffer, &image_buffer_len); + esp_camera_fb_return(framebuffer); + framebuffer = NULL; + if(!is_converted) return; + + Serial.write((const char*)image_buffer, image_buffer_len); + Serial.write("\4\r\n", 3); + + if(image_buffer) { + free(image_buffer); + image_buffer = NULL; + } +} diff --git a/cam_to_serial/camera_pins.h b/cam_to_serial/camera_pins.h new file mode 100644 index 0000000..3408508 --- /dev/null +++ b/cam_to_serial/camera_pins.h @@ -0,0 +1,338 @@ + +#if defined(CAMERA_MODEL_WROVER_KIT) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 21 +#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 19 +#define Y4_GPIO_NUM 18 +#define Y3_GPIO_NUM 5 +#define Y2_GPIO_NUM 4 +#define VSYNC_GPIO_NUM 25 +#define HREF_GPIO_NUM 23 +#define PCLK_GPIO_NUM 22 + +#elif defined(CAMERA_MODEL_ESP_EYE) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 4 +#define SIOD_GPIO_NUM 18 +#define SIOC_GPIO_NUM 23 + +#define Y9_GPIO_NUM 36 +#define Y8_GPIO_NUM 37 +#define Y7_GPIO_NUM 38 +#define Y6_GPIO_NUM 39 +#define Y5_GPIO_NUM 35 +#define Y4_GPIO_NUM 14 +#define Y3_GPIO_NUM 13 +#define Y2_GPIO_NUM 34 +#define VSYNC_GPIO_NUM 5 +#define HREF_GPIO_NUM 27 +#define PCLK_GPIO_NUM 25 + +#define LED_GPIO_NUM 22 + +#elif defined(CAMERA_MODEL_M5STACK_PSRAM) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM 15 +#define XCLK_GPIO_NUM 27 +#define SIOD_GPIO_NUM 25 +#define SIOC_GPIO_NUM 23 + +#define Y9_GPIO_NUM 19 +#define Y8_GPIO_NUM 36 +#define Y7_GPIO_NUM 18 +#define Y6_GPIO_NUM 39 +#define Y5_GPIO_NUM 5 +#define Y4_GPIO_NUM 34 +#define Y3_GPIO_NUM 35 +#define Y2_GPIO_NUM 32 +#define VSYNC_GPIO_NUM 22 +#define HREF_GPIO_NUM 26 +#define PCLK_GPIO_NUM 21 + +#elif defined(CAMERA_MODEL_M5STACK_V2_PSRAM) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM 15 +#define XCLK_GPIO_NUM 27 +#define SIOD_GPIO_NUM 22 +#define SIOC_GPIO_NUM 23 + +#define Y9_GPIO_NUM 19 +#define Y8_GPIO_NUM 36 +#define Y7_GPIO_NUM 18 +#define Y6_GPIO_NUM 39 +#define Y5_GPIO_NUM 5 +#define Y4_GPIO_NUM 34 +#define Y3_GPIO_NUM 35 +#define Y2_GPIO_NUM 32 +#define VSYNC_GPIO_NUM 25 +#define HREF_GPIO_NUM 26 +#define PCLK_GPIO_NUM 21 + +#elif defined(CAMERA_MODEL_M5STACK_WIDE) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM 15 +#define XCLK_GPIO_NUM 27 +#define SIOD_GPIO_NUM 22 +#define SIOC_GPIO_NUM 23 + +#define Y9_GPIO_NUM 19 +#define Y8_GPIO_NUM 36 +#define Y7_GPIO_NUM 18 +#define Y6_GPIO_NUM 39 +#define Y5_GPIO_NUM 5 +#define Y4_GPIO_NUM 34 +#define Y3_GPIO_NUM 35 +#define Y2_GPIO_NUM 32 +#define VSYNC_GPIO_NUM 25 +#define HREF_GPIO_NUM 26 +#define PCLK_GPIO_NUM 21 + +#define LED_GPIO_NUM 2 + +#elif defined(CAMERA_MODEL_M5STACK_ESP32CAM) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM 15 +#define XCLK_GPIO_NUM 27 +#define SIOD_GPIO_NUM 25 +#define SIOC_GPIO_NUM 23 + +#define Y9_GPIO_NUM 19 +#define Y8_GPIO_NUM 36 +#define Y7_GPIO_NUM 18 +#define Y6_GPIO_NUM 39 +#define Y5_GPIO_NUM 5 +#define Y4_GPIO_NUM 34 +#define Y3_GPIO_NUM 35 +#define Y2_GPIO_NUM 17 +#define VSYNC_GPIO_NUM 22 +#define HREF_GPIO_NUM 26 +#define PCLK_GPIO_NUM 21 + +#elif defined(CAMERA_MODEL_M5STACK_UNITCAM) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM 15 +#define XCLK_GPIO_NUM 27 +#define SIOD_GPIO_NUM 25 +#define SIOC_GPIO_NUM 23 + +#define Y9_GPIO_NUM 19 +#define Y8_GPIO_NUM 36 +#define Y7_GPIO_NUM 18 +#define Y6_GPIO_NUM 39 +#define Y5_GPIO_NUM 5 +#define Y4_GPIO_NUM 34 +#define Y3_GPIO_NUM 35 +#define Y2_GPIO_NUM 32 +#define VSYNC_GPIO_NUM 22 +#define HREF_GPIO_NUM 26 +#define PCLK_GPIO_NUM 21 + +#elif defined(CAMERA_MODEL_M5STACK_CAMS3_UNIT) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM 21 +#define XCLK_GPIO_NUM 11 +#define SIOD_GPIO_NUM 17 +#define SIOC_GPIO_NUM 41 + +#define Y9_GPIO_NUM 13 +#define Y8_GPIO_NUM 4 +#define Y7_GPIO_NUM 10 +#define Y6_GPIO_NUM 5 +#define Y5_GPIO_NUM 7 +#define Y4_GPIO_NUM 16 +#define Y3_GPIO_NUM 15 +#define Y2_GPIO_NUM 6 +#define VSYNC_GPIO_NUM 42 +#define HREF_GPIO_NUM 18 +#define PCLK_GPIO_NUM 12 + +#define LED_GPIO_NUM 14 + +#elif defined(CAMERA_MODEL_AI_THINKER) +#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 + +// 4 for flash led or 33 for normal led +#define LED_GPIO_NUM 4 + +#elif defined(CAMERA_MODEL_TTGO_T_JOURNAL) +#define PWDN_GPIO_NUM 0 +#define RESET_GPIO_NUM 15 +#define XCLK_GPIO_NUM 27 +#define SIOD_GPIO_NUM 25 +#define SIOC_GPIO_NUM 23 + +#define Y9_GPIO_NUM 19 +#define Y8_GPIO_NUM 36 +#define Y7_GPIO_NUM 18 +#define Y6_GPIO_NUM 39 +#define Y5_GPIO_NUM 5 +#define Y4_GPIO_NUM 34 +#define Y3_GPIO_NUM 35 +#define Y2_GPIO_NUM 17 +#define VSYNC_GPIO_NUM 22 +#define HREF_GPIO_NUM 26 +#define PCLK_GPIO_NUM 21 + +#elif defined(CAMERA_MODEL_XIAO_ESP32S3) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 10 +#define SIOD_GPIO_NUM 40 +#define SIOC_GPIO_NUM 39 + +#define Y9_GPIO_NUM 48 +#define Y8_GPIO_NUM 11 +#define Y7_GPIO_NUM 12 +#define Y6_GPIO_NUM 14 +#define Y5_GPIO_NUM 16 +#define Y4_GPIO_NUM 18 +#define Y3_GPIO_NUM 17 +#define Y2_GPIO_NUM 15 +#define VSYNC_GPIO_NUM 38 +#define HREF_GPIO_NUM 47 +#define PCLK_GPIO_NUM 13 + +#elif defined(CAMERA_MODEL_ESP32_CAM_BOARD) +// The 18 pin header on the board has Y5 and Y3 swapped +#define USE_BOARD_HEADER 0 +#define PWDN_GPIO_NUM 32 +#define RESET_GPIO_NUM 33 +#define XCLK_GPIO_NUM 4 +#define SIOD_GPIO_NUM 18 +#define SIOC_GPIO_NUM 23 + +#define Y9_GPIO_NUM 36 +#define Y8_GPIO_NUM 19 +#define Y7_GPIO_NUM 21 +#define Y6_GPIO_NUM 39 +#if USE_BOARD_HEADER +#define Y5_GPIO_NUM 13 +#else +#define Y5_GPIO_NUM 35 +#endif +#define Y4_GPIO_NUM 14 +#if USE_BOARD_HEADER +#define Y3_GPIO_NUM 35 +#else +#define Y3_GPIO_NUM 13 +#endif +#define Y2_GPIO_NUM 34 +#define VSYNC_GPIO_NUM 5 +#define HREF_GPIO_NUM 27 +#define PCLK_GPIO_NUM 25 + +#elif defined(CAMERA_MODEL_ESP32S3_CAM_LCD) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 40 +#define SIOD_GPIO_NUM 17 +#define SIOC_GPIO_NUM 18 + +#define Y9_GPIO_NUM 39 +#define Y8_GPIO_NUM 41 +#define Y7_GPIO_NUM 42 +#define Y6_GPIO_NUM 12 +#define Y5_GPIO_NUM 3 +#define Y4_GPIO_NUM 14 +#define Y3_GPIO_NUM 47 +#define Y2_GPIO_NUM 13 +#define VSYNC_GPIO_NUM 21 +#define HREF_GPIO_NUM 38 +#define PCLK_GPIO_NUM 11 + +#elif defined(CAMERA_MODEL_ESP32S2_CAM_BOARD) +// The 18 pin header on the board has Y5 and Y3 swapped +#define USE_BOARD_HEADER 0 +#define PWDN_GPIO_NUM 1 +#define RESET_GPIO_NUM 2 +#define XCLK_GPIO_NUM 42 +#define SIOD_GPIO_NUM 41 +#define SIOC_GPIO_NUM 18 + +#define Y9_GPIO_NUM 16 +#define Y8_GPIO_NUM 39 +#define Y7_GPIO_NUM 40 +#define Y6_GPIO_NUM 15 +#if USE_BOARD_HEADER +#define Y5_GPIO_NUM 12 +#else +#define Y5_GPIO_NUM 13 +#endif +#define Y4_GPIO_NUM 5 +#if USE_BOARD_HEADER +#define Y3_GPIO_NUM 13 +#else +#define Y3_GPIO_NUM 12 +#endif +#define Y2_GPIO_NUM 14 +#define VSYNC_GPIO_NUM 38 +#define HREF_GPIO_NUM 4 +#define PCLK_GPIO_NUM 3 + +#elif defined(CAMERA_MODEL_ESP32S3_EYE) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 15 +#define SIOD_GPIO_NUM 4 +#define SIOC_GPIO_NUM 5 + +#define Y2_GPIO_NUM 11 +#define Y3_GPIO_NUM 9 +#define Y4_GPIO_NUM 8 +#define Y5_GPIO_NUM 10 +#define Y6_GPIO_NUM 12 +#define Y7_GPIO_NUM 18 +#define Y8_GPIO_NUM 17 +#define Y9_GPIO_NUM 16 + +#define VSYNC_GPIO_NUM 6 +#define HREF_GPIO_NUM 7 +#define PCLK_GPIO_NUM 13 + +#elif defined(CAMERA_MODEL_DFRobot_FireBeetle2_ESP32S3) || defined(CAMERA_MODEL_DFRobot_Romeo_ESP32S3) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 45 +#define SIOD_GPIO_NUM 1 +#define SIOC_GPIO_NUM 2 + +#define Y9_GPIO_NUM 48 +#define Y8_GPIO_NUM 46 +#define Y7_GPIO_NUM 8 +#define Y6_GPIO_NUM 7 +#define Y5_GPIO_NUM 4 +#define Y4_GPIO_NUM 41 +#define Y3_GPIO_NUM 40 +#define Y2_GPIO_NUM 39 +#define VSYNC_GPIO_NUM 6 +#define HREF_GPIO_NUM 42 +#define PCLK_GPIO_NUM 5 + +#else +#error "Camera model not selected" +#endif diff --git a/serial_to_camstream.py b/serial_to_camstream.py new file mode 100644 index 0000000..f8c40b7 --- /dev/null +++ b/serial_to_camstream.py @@ -0,0 +1,161 @@ +import tkinter as tk +import serial +import serial.tools.list_ports +import threading +from PIL import Image, ImageTk +from io import BytesIO +import queue + +def connect() -> serial.Serial: + while True: + ports: list = serial.tools.list_ports.comports() + for i, port in enumerate(ports): + print(f"{i}: {port.device} - {port.description}") + + selected_port: str = input("Which port to use?\n") + if selected_port.__len__() < 1: continue + if(not "0123456789".__contains__(selected_port[0])): continue + + selected_port_idx: int = int(selected_port[0]) + if selected_port_idx >= ports.__len__() or selected_port_idx < 0: continue + + s: serial.Serial = serial.Serial(port=ports[selected_port_idx].device, baudrate=921600, timeout=1) + try_counter: int = 0 + while not s.is_open: + if try_counter == 10: + print("Failed to open port for 10 times -> stopping thread") + return + try: + s.open() + except: + print("Opening port failed!") + print(f"Try {try_counter}") + try_counter += 1 + + print(f"Connected successfully! ({ports[selected_port_idx].device})") + try: + s.reset_input_buffer() + except Exception: pass + return s + + +def receiver_thread(s: serial.Serial, kill_thread_event: threading.Event, image_queue: queue.Queue) -> None: + buf: bytearray = bytearray() + delimiter: str = b"\x04\r\n" + + while s.is_open and not kill_thread_event.is_set(): + try: + n = s.in_waiting + except Exception: + n = 1 + + chunk = s.read(n) + + if kill_thread_event.is_set(): return + if chunk: buf.extend(chunk) + + while True: + idx = buf.find(delimiter) + if idx == -1: break + + jpg: bytes = bytes(buf[:idx]) + del buf[: idx + delimiter.__len__()] + if not jpg: continue + + try: + image_queue.put_nowait(jpg) + except queue.Full: + try: + _ = image_queue.get_nowait() + except queue.Empty: pass + + try: + image_queue.put_nowait(jpg) + except queue.Full: pass + + if buf.__len__() > 5000000: buf.clear() + + +def detect_motion(last_image: bytes, new_image: bytes) -> bool: + length1: int = last_image.__len__() + length2: int = new_image.__len__() + length = length1 + if length1 < length2: + new_image = new_image[:length1] + elif length2 < length1: + last_image = last_image[:length2] + length = length2 + + def diff(pixel1, pixel2) -> int: + return pixel1 - pixel2 + + sum: int = 0 + for i in range(length): + sum += diff(last_image[i], new_image[i]) + + norm = sum / length + ret = norm <= -1 or norm >= 1 + # print(f"Norm: {norm}") + return ret + +def main(): + kill_thread_event: threading.Event = threading.Event() + image_queue: queue.Queue = queue.Queue(maxsize=2) + + s: serial.Serial = connect() + + receiver = threading.Thread(target=receiver_thread, daemon=True, args=(s, kill_thread_event, image_queue)) + receiver.start() + + root = tk.Tk() + root.title("ESP32-CAM Stream") + root.geometry("1280x720") + lbl = tk.Label(root) + lbl.pack(fill="both", expand=True) + + state = {"photo": None} + + last_image: bytes | None = None + + def poll_image(): + nonlocal last_image + jpg = None + while True: + try: + jpg = image_queue.get_nowait() + except queue.Empty: + break + + if jpg is not None: + if last_image is not None and detect_motion(last_image, jpg): + print("Motion detected!") + + last_image = jpg + + try: + img = Image.open(BytesIO(jpg)).convert("RGB") + + w = lbl.winfo_width() + h = lbl.winfo_height() + if w > 1 and h > 1: + img = img.resize((w, h), Image.BILINEAR) + + photo = ImageTk.PhotoImage(img) + state["photo"] = photo + lbl.configure(image=photo) + except Exception: pass + + root.after(10, poll_image) + + def on_close(): + kill_thread_event.set() + root.destroy() + print("Closing!") + + root.protocol("WM_DELETE_WINDOW", on_close) + poll_image() + root.mainloop() + s.close() + +if __name__ == '__main__': + main()