Back to Topics
Term 3 DI10-4, DI10-5

Simpson's House Project

Build a real smart home controlled from your iPad

Learning Objectives

  • Understand event-driven programming and the publish/subscribe (pub/sub) design pattern
  • Read and modify Python code that controls physical hardware via GPIO pins
  • Trace data flow across a full IoT system: iPad → WiFi → MQTT broker → Python → hardware
  • Apply functions, parameters, and modular design to real hardware control code
  • Customise a Swift iOS user interface to control physical devices
  • Test and debug a multi-component system using logs and GPIO test scripts

What Are You Building?

The Simpson's House project turns a model house into a real, working smart home. You'll use a Raspberry Pi (a small Linux computer) to control physical hardware — a light, a garage door servo, and a front door servo — all from a custom iPad app you can style and customise.

This is a real IoT (Internet of Things) system. The same architecture is used in commercial smart home products like Google Nest and Philips Hue. By the end you'll understand exactly how those systems work under the hood.

System Architecture

Every command you tap on the iPad travels through four distinct layers:

📱
iPad App
Swift Playgrounds
You design this part
MQTT over
WebSocket
🖥️
Raspberry Pi
Mosquitto broker
+ Python listener
GPIO
signals
🔌
Hardware
LED, 2× servo motors

Hardware Components

Each Pi kit contains:

Component Quantity Purpose GPIO Pin (BCM)
Raspberry Pi 4 1 Runs Linux, Python, MQTT broker
LED + 220Ω resistor 1 Living room light GPIO 17
SG90 Servo Motor 1 Garage door (0° closed, 90° open) GPIO 27
SG90 Servo Motor 1 Front door (0° closed, 90° open) GPIO 23

Key Concept: MQTT Pub/Sub

MQTT is a lightweight messaging protocol designed for IoT devices. It uses a publish/subscribe (pub/sub) pattern — the same idea as a YouTube channel:

  • Publisher — the iPad publishes a message to a topic (e.g. home/light)
  • Broker — Mosquitto (running on the Pi) receives and routes messages
  • Subscriber — the Python script subscribes to topics and reacts when messages arrive

The three topics used in this project:

MQTT Topic Commands Controls
home/light ON / OFF LED on GPIO 17
home/garage OPEN / CLOSE Garage servo on GPIO 27
home/door ON / OFF Front door servo on GPIO 23

Key Concept: Event-Driven Programming

The Python backend uses event-driven programming. Instead of constantly checking "has a message arrived?", the program registers callback functions that the MQTT library calls automatically when specific events happen.

# mqttlistener.py — callback registration
client = mqtt.Client()

# Register callbacks — these run automatically when events occur
client.on_connect    = on_connect    # ← called when broker accepts connection
client.on_disconnect = on_disconnect # ← called when connection drops
client.on_message    = on_message    # ← called EVERY time a message arrives

client.connect("localhost", 1883)
client.loop_forever()  # blocks here, processing events indefinitely

Key Concept: PWM and Servo Control

A servo motor doesn't understand "open" or "closed" — it understands angles (0–180°). We use PWM (Pulse Width Modulation) to communicate the angle. PWM rapidly switches a GPIO pin on and off at 50 Hz; the ratio of on-time to off-time encodes the target angle.

# mqttlistener.py — servo angle control
def set_servo_angle(pwm, angle: int, label: str = "Servo") -> bool:
    # Convert 0–180° angle into a 2–12% PWM duty cycle
    # 0°  → 2%  duty  (1ms pulse at 50Hz)
    # 90° → 7%  duty  (1.5ms pulse)
    # 180°→ 12% duty  (2ms pulse)
    duty = (angle / 180.0) * 10 + 2
    pwm.ChangeDutyCycle(duty)
    time.sleep(0.8)       # wait for servo to physically move
    pwm.ChangeDutyCycle(0) # stop pulses to prevent jitter

# Using it:
set_servo_angle(GARAGE_SERVO_PWM, 90, "Garage Door")  # OPEN
set_servo_angle(GARAGE_SERVO_PWM, 0,  "Garage Door")  # CLOSE

Your Tasks

Work through these steps in order. Your Pi is already set up and running — your job is to customise the iPad app frontend.

1

Connect to Your Pi and Verify Hardware

Find your assigned Pi's IP address from the class list. Connect the iPad app and use gpio_test.py via SSH to confirm the LED and both servos are wired correctly before writing any code.

ssh pi@10.20.12.XXX          # connect to your Pi
python3 gpio_test.py        # run hardware tests

📋 Your IP address: see the class allocation sheet (also on the whiteboard)

2

Open the Swift Playground on iPad

Download SimpsonsHouse.swiftpm from the GitHub repo and open it in Swift Playgrounds on your iPad. Tap Connect to House, enter your Pi's IP address, and verify all three controls respond. The app should already work — don't change the functionality files yet.

📦 Repo: github.com/roanvtkc/simpsons-house

3

Customise the iPad UI

Open the following files and redesign the look of the house — colours, icons, layout, and labels. Do not change the button actions or toggle functions.

