Files
mcp_screen/test_stream.py
T

215 lines
7.8 KiB
Python

#!/usr/bin/env python3
import socket
import time
import argparse
import sys
from PIL import Image, ImageDraw, ImageFont
# Dimensions of the reflective LCD
WIDTH = 400
HEIGHT = 300
def map_to_rlcd_hw_buffer(img_1bit):
"""Converts a standard 1-bit monochrome PIL Image to the Waveshare RLCD hardware buffer layout."""
px = img_1bit.load()
hw_buffer = bytearray(15000)
for y in range(HEIGHT):
for x in range(WIDTH):
if px[x, y]: # True if pixel is white (1)
inv_y = HEIGHT - 1 - y
byte_x = x // 2
block_y = inv_y // 4
index = byte_x * 75 + block_y
local_x = x % 2
local_y = inv_y % 4
bit = 7 - (local_y * 2 + local_x)
hw_buffer[index] |= (1 << bit)
return hw_buffer
def send_udp_frame_chunked(sock, esp32_ip, port, frame_idx, raw_bytes):
"""Sends a 15,000-byte frame split into 15 small UDP packets to bypass MTU and OS limits."""
frame_id = frame_idx % 256
for chunk_idx in range(15):
packet = bytearray(1002)
packet[0] = frame_id
packet[1] = chunk_idx
start = chunk_idx * 1000
packet[2:1002] = raw_bytes[start : start + 1000]
sock.sendto(packet, (esp32_ip, port))
time.sleep(0.010) # Add small pacing delay to prevent network buffer flooding
def generate_animation_frame(frame_idx, mode_name):
"""Generates a test pattern animation frame using Pillow."""
# Create 1-bit image (0 = black background)
img = Image.new("1", (WIDTH, HEIGHT), 0)
draw = ImageDraw.Draw(img)
# Draw header text
draw.text((10, 10), "ESP32-S3 VIDEO STREAM TEST", fill=1)
draw.line((10, 22, 390, 22), fill=1)
# Display active settings
draw.text((15, 30), f"Protocol : {mode_name}", fill=1)
draw.text((15, 45), f"Frame No : {frame_idx}", fill=1)
# 1. Draw a bouncing rectangle
rect_w, rect_h = 60, 40
bounce_x = int((frame_idx * 5) % (WIDTH - rect_w - 20)) + 10
bounce_y = int(120 + 20 * (frame_idx % 10 < 5 and (frame_idx % 5) or (5 - frame_idx % 5)))
draw.rectangle([bounce_x, bounce_y, bounce_x + rect_w, bounce_y + rect_h], outline=1, width=2)
draw.text((bounce_x + 10, bounce_y + 15), "TCP/UDP", fill=1)
# 2. Draw a spinning needle / line
import math
angle = frame_idx * 0.1
center_x, center_y = 300, 200
radius = 40
end_x = center_x + radius * math.cos(angle)
end_y = center_y + radius * math.sin(angle)
draw.ellipse([center_x - radius, center_y - radius, center_x + radius, center_y + radius], outline=1)
draw.line((center_x, center_y, end_x, end_y), fill=1)
# 3. Dynamic scrolling text at the bottom
scrolling_text = "Waveshare RLCD 4.2inch -- Low Power -- High Framerate Streaming in MicroPython"
scroll_pos = (frame_idx * 3) % 400
draw.text((10 - scroll_pos, 275), scrolling_text, fill=1)
draw.text((410 - scroll_pos, 275), scrolling_text, fill=1)
draw.line((10, 270, 390, 270), fill=1)
return img
def stream_camera(esp32_ip, port, protocol):
"""Streams camera capture frames using OpenCV (requires opencv-python)."""
try:
import cv2
except ImportError:
print("Error: opencv-python is not installed. Run 'pip install opencv-python' or use default animation mode.")
sys.exit(1)
print(f"Opening camera stream...")
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("Error: Could not open camera.")
sys.exit(1)
sock = None
if protocol == "tcp":
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((esp32_ip, port))
else: # udp
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Enable broadcast if destination is broadcast
if esp32_ip.endswith(".255") or esp32_ip == "255.255.255.255":
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
print(f"Streaming webcam to {esp32_ip}:{port} via {protocol.upper()}...")
frame_count = 0
start_time = time.time()
try:
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# Flip image, convert to RGB
frame = cv2.flip(frame, 1)
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_img = Image.fromarray(frame_rgb)
# Resize and dither
pil_img.thumbnail((WIDTH, HEIGHT))
bg = Image.new("1", (WIDTH, HEIGHT), 0)
offset = ((WIDTH - pil_img.width) // 2, (HEIGHT - pil_img.height) // 2)
bg.paste(pil_img.convert("1", dither=Image.FLOYDSTEINBERG), offset)
# Map pixels to hardware buffer
raw_bytes = map_to_rlcd_hw_buffer(bg)
# Transmit
if protocol == "tcp":
sock.sendall(raw_bytes)
else:
send_udp_frame_chunked(sock, esp32_ip, port, frame_count, raw_bytes)
frame_count += 1
elapsed = time.time() - start_time
if elapsed >= 2.0:
print(f"Streaming performance: {frame_count / elapsed:.1f} FPS")
frame_count = 0
start_time = time.time()
# Target ~30 FPS
time.sleep(0.033)
finally:
cap.release()
if sock:
sock.close()
def stream_animation(esp32_ip, port, protocol):
"""Streams a generated Pillow vector animation."""
sock = None
if protocol == "tcp":
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((esp32_ip, port))
else: # udp
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
if esp32_ip.endswith(".255") or esp32_ip == "255.255.255.255":
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
print(f"Streaming vector animation to {esp32_ip}:{port} via {protocol.upper()}...")
frame_idx = 0
start_time = time.time()
try:
while True:
# Generate frame
pil_img = generate_animation_frame(frame_idx, f"{protocol.upper()} Stream")
# Map pixels to hardware buffer
raw_bytes = map_to_rlcd_hw_buffer(pil_img)
# Transmit
if protocol == "tcp":
sock.sendall(raw_bytes)
else:
send_udp_frame_chunked(sock, esp32_ip, port, frame_idx, raw_bytes)
frame_idx += 1
# FPS tracking printout
if frame_idx % 60 == 0:
elapsed = time.time() - start_time
print(f"Streaming performance: {60 / elapsed:.1f} FPS")
start_time = time.time()
# Restrict rate to ~25 FPS to match update scan limits nicely
time.sleep(0.04)
except KeyboardInterrupt:
print("\nStreaming stopped.")
finally:
if sock:
sock.close()
def main():
parser = argparse.ArgumentParser(description="Host Client for ESP32-S3 TCP/UDP Video Streaming")
parser.add_argument("--ip", default="192.168.68.123", help="Target ESP32-S3 IP Address (default: 192.168.68.123)")
parser.add_argument("--protocol", choices=["tcp", "udp"], default="tcp", help="Streaming protocol (default: tcp)")
parser.add_argument("--source", choices=["camera", "animation"], default="animation", help="Streaming source (default: animation)")
args = parser.parse_args()
# Assign ports based on protocol selection
port = 8081 if args.protocol == "tcp" else 8082
if args.source == "camera":
stream_camera(args.ip, port, args.protocol)
else:
stream_animation(args.ip, port, args.protocol)
if __name__ == "__main__":
main()