Smart Home
Entertainment System
Media Center
Transform your Raspberry Pi Pico into a complete media center controller — browse movies, TV shows, music and photos with an OLED menu, IR buttons, RGB ambient lighting and real-time status displays. Simulate everything in Wokwi first!
▶ Open Free SimulationKey Features
This project turns a Raspberry Pi Pico into a fully functional media center controller with hierarchical menus, ambient lighting that changes per content type, playback simulation, and a 4-state machine managing the entire UI flow.
Multi-Media Library
Movies, TV Shows, Music & Photos with genre subcategories.
Button Navigation
UP, DOWN, SELECT, BACK, PLAY buttons simulating IR remote.
OLED Menu System
Scrollable 128×64 interface with header, selection highlight & footer.
Volume Control
Rotary encoder with real-time volume bar on display.
Playback Controls
Play, Pause, Stop with animated progress bar and timer.
RGB Ambient Light
PWM-controlled mood lighting — unique colour per media type.
Buzzer Feedback
Single, double & select beep patterns for every action.
Splash Screen
Boot animation with rainbow RGB sweep on startup.
Components Required
Wokwi Simulation: The diagram.json uses a Pi Pico, SSD1306 OLED, RGB LED, 3 resistors, 5 push buttons and a buzzer — everything available free in Wokwi without any physical hardware.
Circuit Connections
| Component | Pin / Signal | Pico GPIO | Notes |
|---|---|---|---|
| OLED SDA | I2C Data | GP0 | I2C0 |
| OLED SCL | I2C Clock | GP1 | I2C0 @ 400 kHz |
| OLED VCC | Power | 3.3V | |
| OLED GND | Ground | GND.8 | |
| RGB Red | PWM | GP6 → 220Ω | |
| RGB Green | PWM | GP7 → 220Ω | |
| RGB Blue | PWM | GP8 → 220Ω | |
| RGB COM | Common Cathode | GND.1 | |
| BTN UP | PULL_UP input | GP15 | → GND.2 |
| BTN DOWN | PULL_UP input | GP16 | → GND.3 |
| BTN SELECT | PULL_UP input | GP17 | → GND.4 |
| BTN BACK | PULL_UP input | GP18 | → GND.5 |
| BTN PLAY | PULL_UP input | GP19 | → GND.6 |
| Buzzer + | Digital OUT | GP13 | → GND.7 |
| Encoder CLK | PULL_UP input | GP10 | |
| Encoder DT | PULL_UP input | GP11 | |
| Encoder SW | PULL_UP input | GP12 | |
| IR Receiver OUT | Signal | GP15 | (physical build) |
| DHT22 Data | Signal | GP14 | (physical build) |
┌──────────────────────────────────┐ ┌────────────────┐ │ Raspberry Pi Pico │ │ SSD1306 OLED │ │ GP0 (SDA) ────────────────────────│ SDA │ │ GP1 (SCL) ────────────────────────│ SCL │ │ 3.3V ────────────────────────│ VCC │ │ GND ────────────────────────│ GND │ └──────────────────────────────────┘ └────────────────┘ ┌──────────────────────────────────┐ ┌────────────────┐ │ GP6 ──────────── 220Ω ──────────│ RGB Red │ │ GP7 ──────────── 220Ω ──────────│ RGB Green │ │ GP8 ──────────── 220Ω ──────────│ RGB Blue │ │ GND ────────────────────────────│ COM (Cathode) │ └──────────────────────────────────┘ └────────────────┘ Buttons (all: pin → GND, PULL_UP enabled) GP15──────[UP] GP16──────[DOWN] GP17──────[SELECT] GP18──────[BACK] GP19──────[PLAY] GP13──────[BUZZER]
diagram.json
Paste the block below into the diagram.json tab in Wokwi to instantly load the complete circuit — Pico, OLED, RGB LED, 3 resistors, 5 buttons and a buzzer, fully wired.
{
"version": 1,
"author": "Media Entertainment System",
"editor": "wokwi",
"parts": [
{ "type": "wokwi-pi-pico", "id": "pico", "top": 0, "left": 0, "attrs": {} },
{
"type": "wokwi-ssd1306", "id": "oled1",
"top": -137.33, "left": 150,
"attrs": { "i2cAddress": "0x3C" }
},
{
"type": "wokwi-rgb-led", "id": "rgb1",
"top": -80, "left": -180,
"attrs": { "common": "cathode" }
},
{ "type": "wokwi-resistor", "id": "r1", "top": -100, "left": -220, "rotate": 90, "attrs": { "value": "220" } },
{ "type": "wokwi-resistor", "id": "r2", "top": -100, "left": -190, "rotate": 90, "attrs": { "value": "220" } },
{ "type": "wokwi-resistor", "id": "r3", "top": -100, "left": -160, "rotate": 90, "attrs": { "value": "220" } },
{ "type": "wokwi-pushbutton", "id": "btn_up", "top": 120, "left": -220, "attrs": { "color": "blue", "label": "UP" } },
{ "type": "wokwi-pushbutton", "id": "btn_down", "top": 160, "left": -220, "attrs": { "color": "blue", "label": "DOWN" } },
{ "type": "wokwi-pushbutton", "id": "btn_select", "top": 120, "left": -140, "attrs": { "color": "green", "label": "SELECT" } },
{ "type": "wokwi-pushbutton", "id": "btn_back", "top": 160, "left": -140, "attrs": { "color": "red", "label": "BACK" } },
{ "type": "wokwi-pushbutton", "id": "btn_play", "top": 140, "left": -60, "attrs": { "color": "yellow", "label": "PLAY" } },
{
"type": "wokwi-buzzer", "id": "buzzer1",
"top": 90, "left": 120,
"attrs": { "volume": "0.5" }
}
],
"connections": [
["pico:GP0", "oled1:SDA", "green", ["v0"]],
["pico:GP1", "oled1:SCL", "blue", ["v0"]],
["pico:3V3(OUT)", "oled1:VCC", "red", ["v0"]],
["pico:GND.8", "oled1:GND", "black", ["v0"]],
["pico:GP6", "r1:1", "red", ["v0"]],
["r1:2", "rgb1:R", "red", ["v0"]],
["pico:GP7", "r2:1", "green", ["v0"]],
["r2:2", "rgb1:G", "green", ["v0"]],
["pico:GP8", "r3:1", "blue", ["v0"]],
["r3:2", "rgb1:B", "blue", ["v0"]],
["rgb1:COM", "pico:GND.1", "black", ["v0"]],
["pico:GP15", "btn_up:1.l", "blue", ["v0"]],
["btn_up:2.l", "pico:GND.2", "black", ["v0"]],
["pico:GP16", "btn_down:1.l", "blue", ["v0"]],
["btn_down:2.l", "pico:GND.3", "black", ["v0"]],
["pico:GP17", "btn_select:1.l","green", ["v0"]],
["btn_select:2.l","pico:GND.4", "black", ["v0"]],
["pico:GP18", "btn_back:1.l", "red", ["v0"]],
["btn_back:2.l", "pico:GND.5", "black", ["v0"]],
["pico:GP19", "btn_play:1.l", "yellow", ["v0"]],
["btn_play:2.l", "pico:GND.6", "black", ["v0"]],
["pico:GP13", "buzzer1:1", "magenta", ["v0"]],
["buzzer1:2", "pico:GND.7", "black", ["v0"]]
],
"dependencies": {}
}
Full MicroPython Code
Paste the following into main.py (or sketch.py in Wokwi). The custom SSD1306 driver is embedded — no external library required.
Wokwi setup: Create a new Raspberry Pi Pico project, paste the diagram.json above, then paste this code into main.py and click ▶ Run.
from machine import Pin, I2C, PWM import time import framebuf import random # ── SSD1306 OLED Driver (128×64, embedded) ────────────────── class SSD1306_I2C: def __init__(self, width, height, i2c, addr=0x3C): self.i2c = i2c; self.addr = addr self.width = width; self.height = height self.pages = height // 8 self.buffer = bytearray(self.pages * width) self.framebuf = framebuf.FrameBuffer(self.buffer, width, height, framebuf.MONO_VLSB) self.init_display() def init_display(self): for cmd in ( 0xAE,0xD5,0x80,0xA8,0x3F,0xD3,0x00,0x40,0x8D,0x14, 0x20,0x00,0xA1,0xC8,0xDA,0x12,0x81,0xCF,0xD9,0xF1, 0xDB,0x40,0xA4,0xA6,0xAF ): self.write_cmd(cmd) self.fill(0); self.show() def write_cmd(self, cmd): self.i2c.writeto(self.addr, bytearray([0x00, cmd])) def write_data(self, buf): self.i2c.writeto(self.addr, b'\x40' + buf) def show(self): for p in range(self.pages): self.write_cmd(0xB0+p); self.write_cmd(0x00); self.write_cmd(0x10) self.write_data(self.buffer[p*self.width:(p+1)*self.width]) def fill(self, c): self.framebuf.fill(c) def text(self, s, x, y, c=1): self.framebuf.text(s, x, y, c) def rect(self, x, y, w, h, c): self.framebuf.rect(x, y, w, h, c) def fill_rect(self, x, y, w, h, c): self.framebuf.fill_rect(x, y, w, h, c) def line(self, x1, y1, x2, y2, c): self.framebuf.line(x1, y1, x2, y2, c) # ── Hardware Initialisation ────────────────────────────────── i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000) oled = SSD1306_I2C(128, 64, i2c) led_r = PWM(Pin(6)); led_r.freq(1000) led_g = PWM(Pin(7)); led_g.freq(1000) led_b = PWM(Pin(8)); led_b.freq(1000) encoder_clk = Pin(10, Pin.IN, Pin.PULL_UP) encoder_dt = Pin(11, Pin.IN, Pin.PULL_UP) encoder_sw = Pin(12, Pin.IN, Pin.PULL_UP) btn_up = Pin(15, Pin.IN, Pin.PULL_UP) btn_down = Pin(16, Pin.IN, Pin.PULL_UP) btn_select = Pin(17, Pin.IN, Pin.PULL_UP) btn_back = Pin(18, Pin.IN, Pin.PULL_UP) btn_play = Pin(19, Pin.IN, Pin.PULL_UP) buzzer = Pin(13, Pin.OUT) # ── System State ──────────────────────────────────────────── volume = 50 last_enc_clk = 1 debounce = {"up":0,"down":0,"select":0,"back":0,"play":0} DEBOUNCE_MS = 200 current_state = "main_menu" current_category = None current_subcat = None current_item = None playback_status = "stopped" playback_prog = 0 selected_idx = 0 # ── Media Library ──────────────────────────────────────────── MEDIA = { "Movies": { "Action": ["The Matrix","Die Hard","Mad Max Fury Road","John Wick"], "Comedy": ["Superbad","The Hangover","Anchorman","Step Brothers"], "Drama": ["Shawshank Redemption","Forrest Gump","The Godfather"], "Sci-Fi": ["Inception","Interstellar","Blade Runner 2049"] }, "TV Shows": { "Sitcoms": ["Friends S01E01","The Office S01E01","Parks&Rec S01E01"], "Drama": ["Breaking Bad S01E01","Game of Thrones S01E01"], "Sci-Fi": ["Stranger Things S01E01","Black Mirror S01E01"] }, "Music": { "Rock": ["Queen - Bohemian Rhapsody","AC/DC - Thunderstruck"], "Pop": ["Michael Jackson - Thriller","Madonna - Like a Prayer"], "Classical": ["Beethoven - Symphony No.9","Mozart - Requiem"], "Jazz": ["Miles Davis - So What","John Coltrane - Giant Steps"] }, "Photos": { "Albums": ["Vacation 2024","Family Events","Nature Photography","Cityscapes"] } } LIGHTING = { "Movies": (255,0,100), "TV Shows": (0,150,255), "Music": (255,100,0), "Photos": (0,255,100), "Playing": (100,0,255), } # ── Helpers ────────────────────────────────────────────────── def set_rgb(r, g, b): led_r.duty_u16(int((255-r)/255*65535)) led_g.duty_u16(int((255-g)/255*65535)) led_b.duty_u16(int((255-b)/255*65535)) def beep(pat="single"): if pat=="single": buzzer.on(); time.sleep(0.05); buzzer.off() elif pat=="double": for _ in range(2): buzzer.on(); time.sleep(0.04); buzzer.off(); time.sleep(0.04) elif pat=="select": buzzer.on(); time.sleep(0.08); buzzer.off() def btn_pressed(name, pin): now = time.ticks_ms() if pin.value()==0 and time.ticks_diff(now, debounce[name])>DEBOUNCE_MS: debounce[name]=now; return True return False def read_encoder(): global volume, last_enc_clk clk = encoder_clk.value() if clk != last_enc_clk: volume = min(100, volume+2) if encoder_dt.value()!=clk else max(0, volume-2) last_enc_clk = clk; beep("single") # ── Display Functions ──────────────────────────────────────── def draw_header(title): oled.fill_rect(0,0,128,12,1) oled.text(title[:16],2,2,0) oled.line(0,12,128,12,1) def draw_menu(items, sel, sy=16): vis=4; off=max(0,sel-vis+1) for i in range(vis): idx=off+i if idx>=len(items): break y=sy+i*11; txt=items[idx][:15] if idx==sel: oled.fill_rect(0,y,128,10,1) oled.text(f">{txt}",4,y+1,0) else: oled.text(f" {txt}",4,y+1,1) def show_main_menu(): oled.fill(0); draw_header("MEDIA CENTER") draw_menu(list(MEDIA.keys()), selected_idx) oled.text("VOL:",2,54); oled.text(f"{volume}%",30,54) oled.text("12:45",92,54); oled.show() def show_category_menu(): oled.fill(0); draw_header(current_category[:14]) draw_menu(list(MEDIA[current_category].keys()), selected_idx) oled.text("B:Back",2,54); oled.show() def show_item_list(): oled.fill(0); draw_header(current_subcat[:14]) draw_menu(MEDIA[current_category][current_subcat], selected_idx) oled.text("B:Back SEL:Play",2,54); oled.show() def show_now_playing(): global playback_prog oled.fill(0); draw_header("NOW PLAYING") if current_item: oled.text(current_item[:21],4,18) oled.text(current_subcat[:16],4,28) icon = ">" if playback_status=="playing" else "||" if playback_status=="paused" else "[]" oled.text(icon,4,38) # Progress bar oled.rect(2,45,124,6,1) oled.fill_rect(3,46,int(playback_prog*1.2),4,1) ct=int(playback_prog/100*180) oled.text(f"{ct//60}:{ct%60:02d}/3:00",30,38) oled.text("PLAY BACK STOP",2,55); oled.show() def show_splash(): oled.fill(0); oled.rect(10,10,108,44,1) oled.text("MEDIA CENTER",22,20) oled.text("===========",22,30) oled.text("Loading...",30,40); oled.show() for i in range(0,255,15): set_rgb(i,255-i,128); time.sleep(0.05) set_rgb(0,0,0) # ── State Handlers ─────────────────────────────────────────── def handle_main(): global current_state, current_category, selected_idx cats = list(MEDIA.keys()) if btn_pressed("up",btn_up): selected_idx=(selected_idx-1)%len(cats); beep() if btn_pressed("down",btn_down): selected_idx=(selected_idx+1)%len(cats); beep() if btn_pressed("select",btn_select): current_category=cats[selected_idx]; current_state="category_menu" selected_idx=0; beep("select") r,g,b=LIGHTING.get(current_category,(0,0,0)); set_rgb(r,g,b) show_main_menu() def handle_category(): global current_state, current_subcat, selected_idx subs = list(MEDIA[current_category].keys()) if btn_pressed("up",btn_up): selected_idx=(selected_idx-1)%len(subs); beep() if btn_pressed("down",btn_down): selected_idx=(selected_idx+1)%len(subs); beep() if btn_pressed("select",btn_select): current_subcat=subs[selected_idx]; current_state="item_list"; selected_idx=0; beep("select") if btn_pressed("back",btn_back): current_state="main_menu"; selected_idx=0; beep(); set_rgb(0,0,0) show_category_menu() def handle_items(): global current_state, current_item, selected_idx, playback_status, playback_prog items = MEDIA[current_category][current_subcat] if btn_pressed("up",btn_up): selected_idx=(selected_idx-1)%len(items); beep() if btn_pressed("down",btn_down): selected_idx=(selected_idx+1)%len(items); beep() if btn_pressed("select",btn_select): current_item=items[selected_idx]; current_state="playing" playback_status="playing"; playback_prog=0; beep("double") r,g,b=LIGHTING["Playing"]; set_rgb(r,g,b) if btn_pressed("back",btn_back): current_state="category_menu"; selected_idx=0; beep() show_item_list() def handle_playing(): global current_state, playback_status, playback_prog if btn_pressed("play",btn_play): playback_status = "paused" if playback_status=="playing" else "playing"; beep() if btn_pressed("back",btn_back): playback_status="stopped"; current_state="item_list"; playback_prog=0; beep() r,g,b=LIGHTING.get(current_category,(0,0,0)); set_rgb(r,g,b) if playback_status=="playing": playback_prog = min(100, playback_prog+0.5) if playback_prog>=100: playback_status="stopped"; current_state="item_list" playback_prog=0; beep("double") show_now_playing() # ── Main Loop ──────────────────────────────────────────────── print("📺 SMART HOME ENTERTAINMENT SYSTEM") show_splash(); time.sleep(2) try: while True: read_encoder() if current_state=="main_menu": handle_main() elif current_state=="category_menu": handle_category() elif current_state=="item_list": handle_items() elif current_state=="playing": handle_playing() time.sleep(0.05) except KeyboardInterrupt: set_rgb(0,0,0); buzzer.off() oled.fill(0); oled.text("GOODBYE!",40,28); oled.show() time.sleep(1); oled.fill(0); oled.show()
4-State Machine
The entire UI is controlled by a clean state machine. Each state handles its own button inputs, display rendering and RGB lighting, then transitions based on user actions.
🏠 main_menu
Top-level category list. UP/DOWN scrolls, SELECT enters, volume shown at bottom.
📂 category_menu
Genre sub-list for chosen category. BACK returns to main. RGB lighting activates.
📋 item_list
Individual media titles. SELECT begins playback with double-beep + purple LED.
▶ playing
Progress bar, time counter, status icon. PLAY toggles pause. BACK stops and returns.
State transitions: main_menu → category_menu → item_list → playing → item_list. Each BACK button step moves up one level. The encoder adjusts volume in any state.
Media Library
Movies
- Action: The Matrix, Die Hard, Mad Max, John Wick
- Comedy: Superbad, The Hangover, Anchorman
- Drama: Shawshank, Forrest Gump, The Godfather
- Sci-Fi: Inception, Interstellar, Blade Runner
TV Shows
- Sitcoms: Friends, The Office, Parks & Rec
- Drama: Breaking Bad, Game of Thrones, The Crown
- Sci-Fi: Stranger Things, Black Mirror, Westworld
Music
- Rock: Queen, AC/DC, Led Zeppelin
- Pop: Michael Jackson, Madonna, Prince
- Classical: Beethoven, Mozart, Bach
- Jazz: Miles Davis, Coltrane, Ella Fitzgerald
Photos
- Albums: Vacation 2024
- Family Events
- Nature Photography
- Cityscapes
RGB Ambient Lighting
The RGB LED changes colour automatically based on the current media category, using PWM for smooth, precise colour mixing at 1 kHz frequency.
| Mode | Colour | R / G / B | Trigger |
|---|---|---|---|
| Movies | Purple/Pink | 255 / 0 / 100 | Enter Movies category |
| TV Shows | Blue | 0 / 150 / 255 | Enter TV Shows |
| Music | Orange | 255 / 100 / 0 | Enter Music |
| Photos | Green | 0 / 255 / 100 | Enter Photos |
| Playing | Deep Purple | 100 / 0 / 255 | Playback starts |
| Idle | Off | 0 / 0 / 0 | Back to main menu |
IR Remote Button Mapping
In the Wokwi simulation, push buttons replace IR remote signals. The physical build can add a VS1838B IR receiver on GP15 with NEC protocol decoding.
| Button / Key | Simulation Pin | Action |
|---|---|---|
| UP ▲ | GP15 (Blue btn) | Navigate menu up |
| DOWN ▼ | GP16 (Blue btn) | Navigate menu down |
| SELECT / OK | GP17 (Green btn) | Choose item / start playback |
| BACK | GP18 (Red btn) | Previous menu / stop playback |
| PLAY / PAUSE | GP19 (Yellow btn) | Toggle play/pause during playback |
| Volume encoder | GP10/11 | Rotate to adjust volume 0–100% |
| Encoder button | GP12 | Mute / confirm |
Comments
Post a Comment