HeroSectionView.swift
DeviceCard.swift
GarageDoorCard.swift
ConnectionStatusCard.swift

💡 Each file has // CUSTOMIZE: comments showing exactly where to make changes.

4

Read and Annotate the Python Backend

Open mqttlistener.py on your Pi and add your own comments explaining what each section does. Focus on understanding these three areas:

  • How on_message() routes commands to the correct device function
  • How set_servo_angle() converts an angle to a PWM duty cycle
  • How setup_gpio() configures the Pi's pins at startup
5

Extension: Add a New Device

Add a fourth controllable device of your own design. You'll need to modify both:

  • Python backend — add a new topic subscription, control function, and GPIO setup
  • Swift frontend — add a new device card that publishes to your new MQTT topic

💡 Start by copying an existing simple device (like the light) and adapting the topic name and pin number.

The Swift App: How It's Structured

The iPad app has two types of files — files you customise (marked with // CUSTOMIZE: comments) and files you never edit (marked // ⚠️ DO NOT EDIT). Understanding the property wrapper pattern first will make the customisation much easier.

@StateObject, @ObservedObject, and @Binding

The app's state — whether each device is on or off, whether the MQTT client is connected — all lives in SimpsonsHouseMQTTClient, an ObservableObject class. Three Swift property wrappers connect views to this shared state:

// ContentView.swift — the root view owns the MQTT client
struct ContentView: View {
    @StateObject private var mqttClient = SimpsonsHouseMQTTClient()
    // @StateObject creates and owns the object for the lifetime of this view
}

// DeviceControlsSection.swift — a child view that reads the same client
struct DeviceControlsSection: View {
    @ObservedObject var mqttClient: SimpsonsHouseMQTTClient
    // @ObservedObject watches an object created elsewhere (passed in)
}

// ConnectionStatusCard.swift — needs to toggle sheet booleans in the parent
struct ConnectionStatusCard: View {
    @ObservedObject var mqttClient: SimpsonsHouseMQTTClient
    @Binding var showingInfo: Bool   // @Binding = two-way link to parent's @State
    @Binding var showingLogs: Bool
}
@StateObject

Creates and owns the object

Used once, at the top. ContentView creates the MQTT client and keeps it alive for the whole app.

ContentView.swift

@ObservedObject

Watches an object passed in

Child views receive the client from ContentView. They watch it for changes but don't own it.

DeviceControlsSection, GarageDoorCard, etc.

@Binding

Two-way link to a parent @State

The sheet booleans (showingInfo, showingLogs) live in ContentView; @Binding lets a child toggle them.

ConnectionStatusCard.swift

Files You Customise

There are six customisable files. None of them change what the device does — only how it looks.

HeroSectionView.swift — Top banner: icon, title, subtitle, background

What you can change

  • SF Symbol name (house.fill, bolt.house.fill, tv.fill, sun.max.fill…)
  • Icon foreground colour
  • Wrap icon in a ZStack with a Circle badge background
  • App title and subtitle text strings
  • Solid colour, gradient, or .ultraThinMaterial background with .clipShape(RoundedRectangle(...))
  • Border overlay using .stroke()

Example customisations

// Swap the SF Symbol:
Image(systemName: "bolt.house.fill")
    .font(.system(size: 56))
    .foregroundStyle(.yellow)

// Add a coloured badge circle behind the icon:
ZStack {
    Circle()
        .fill(Color.yellow)
        .frame(width: 90, height: 90)
    Image(systemName: "house.fill")
        .font(.system(size: 40))
        .foregroundStyle(.white)
}

// Add a gradient card background:
.background(LinearGradient(
    colors: [.yellow, .orange],
    startPoint: .top, endPoint: .bottom
))
.clipShape(RoundedRectangle(cornerRadius: 20))
DeviceCard.swift — Reusable tile for Light and Front Door

What you can change

  • Card background that changes colour when isOn is true
  • Glowing drop shadow when device is active
  • Icon wrapped in a coloured circle ZStack
  • ON/OFF status as a styled Capsule() pill badge
  • Font weight, size, and colour of title / subtitle labels
Key insight: the isOn Bool and accentColor Color are passed as parameters — use them in your styling to make the card visually react to state.

Example customisations

// Active glow effect — the whole card changes when the device is ON:
.background(isOn ? accentColor.opacity(0.12) : Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: isOn ? accentColor.opacity(0.4) : .clear, radius: 8)

// Coloured badge pill for ON/OFF status:
Text(isOn ? "ON" : "OFF")
    .font(.caption2).fontWeight(.bold)
    .foregroundStyle(isOn ? accentColor : .secondary)
    .padding(.horizontal, 8).padding(.vertical, 2)
    .background(isOn ? accentColor.opacity(0.15) : Color(.systemGray6))
    .clipShape(Capsule())

// Icon in a coloured circle that responds to state:
ZStack {
    Circle()
        .fill(isOn ? accentColor.opacity(0.2) : Color(.systemGray6))
        .frame(width: 48, height: 48)
    Image(systemName: icon)
        .font(.title2)
        .foregroundStyle(isOn ? accentColor : .secondary)
}
GarageDoorCard.swift — Full-width card with OPEN + CLOSE buttons

What you can change

  • OPEN button background (active state = orange)
  • CLOSE button background (active state = blue)
  • Card background tint when door is open
  • Border .overlay() that appears in orange when open
  • State label styled as a coloured badge Capsule()
  • Icon switches automatically: garagegarage.open

Example customisations

// Coloured OPEN/CLOSE buttons with active state highlight:
// CLOSE button — highlighted (blue) when door is closed
.foregroundStyle(!mqttClient.deviceStates.garageOpen ? .white : .blue)
.background(!mqttClient.deviceStates.garageOpen ? Color.blue : Color.blue.opacity(0.12))
.clipShape(RoundedRectangle(cornerRadius: 10))

// OPEN button — highlighted (orange) when door is open
.foregroundStyle(mqttClient.deviceStates.garageOpen ? .white : .orange)
.background(mqttClient.deviceStates.garageOpen ? Color.orange : Color.orange.opacity(0.12))
.clipShape(RoundedRectangle(cornerRadius: 10))

// Card background that tints orange when the door is open:
.background(mqttClient.deviceStates.garageOpen
    ? Color.orange.opacity(0.05)
    : Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 16))
