Smart Home Entertainment System - Media Center using Raspberry pi pico

Smart Home Entertainment System – Media Center with Raspberry Pi Pico | MakeMindz
📺 Advanced Pico Project — #18

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!

🐍 MicroPython 🖥️ SSD1306 OLED 🌈 RGB Ambient LED 🔔 Buzzer Feedback 🧪 Wokwi Simulator
▶ Open Free Simulation
01 — Overview

Key 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.


02 — Hardware

Components Required

Raspberry Pi Pico — RP2040 microcontroller (simulated in Wokwi)
SSD1306 OLED 128×64 — I2C display (0.96" or 1.3")
RGB LED — Common cathode, ambient lighting
Active Buzzer — Notification sounds
Push Buttons × 5 — UP, DOWN, SELECT, BACK, PLAY
Rotary Encoder — Volume / navigation control
IR Receiver (VS1838B) — Remote control input
DHT22 Sensor — System temperature monitoring
220Ω Resistors × 3 — RGB LED current limiting
Breadboard & Jumper Wires — Connections
MicroPython Firmware — Installed on Pico
💡

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.


03 — Wiring

Circuit Connections

ComponentPin / SignalPico GPIONotes
OLED SDAI2C DataGP0I2C0
OLED SCLI2C ClockGP1I2C0 @ 400 kHz
OLED VCCPower3.3V
OLED GNDGroundGND.8
RGB RedPWMGP6 → 220Ω
RGB GreenPWMGP7 → 220Ω
RGB BluePWMGP8 → 220Ω
RGB COMCommon CathodeGND.1
BTN UPPULL_UP inputGP15→ GND.2
BTN DOWNPULL_UP inputGP16→ GND.3
BTN SELECTPULL_UP inputGP17→ GND.4
BTN BACKPULL_UP inputGP18→ GND.5
BTN PLAYPULL_UP inputGP19→ GND.6
Buzzer +Digital OUTGP13→ GND.7
Encoder CLKPULL_UP inputGP10
Encoder DTPULL_UP inputGP11
Encoder SWPULL_UP inputGP12
IR Receiver OUTSignalGP15(physical build)
DHT22 DataSignalGP14(physical build)

04 — Wokwi

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.

diagram.json
{
  "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": {}
}

05 — MicroPython

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.

main.py — Smart Home Media Center
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()

06 — Architecture

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.


07 — Content

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

08 — Ambience

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.

ModeColourR / G / BTrigger
MoviesPurple/Pink255 / 0 / 100Enter Movies category
TV ShowsBlue0 / 150 / 255Enter TV Shows
MusicOrange255 / 100 / 0Enter Music
PhotosGreen0 / 255 / 100Enter Photos
PlayingDeep Purple100 / 0 / 255Playback starts
IdleOff0 / 0 / 0Back to main menu

09 — Controls

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 / KeySimulation PinAction
UP ▲GP15 (Blue btn)Navigate menu up
DOWN ▼GP16 (Blue btn)Navigate menu down
SELECT / OKGP17 (Green btn)Choose item / start playback
BACKGP18 (Red btn)Previous menu / stop playback
PLAY / PAUSEGP19 (Yellow btn)Toggle play/pause during playback
Volume encoderGP10/11Rotate to adjust volume 0–100%
Encoder buttonGP12Mute / confirm

10 — Learning

What You'll Learn

Media player architecture & state machines
OLED hierarchical menu system design
Rotary encoder interrupt handling
RGB LED PWM colour mixing
IR remote protocol decoding (NEC, RC5)
Button debouncing in MicroPython
Progress bar & animation rendering
Playlist and queue management
Embedded custom OLED driver writing
Power management & sleep modes
Temperature-based fan control logic
User interface responsiveness optimisation

11 — Use Cases

Real-World Applications

Home Theater Control — Central media management system
Smart TV Interface — Custom replacement menu
Music Streaming Hub — Spotify / Apple Music controller
Photo Frame Display — Digital slideshow player
Party Entertainment — DJ controller with lighting effects
Hotel Room Systems — Guest entertainment interface
Restaurant / Bar — Background music and video control
Museum Exhibits — Interactive media stations
Waiting Rooms — Automated content player
Educational Displays — School presentation system

12 — More Projects

Raspberry Pi Pico Projects

© 2026 MakeMindz · Simulated with Wokwi

Raspberry Pi Pico · MicroPython · SSD1306 · RGB LED · Smart Home · Media Center

Comments

try for free