A Raspberry Pi lamp server bridges the gap between embedded hardware and web APIs, allowing you to control physical AC or DC loads via HTTP requests. Whether you are building a custom smart home node or a remote lab power switch, the core challenge is reliably toggling GPIO pins while serving web traffic without crashing the OS. This guide targets the Raspberry Pi 4 Model B (2GB RAM) running Raspberry Pi OS Bookworm (64-bit), utilizing the modern gpiozero library and Flask to ensure compatibility with the latest kernel updates.
Target Board: Raspberry Pi 4 Model B (2GB). The code is fully compatible with the Pi 5, provided you use the updated Bookworm pin factory backend detailed below.
Hardware BOM and GPIO Pin Mapping
Before writing code, you need the right components. The transition to Raspberry Pi OS Bookworm deprecated the legacy RPi.GPIO library in favor of lgpio and gpiozero. Your hardware must support 3.3V logic triggering, which standard optocoupler relay modules handle perfectly.
| Component | Exact Model / Variant | Est. Price | Role & Technical Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi 4 Model B (2GB) | $45.00 | Main server. 2GB is sufficient for Flask + headless OS. |
| Power Supply | CanaKit 3.5A USB-C (Official) | $12.00 | Prevents brownouts when the relay coil energizes. |
| Relay Module | HiLetgo 1-Ch 5V Relay (Songle SRD-05VDC-SL-C) | $6.50 | Optocoupler isolated. Handles 10A @ 120V AC. |
| Wiring (Mains) | 18 AWG Stranded THHN (Black/White/Green) | $8.00 | Required for 120V AC lamp connections. Do not use 22 AWG jumper wires for mains. |
| Connectors | Wago 221 Lever Nuts (3-Port) | $5.00 | Secure, vibration-proof splices for the AC load side. |
GPIO Pin Mapping
We use physical Pin 11 (BCM GPIO 17) for the relay trigger. The relay module requires a 5V power rail to energize the coil, while the optocoupler LED inside the module safely accepts the Pi's 3.3V GPIO logic high signal.
| Raspberry Pi Pin (Physical) | BCM GPIO | Relay Module Pin | Wire Color / Note |
|---|---|---|---|
| Pin 2 | 5V Power | VCC (or JD-VCC) | Red (Provides coil current) |
| Pin 6 | Ground | GND | Black (Common ground) |
| Pin 11 | GPIO 17 | IN (Signal) | Blue (3.3V logic trigger) |
| N/A (Mains Side) | N/A | COM / NO | 18 AWG Black (Hot Line switching) |
Mains Wiring and Relay Setup
- Prepare the Relay Module: Locate the blue jumper cap on the relay module bridging
VCCandJD-VCC. Leave this jumper in place for this build. Removing it requires a separate 5V power supply for the coil, which is unnecessary for a single-channel Pi 4 setup. - Connect Low Voltage: Wire the Pi 5V, GND, and GPIO 17 to the relay's VCC, GND, and IN pins using 22 AWG Dupont jumpers.
- Wire the AC Load (Hot): Cut the Hot (black) wire of your lamp's power cord. Strip 1/2 inch of insulation. Connect the cord's source-side hot wire to the relay's
COM(Common) terminal screw. Connect the lamp-side hot wire to theNO(Normally Open) terminal screw. - Wire the AC Neutral and Ground: Use Wago lever nuts to splice the cord's source-side neutral (white) directly to the lamp-side neutral. Do the same for the bare copper ground wires. The relay does not switch the neutral or ground.
- Enclose: Never leave a bare relay module exposed to mains voltage. Mount the Pi and relay in a non-conductive, ventilated project box (e.g., ABS plastic) with proper cable glands for strain relief.
Python Flask Lamp Server Code
With Raspberry Pi OS Bookworm, the legacy RPi.GPIO library throws memory access errors unless run as root. The official, secure path is gpiozero backed by lgpio. Install the dependencies via SSH:
sudo apt update
sudo apt install python3-gpiozero python3-lgpio python3-flask
Create a file named lamp_server.py and paste the following complete, compilable code. It includes explicit pin definitions, route error handling, and graceful shutdown hooks.
from flask import Flask, jsonify, request
from gpiozero import OutputDevice
import signal
import sys
import logging
# Configure logging for systemd journal integration
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
app = Flask(__name__)
# Pin Definition: BCM GPIO 17 (Physical Pin 11)
# active_high=True means 3.3V turns the optocoupler ON
# initial_value=False ensures the lamp is OFF on boot
LAMP_RELAY = OutputDevice(17, active_high=True, initial_value=False)
logging.info('GPIO 17 initialized. Lamp state: OFF')
@app.route('/api/lamp/status', methods=['GET'])
def get_status():
try:
state = 'on' if LAMP_RELAY.is_active else 'off'
return jsonify({'status': 'success', 'lamp': state}), 200
except Exception as e:
logging.error(f'Status check failed: {str(e)}')
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/lamp/toggle', methods=['POST'])
def toggle_lamp():
try:
LAMP_RELAY.toggle()
state = 'on' if LAMP_RELAY.is_active else 'off'
logging.info(f'Lamp toggled to: {state}')
return jsonify({'status': 'success', 'lamp': state}), 200
except Exception as e:
logging.error(f'Toggle failed: {str(e)}')
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/api/lamp/<action>', methods=['POST'])
def control_lamp(action):
try:
if action == 'on':
LAMP_RELAY.on()
elif action == 'off':
LAMP_RELAY.off()
else:
return jsonify({'status': 'error', 'message': 'Invalid action. Use on/off.'}), 400
logging.info(f'Lamp set to: {action}')
return jsonify({'status': 'success', 'lamp': action}), 200
except Exception as e:
logging.error(f'Control failed: {str(e)}')
return jsonify({'status': 'error', 'message': str(e)}), 500
def graceful_shutdown(signum, frame):
logging.info('Shutdown signal received. Turning off lamp and cleaning up GPIO.')
LAMP_RELAY.off()
LAMP_RELAY.close()
sys.exit(0)
# Catch SIGINT and SIGTERM for clean GPIO release
signal.signal(signal.SIGINT, graceful_shutdown)
signal.signal(signal.SIGTERM, graceful_shutdown)
if __name__ == '__main__':
# Bind to 0.0.0.0 to accept LAN requests, port 5000
app.run(host='0.0.0.0', port=5000, debug=False)
Run the server with python3 lamp_server.py. You can test it from another machine on your LAN using cURL: curl -X POST http://<PI_IP_ADDRESS>:5000/api/lamp/on.
Debugging GPIO and Server Errors
When migrating older tutorials to modern Pi OS, or when physical wiring fails, you will encounter specific error strings. Here is how to diagnose them.
Exact Error: gpiozero.exc.BadPinFactory: Unable to load any default pin factory!
Ranked Causes & Fixes:
- Missing lgpio backend (Most Likely): Bookworm stripped out legacy pin factories. You forgot to install the C-extension backend. Fix: Run
sudo apt install python3-lgpio. - Running in a Docker Container without Privileges: Docker blocks access to
/dev/gpiomemby default. Fix: Pass--device /dev/gpiomemin yourdocker runcommand. - Corrupted gpiozero environment: You installed
gpiozeroviapipinside a virtual environment but lack the system-levellgpiobindings. Fix: Usesudo apt install python3-gpiozeroto get the OS-managed package, or installrpi-lgpiovia pip in your venv.
Exact Error: RuntimeError: No access to /dev/mem. Try running as root!
Ranked Causes & Fixes:
- Using Legacy RPi.GPIO on Bookworm: You copied code from a 2021 tutorial using
import RPi.GPIO as GPIO. Fix: Rewrite the script usinggpiozeroas shown above. Running web servers as root (viasudo) to fix this is a severe security risk. - Incorrect User Groups: Your user is not in the
gpiogroup. Fix: Runsudo usermod -aG gpio $USERand reboot.
The First Three Things to Check When the Relay Fails to Click
If the server returns a 200 OK but the physical relay does not click:
- Measure the IN Pin Voltage: Use a multimeter to measure between the Relay IN pin and GND while sending an 'ON' command. It should read ~3.2V. If it reads 0V, your GPIO pin is dead or misconfigured in software.
- Check the JD-VCC Jumper: If you removed the jumper thinking you needed external power but didn't supply it, the coil has no ground reference. Put the jumper back.
- Verify Pi Power Supply Headroom: The relay coil draws ~70mA. If your Pi is running on a cheap phone charger, the 5V rail may droop below 4.6V, causing the optocoupler to fail to trigger. Check the Pi's syslog for
Under-voltage detectedwarnings.
Scaling: Simplify or Extend the Build
Depending on your end goal, a Flask server on a Pi 4 might be overkill, or it might be just the foundation.
How to Simplify (The Minimalist Node)
If you only need a single lamp controller and want to reduce power draw and BOM cost, swap the Pi 4 for a Raspberry Pi Zero 2 W ($15). Because the Zero 2 W shares the same BCM2710A1 architecture as the Pi 3, the exact same gpiozero Bookworm code runs without modification. To eliminate the Flask web server overhead entirely, replace the HTTP API with a lightweight MQTT client using the paho-mqtt library, subscribing to a home/lamp/set topic. This reduces RAM usage from ~80MB (Flask) to under 15MB, leaving the Zero 2 W completely idle between commands.
How to Extend (Sensor Fusion and Automation)
To turn this from a remote switch into an autonomous system, add an HC-SR501 PIR Motion Sensor. Wire the PIR VCC to 5V, GND to GND, and OUT to BCM GPIO 27. Using gpiozero's MotionSensor class, you can attach a callback function that turns the lamp on for 5 minutes whenever motion is detected, overriding the Flask API state. For production home automation, abandon the custom Flask app and integrate the Pi as an MQTT GPIO bridge for Home Assistant, allowing you to mix your bare-metal relay node with commercial Zigbee/Matter devices in a single unified dashboard.
References: For the latest on Bookworm GPIO changes, consult the Raspberry Pi OS Bookworm Release Notes. For Flask API routing and error handling patterns, refer to the Official Flask 3.0 Documentation. Detailed pin factory backend configurations are maintained in the gpiozero ReadTheDocs.