.overlay(
    RoundedRectangle(cornerRadius: 16)
        .stroke(mqttClient.deviceStates.garageOpen ? Color.orange : .clear, lineWidth: 2)
)
ConnectionStatusCard.swift — Connect / Disconnect panel with status dot

What you can change

  • Connect button — bold blue background, white text
  • Disconnect button — red background to signal danger
  • Status pill — green/red capsule background behind the dot
  • Pulsing .animation(.easeInOut.repeatForever()) on the status dot
  • Wrap the whole card in a RoundedRectangle with shadow

Example customisations

// Styled Connect / Disconnect button:
.foregroundStyle(.white)
.background(mqttClient.isConnected ? Color.red : Color.blue)
.clipShape(RoundedRectangle(cornerRadius: 16))

// Status pill with capsule background:
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(mqttClient.isConnected ? Color.green.opacity(0.1) : Color.red.opacity(0.1))
.clipShape(Capsule())

// Pulsing dot animation when connected:
Circle()
    .fill(mqttClient.isConnected ? Color.green : Color.red)
    .frame(width: 10, height: 10)
    .scaleEffect(mqttClient.isConnected ? 1.2 : 1.0)
    .animation(.easeInOut(duration: 0.8).repeatForever(), value: mqttClient.isConnected)
ConnectionPromptView.swift — Offline placeholder shown when not connected

What you can change

  • Icon — try wifi.exclamationmark, house.slash.fill, or antenna.radiowaves.left.and.right.slash
  • Heading and description text — personalise to your project's theme
  • Card background with .background(Color(.secondarySystemBackground)) and .clipShape(RoundedRectangle(cornerRadius: 20))
  • A fade-in .transition(.opacity) or scale animation on .onAppear
ContentView.swift — Overall screen background colour

The ScrollView inside ContentView has one customisation point — its background. Look for the comment // CUSTOMIZE: Change the background colour of the whole screen.

  • .background(Color.yellow.opacity(0.05)) — subtle warm tint
  • .background(Color("MyBackgroundColor")) — named asset colour
  • .background(LinearGradient(colors: [.blue.opacity(0.1), .purple.opacity(0.05)], startPoint: .top, endPoint: .bottom))

Swift UI Customisation Checklist

Pi Login Details

All Pis use the same credentials:

# SSH from your Mac (Terminal) or from iPad using Secure ShellFish
ssh pi@10.20.12.XXX
# Password: tkcraspberry

# Useful commands once connected:
sudo systemctl status simpsons-house    # is the controller running?
sudo journalctl -u simpsons-house -f    # watch live logs
python3 gpio_test.py                    # test hardware before coding

Troubleshooting

iPad app says "Disconnected" and won't connect

1. Make sure your iPad is on The King's Devices WiFi — not 4G/5G.

2. Check the IP address in the app settings matches your Pi's IP from the class list.

3. On the Pi, verify Mosquitto is running: sudo systemctl status mosquitto

App connects but nothing moves on the hardware

1. Run python3 gpio_test.py to check if the hardware itself works.

2. Check the controller is running: sudo systemctl status simpsons-house

3. Watch live logs while tapping the app: sudo journalctl -u simpsons-house -f

Servo moves to the wrong position

The servo may be physically mounted in a different orientation to how it was designed.

In mqttlistener.py, find control_garage_door() or control_door() and swap the 0° and 90° values.

Then restart the service: sudo systemctl restart simpsons-house

Swift Playground won't build / compile errors

Do not edit MQTTClient.swift or Models.swift — those files are marked DO NOT EDIT.

If you've accidentally broken something, download a fresh copy from the GitHub repo.

Resources