Implement notetaking app, ST7796 display with touch support, audio beep navigation, and video streaming
This commit is contained in:
+214
@@ -0,0 +1,214 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user