diff --git a/README.md b/README.md
new file mode 100644
index 0000000..490c644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,203 @@
+# Project stream frame via websocket using ESP32 to LED Panel 128x128
+
+### Pinout
+![](/img/connector.jpg)
+
+
+### Example code to send image/gif.video... to ESP32
+
+```python
+from PIL import Image, ImageSequence
+import socket
+import struct
+import time
+import cv2
+import serial
+from flask import Flask, request, redirect, make_response
+import os
+import asyncio
+import websockets
+import threading
+### Flask configuration
+app = Flask(__name__)
+UPLOAD_FOLDER = 'uploads'
+RESIZED_FOLDER = 'resized'
+os.makedirs(UPLOAD_FOLDER, exist_ok=True)
+os.makedirs(RESIZED_FOLDER, exist_ok=True)
+
+### Socket configuration
+sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+udp_ip = '192.168.2.243' # IP của ESP32
+udp_port = 5000
+uri = "ws://192.168.2.243:81"
+media_path = '/home/qthi/Documents/quyth/Image/128x128/gif2.gif'
+
+# To manage the WebSocket thread
+websocket_thread = None
+websocket_stop_event = threading.Event()
+
+### Page
+@app.route('/')
+def upload_form():
+ return '''
+
+ Upload Image
+
+
+ '''
+frames = []
+@app.route('/upload', methods=['POST'])
+def upload_file():
+ if 'file' not in request.files:
+ return redirect(request.url)
+
+ file = request.files['file']
+ if file.filename == '':
+ return redirect(request.url)
+
+ if file:
+ filepath = os.path.join(UPLOAD_FOLDER, file.filename)
+ file.save(filepath)
+ frames = resize_gif_and_get_colors(filepath, (128, 128))
+ threading.Thread(target=start_websocket_thread, args=(frames,)).start()
+ # return send_file(resized_filepath, as_attachment=True)
+ return make_response('OK',200)
+
+def resize_gif_and_get_colors(input_path, new_size):
+ """
+ Thay đổi kích thước từng khung hình của ảnh GIF và trả về mảng màu RGB565 cho từng khung hình.
+
+ :param input_path: Đường dẫn đến ảnh GIF gốc
+ :param new_size: Kích thước mới (width, height)
+ :return: Danh sách các mảng màu của từng khung hình
+ """
+ frames_colors = []
+
+ with Image.open(input_path) as img:
+ for frame in ImageSequence.Iterator(img):
+ frame = frame.convert('RGB').resize(new_size, Image.ANTIALIAS)
+ width, height = frame.size
+ frame_data = bytearray()
+ for y in range(height):
+ for x in range(width):
+ r, g, b = frame.getpixel((x, y))
+ rgb565 = rgb888_to_rgb565(r, g, b)
+ frame_data.extend(struct.pack('!' + 'H', rgb565))
+
+ frames_colors.append((frame_data,width,height))
+
+ return frames_colors
+
+def rgb888_to_rgb565(r, g, b):
+ """Chuyển đổi từ RGB888 sang RGB565"""
+ return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
+def load_image_to_rgb565(file_path):
+ """Tải tệp ảnh và chuyển đổi từng pixel sang định dạng RGB565"""
+ img = Image.open(file_path)
+ img = img.convert('RGB') # Đảm bảo ảnh ở định dạng RGB
+
+ width, height = img.size
+ frame_data = []
+
+ for y in range(height):
+ for x in range(width):
+ r, g, b = img.getpixel((x, y))
+ rgb565 = rgb888_to_rgb565(r, g, b)
+ frame_data.append(rgb565)
+ print(frame_data)
+ return frame_data
+def load_gif_to_rgb565(file_path):
+ """Tải tệp GIF và chuyển đổi từng khung hình sang định dạng RGB565"""
+ gif = Image.open(file_path)
+ frames = []
+
+ for frame in ImageSequence.Iterator(gif):
+ frame = frame.convert('RGB')
+ width, height = frame.size
+ frame_data = []
+ for y in range(height):
+ for x in range(width):
+ r, g, b = frame.getpixel((x, y))
+ rgb565 = rgb888_to_rgb565(r, g, b)
+ frame_data.append(rgb565)
+ frames.append((frame_data, width, height))
+
+ return frames
+def load_video_to_rgb565(file_path):
+ """Tải video và chuyển đổi từng khung hình sang định dạng RGB565"""
+ cap = cv2.VideoCapture(file_path)
+ frames = []
+
+ while True:
+ ret, frame = cap.read()
+ if not ret:
+ break
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
+ height, width = frame.shape[:2]
+ frame_data = []
+ for y in range(height):
+ for x in range(width):
+ r, g, b = frame[y, x]
+ rgb565 = rgb888_to_rgb565(r, g, b)
+ frame_data.append(rgb565)
+ frames.append((frame_data, width, height))
+ print(frames)
+ cap.release()
+ return frames
+def send_frame(frame_data, udp_ip, udp_port):
+ """Gửi khung hình qua UDP"""
+
+ packet_size = 1460 # Kích thước gói tối đa để tránh tràn UDP
+ num_packets = (len(frame_data) * 2 + packet_size - 1) // packet_size
+ for i in range(num_packets):
+ start_index = i * packet_size // 2
+ end_index = min((i + 1) * packet_size // 2, len(frame_data))
+ packet = struct.pack('!' + 'H' * (end_index - start_index), *frame_data[start_index:end_index])
+ sock.sendto(packet, (udp_ip, udp_port))
+ # time.sleep(0.1)
+
+def send_data_via_serial(port, baudrate, data):
+ with serial.Serial(port, baudrate, timeout=1) as ser:
+ # Gửi mảng dữ liệu
+ for value in data:
+ # Gửi giá trị RGB565 (2 byte)
+ ser.write(struct.pack(' bằng địa chỉ IP của ESP32
+ # uri = "ws://localhost:8765" # Thay bằng địa chỉ IP của ESP32
+ #frames = load_gif_to_rgb565(file_path)
+ async with websockets.connect(uri) as websocket:
+ while not stop_event.is_set():
+ #frame_data = load_image_to_rgb565(file_path)
+ # frame_data = create_test_pattern()
+ for frame_data, _, _ in frames:
+ # Chia nhỏ dữ liệu nếu cần (tùy thuộc vào kích thước của dữ liệu và giới hạn của WebSocket)
+ chunk_size = 12288 # Kích thước mỗi phần dữ liệu
+ for i in range(0, len(frame_data), chunk_size):
+ chunk = frame_data[i:i + chunk_size]
+ await websocket.send(chunk)
+ await asyncio.sleep(1/10) # Giữ tốc độ gửi dữ liệu
+
+def main():
+ app.run(debug=True)
+if __name__ == "__main__":
+ main()
+
+```
+
diff --git a/img/connector.jpg b/img/connector.jpg
new file mode 100644
index 0000000..880d134
Binary files /dev/null and b/img/connector.jpg differ