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()