Kod Arduino:
#include <Wire.h>
#include <U8g2lib.h>
// OLED 128x64, rotated (portrait mode - 64px wide, 128px tall)
// Full framebuffer mode for fast screen updates
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R3, U8X8_PIN_NONE);
// Button pin definitions
#define BTN_UP 7
#define BTN_DOWN 9
#define BTN_RIGHT 8
#define BTN_LEFT 10
#define BTN_A 5
#define BTN_B 4
// Timestamp of last received serial frame
// Used to detect loss of video signal
unsigned long lastSerialTime = 0;
const unsigned long serialTimeout = 1000; // 1 second timeout
// Previous button states (for edge detection)
int pU=0, pD=0, pL=0, pR=0, pA=0, pB=0;
// Fake system boot logs displayed during boot / no-signal state
const char* sys_logs[] = {
"MCU: 328P",
"I2C: 800K",
"SRL: 500K",
"BUF: 1024B",
"REELS..."
};
// Draw static system log lines (used in boot & idle screens)
void drawStaticSystemLogs(int limit) {
u8g2.setFont(u8g2_font_4x6_tf);
for(int i = 0; i < limit; i++) {
// Logs start lower to leave room for header
u8g2.setCursor(2, 40 + (i * 9));
u8g2.print("[OK] ");
u8g2.print(sys_logs[i]);
}
}
// Boot animation displayed on power-up
void playBootAnimation() {
// --- SPLASH SCREEN ---
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x12_tf);
u8g2.drawStr(5, 20, "REELS OS");
u8g2.drawHLine(2, 25, 60);
u8g2.sendBuffer();
delay(400);
// --- FAKE SYSTEM LOGS ---
for(int i = 1; i <= 5; i++) {
u8g2.clearBuffer();
// Persistent header
u8g2.setFont(u8g2_font_6x12_tf);
u8g2.drawStr(5, 20, "REELS OS");
u8g2.drawHLine(2, 25, 60);
drawStaticSystemLogs(i);
u8g2.sendBuffer();
delay(30);
}
}
void setup() {
// High-speed serial for video data
Serial.begin(500000);
// Increase I2C clock for fast OLED refresh
Wire.setClock(800000);
// Initialize OLED display
u8g2.begin();
u8g2.setFlipMode(1);
// Show boot animation at startup
playBootAnimation();
lastSerialTime = millis();
// Configure buttons with internal pullups
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
pinMode(BTN_LEFT, INPUT_PULLUP);
pinMode(BTN_RIGHT, INPUT_PULLUP);
pinMode(BTN_A, INPUT_PULLUP);
pinMode(BTN_B, INPUT_PULLUP);
}
// Send button state only when it changes (reduces serial traffic)
void sendBtn(const char *name, int state, int &prev) {
if (state != prev) {
Serial.print(name);
Serial.print(" ");
Serial.println(state);
prev = state;
}
}
// --- HANDLE BUTTON INPUTS ---
sendBtn("UP", !digitalRead(BTN_UP), pU);
sendBtn("DOWN", !digitalRead(BTN_DOWN), pD);
sendBtn("LEFT", !digitalRead(BTN_LEFT), pL);
sendBtn("RIGHT", !digitalRead(BTN_RIGHT), pR);
sendBtn("A", !digitalRead(BTN_A), pA);
sendBtn("B", !digitalRead(BTN_B), pB);
// State machine for framed serial protocol
static bool receiving = false;
static bool escaping = false;
static int idx = 0;
// Direct pointer to OLED framebuffer (1024 bytes)
uint8_t* screenBuffer = u8g2.getBufferPtr();
// --- SERIAL VIDEO RECEPTION ---
if (Serial.available()) {
lastSerialTime = millis();
while (Serial.available()) {
uint8_t c = Serial.read();
// Start frame marker
if (!receiving) {
if (c == 0xFF) {
receiving = true;
escaping = false;
idx = 0;
}
}
// End frame marker
else if (c == 0xFE) {
// Only draw when full frame received
if (idx == 1024) {
u8g2.sendBuffer();
}
receiving = false;
}
// Restart frame if unexpected start marker
else if (c == 0xFF) {
escaping = false;
idx = 0;
}
// Handle escaped bytes
else if (escaping) {
escaping = false;
uint8_t decoded =
(c == 0x00) ? 0xFD :
(c == 0x01) ? 0xFF :
(c == 0x02) ? 0xFE : c;
if (idx < 1024) screenBuffer[idx++] = decoded;
}
// Escape byte detected
else if (c == 0xFD) {
escaping = true;
}
// Normal framebuffer data
else {
if (idx < 1024) screenBuffer[idx++] = c;
}
}
}
// --- NO SIGNAL / IDLE MODE ---
else if (millis() - lastSerialTime > serialTimeout) {
// Header
u8g2.setFont(u8g2_font_6x12_tf);
u8g2.drawStr(5, 20, "REELS OS");
u8g2.drawHLine(2, 25, 60);
// Static logs
drawStaticSystemLogs(5);
u8g2.drawHLine(2, 90, 60);
// Status text + spinner animation
u8g2.setFont(u8g2_font_4x6_tf);
u8g2.drawStr(2, 102, "LINK: OFFLINE");
const char* spinner = "|/-\\";
u8g2.setCursor(2, 115);
u8g2.print("NO SIGNAL ");
u8g2.print(spinner[(millis() / 150) % 4]);
Kod na PC (Windows) - Wariant rolki
import serial
import time
import threading
import keyboard
import numpy as np
from PIL import Image, ImageEnhance, ImageOps
import mss
import webbrowser
# -------------------------------------------------
# CONFIG
# -------------------------------------------------
COM_PORT = "COM6"
BAUD = 500000
WIDTH = 128 # Physical OLED Width
HEIGHT = 64 # Physical OLED Height
# Global serial object
ser = None
def get_serial_connection():
"""Attempts to connect to the Arduino. Loops until successful."""
global ser
while True:
try:
print(f"🔍 Searching for USB on {COM_PORT}...")
ser = serial.Serial(COM_PORT, BAUD, timeout=0.05)
print(f"✅ Connected to {COM_PORT}")
return # Exit loop once connected
except (serial.SerialException, FileNotFoundError):
print(f"❌ USB not found. Retrying in 2 seconds...")
time.sleep(2)
# Initial connection attempt
get_serial_connection()
# -------------------------------------------------
# CAPTURE & PROCESS
# -------------------------------------------------
def auto_contrast(img):
"""
Optimizes the image for a 1-bit OLED.
Uses a center-mask to ignore dark/bright UI edges when calculating contrast.
"""
gray = img.convert("L") # Convert to Grayscale
w, h = gray.size
# Define the 'Interest Zone' (Center 50% of the screen)
left, top = w // 4, h // 4
right, bottom = int(w * 0.75), int(h * 0.75)
# Calculate average brightness of the center crop
center_crop = gray.crop((left, top, right, bottom))
mean = np.array(center_crop).mean()
# If the center is dark, boost contrast and brightness more aggressively
contrast_factor = 1.2 if mean > 120 else 1.6
brightness_factor = 1.0 if mean > 120 else 1.2
# Mask for Autocontrast logic
mask = Image.new("L", (w, h), 0)
white_box = Image.new("L", (right - left, bottom - top), 255)
mask.paste(white_box, (left, top))
# Apply processing chain
gray = ImageOps.autocontrast(gray, cutoff=2, mask=mask)
gray = ImageEnhance.Contrast(gray).enhance(contrast_factor)
gray = ImageEnhance.Brightness(gray).enhance(brightness_factor)
gray = ImageEnhance.Sharpness(gray).enhance(1.3)
def capture_and_dither(sct, capture_region):
"""
Captures screen, resizes to vertical dimensions, dithers to 1-bit,
and packs bits into a format the SH1106 driver understands.
"""
img = sct.grab(capture_region)
pil = Image.frombytes("RGB", img.size, img.rgb)
# Resize to 64x128 (matches the logical R1 orientation of the OLED)
pil = pil.resize((HEIGHT, WIDTH), Image.BILINEAR)
pil = auto_contrast(pil)
# Convert to 1-bit black and white using Floyd-Steinberg dithering
pil = pil.convert("1", dither=Image.FLOYDSTEINBERG)
# Rotate 90deg to match physical orientation
pil = pil.rotate(90, expand=True)
# Reorganize the numpy array to match the OLED's page-based memory layout
arr = np.array(pil).reshape((8, 8, 128))[:, ::-1, :]
return np.packbits(arr, axis=1).tobytes()
# -------------------------------------------------
# SERIAL PROTOCOL
# -------------------------------------------------
START = bytes([0xFF]) # Start of frame marker
END = bytes([0xFE]) # End of frame marker
def escape_frame(data: bytes) -> bytes:
"""
Prevents data bytes from being confused with START/END markers.
If 0xFF appears in the image, it's swapped for an escape sequence.
"""
return data.replace(b'\xfd', b'\xfd\x00') \
.replace(b'\xff', b'\xfd\x01') \
.replace(b'\xfe', b'\xfd\x02')
def send_frame(frame: bytes):
"""Sends the escaped byte frame over serial."""
global ser
try:
if ser and ser.is_open:
ser.write(START + escape_frame(frame) + END)
except (serial.SerialException, AttributeError):
# If writing fails, the USB was likely pulled
ser = None
# -------------------------------------------------
# THREADS
# -------------------------------------------------
def stream_thread():
"""Captures screen and sends to Arduino as fast as possible."""
global ser
with mss.mss() as sct:
monitor = sct.monitors[1]
# Calculate a 1:2 aspect ratio box centered on the screen
capture_height = monitor["height"]
capture_width = capture_height // 2
left_offset = (monitor["width"] - capture_width) // 2
capture_region = {
"top": monitor["top"],
"left": monitor["left"] + left_offset,
"width": capture_width,
"height": capture_height
}
while True:
if ser:
frame = capture_and_dither(sct, capture_region)
send_frame(frame)
time.sleep(0.001) # High-speed refresh
else:
# If serial is lost, wait and let the main loop reconnect
time.sleep(1)
def handle_input(line):
"""Maps Arduino serial messages to PC keyboard actions."""
try:
btn, val = line.split()
val = int(val)
except:
return
if btn == "A":
if val == 1:
print("🚀 Reels launched")
webbrowser.open("https://www.instagram.com/reels/")
elif btn == "B":
if val == 1:
print("🔄 Refreshing")
keyboard.send("f5")
else:
# Physical to Logical mapping for 90deg rotation
mapping = {
"UP": "right",
"RIGHT": "down",
"DOWN": "left",
"LEFT": "up"
}
if btn in mapping:
key = mapping[btn]
keyboard.press(key) if val else keyboard.release(key)
def input_thread():
"""Listens for button presses coming FROM the Arduino."""
global ser
while True:
if ser:
try:
line = ser.readline().decode(errors="ignore").strip()
if line:
handle_input(line)
except:
ser = None # Trigger reconnection
else:
time.sleep(1)
# -------------------------------------------------
# MAIN RUNNER
# -------------------------------------------------
print("✅ REELS OS HOST STARTED")
# Start threads as Daemons so they close when the script exits
threading.Thread(target=stream_thread, daemon=True).start()
threading.Thread(target=input_thread, daemon=True).start()
try:
while True:
# If the serial object becomes None, try to reconnect
if ser is None:
get_serial_connection()
time.sleep(1)
except KeyboardInterrupt:
print("👋 Shutting down...")
Kod na PC (Windows) - Wariant Fullscreen
import serial
import time
import threading
import keyboard
import numpy as np
from PIL import Image, ImageEnhance, ImageOps
import mss
# -------------------------------------------------
# CONFIG
# -------------------------------------------------
COM_PORT = "COM6"
BAUD = 500000
WIDTH = 128
HEIGHT = 64
ser = serial.Serial(COM_PORT, BAUD, timeout=0.01)
# -------------------------------------------------
# CAPTURE & PROCESS
# -------------------------------------------------
def auto_contrast(img):
gray = img.convert("L")
w, h = gray.size
# Calculate coordinates for the center 50% of the image
left = w // 4
top = h // 4
right = int(w * 0.75)
bottom = int(h * 0.75)
# 1. Get the mean brightness of ONLY the center crop
center_crop = gray.crop((left, top, right, bottom))
mean = np.array(center_crop).mean()
# Dynamic tuning based on the center brightness
contrast_factor = 1.2 if mean > 120 else 1.6
brightness_factor = 1.0 if mean > 120 else 1.2
# 2. Create a mask so autocontrast only looks at the center
mask = Image.new("L", (w, h), 0)
white_box = Image.new("L", (right - left, bottom - top), 255)
mask.paste(white_box, (left, top))
# Apply autocontrast. It calculates the stretch using ONLY the white masked
# area (the center), but applies the visual change to the ENTIRE image!
gray = ImageOps.autocontrast(gray, cutoff=2, mask=mask)
gray = ImageEnhance.Contrast(gray).enhance(contrast_factor)
gray = ImageEnhance.Brightness(gray).enhance(brightness_factor)
gray = ImageEnhance.Sharpness(gray).enhance(1.3)
def capture_and_dither(sct, capture_region):
img = sct.grab(capture_region)
pil = Image.frombytes("RGB", img.size, img.rgb)
# Resize to standard horizontal OLED dimensions (128x64)
pil = pil.resize((WIDTH, HEIGHT), Image.BILINEAR)
pil = auto_contrast(pil)
pil = pil.convert("1", dither=Image.FLOYDSTEINBERG)
# Numpy bit-rotation to match OLED hardware memory structure
arr = np.array(pil).reshape((8, 8, 128))[:, ::-1, :]
return np.packbits(arr, axis=1).tobytes()
# -------------------------------------------------
# SEND FRAME
# -------------------------------------------------
START = bytes([0xFF])
END = bytes([0xFE])
def escape_frame(data: bytes) -> bytes:
return data.replace(b'\xfd', b'\xfd\x00') \
.replace(b'\xff', b'\xfd\x01') \
.replace(b'\xfe', b'\xfd\x02')
def send_frame(frame: bytes):
ser.write(START + escape_frame(frame) + END)
# -------------------------------------------------
# STREAMING THREAD
# -------------------------------------------------
def stream_thread():
with mss.mss() as sct:
# Get dimensions of the ENTIRE primary monitor
monitor = sct.monitors[1]
print(f"📷 Capturing Full Screen. Resolution: {monitor['width']}x{monitor['height']}")
while True:
# Pass the full monitor directly to the capture function
frame = capture_and_dither(sct, monitor)
send_frame(frame)
time.sleep(0.005) # Tiny delay to keep the Serial connection stable
# -------------------------------------------------
# BUTTON HANDLING
# -------------------------------------------------
def handle_input(line):
try:
btn, val = line.split()
val = int(val)
except:
return
# Normal horizontal mappings
mapping = {
"UP": "up", "DOWN": "down",
"LEFT": "left", "RIGHT": "right",
"A": "z", "B": "x",
}
if btn in mapping:
key = mapping[btn]
keyboard.press(key) if val else keyboard.release(key)
def input_thread():
while True:
try:
line = ser.readline().decode(errors="ignore").strip()
if line:
handle_input(line)
except:
pass
# -------------------------------------------------
# START
# -------------------------------------------------
print("✅ Streaming Full Screen - Horizontal (Max FPS Numpy Mode)")
threading.Thread(target=stream_thread, daemon=True).start()
threading.Thread(target=input_thread, daemon=True).start()
while True:
time.sleep(0.1)