If you want to know how to access a Raspberry Pi remotely to control physical hardware, skip the GUI and use MQTT over a local LAN or Tailscale mesh. While SSH is fine for terminal commands, MQTT (Message Queuing Telemetry Transport) provides sub-10ms latency for machine-to-machine GPIO toggling, making it the definitive protocol for embedded remote access. Below, we break down the decision matrix, wire a Pi 5 relay node, and write a production-ready Python script with full error handling.
The Remote Access Decision Matrix
Before writing code, you must choose the right transport layer. Makers often default to VNC or raw SSH, which introduces unnecessary overhead for hardware control. Use this decision tree to pick your protocol.
| Method | Best For | Latency | Verdict |
|---|---|---|---|
| SSH | Headless terminal access, script deployment, log reading | Low (~20ms) | Pick for initial setup and OS maintenance. |
| VNC / RDP | Desktop GUI, pixel-level debugging, camera framing | High (>100ms) | Avoid for embedded hardware control. Too heavy. |
| MQTT | Machine-to-Machine GPIO, sensor telemetry, automation | Very Low (<10ms) | DEFAULT PICK for remote hardware actuation. |
Hardware Spec Sheet and Pin Mapping
This build targets the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS Bookworm (64-bit). The Pi 5 requires the active cooler for sustained GPIO switching loads, and its PCIe interface changes the underlying pin factory backend to lgpio.
Parts List
| Component | Exact Variant / Model | Est. Price (2026) |
|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB RAM) | $60.00 |
| Cooling | Official Pi 5 Active Cooler | $5.00 |
| Actuator | 2-Channel 5V Relay Module (Omron G5LE) | $6.50 |
| Indicator | 5mm Blue LED with 330Ω inline resistor | $0.50 |
| Wiring | 22 AWG stranded silicone hookup wire | $8.00 |
Pin Mapping Table
| BCM GPIO | Physical Pin | Function | Wire Color |
|---|---|---|---|
| 17 | 11 | Relay Channel 1 (IN1) | Orange |
| 27 | 13 | Status LED (Anode) | Blue |
| GND | 9 | Relay GND & LED Cathode | Black |
| 5V | 2 | Relay VCC (Power) | Red |
Step-by-Step: Building the Remote MQTT GPIO Node
Follow these steps to configure the OS, install the broker, and wire the hardware.
- Flash the OS: Use Raspberry Pi Imager to flash Raspberry Pi OS Bookworm (64-bit, Lite version). Enable SSH and configure your WiFi credentials in the advanced settings menu.
- Install the MQTT Broker: SSH into the Pi and install Mosquitto. This acts as the local message router.
sudo apt update && sudo apt install mosquitto mosquitto-clients -y - Configure Mosquitto for Remote Access: By default, Mosquitto only listens on localhost. Edit the config to allow LAN connections:
echo 'listener 1883' | sudo tee -a /etc/mosquitto/mosquitto.conf
echo 'allow_anonymous true' | sudo tee -a /etc/mosquitto/mosquitto.conf
sudo systemctl restart mosquitto - Install Python Dependencies: We use
gpiozerowith thelgpiobackend (mandatory for Pi 5) and the Eclipse Paho MQTT client.
sudo apt install python3-gpiozero python3-lgpio python3-pip -y
pip3 install paho-mqtt --break-system-packages - Wire the Hardware: Connect the 5V and GND pins to the relay module VCC and GND. Connect BCM 17 to IN1. Connect BCM 27 to the LED anode, and the LED cathode to GND. Warning: Never wire 5V directly to a BCM GPIO pin; it will instantly destroy the Pi 5's RP1 chip.
The Python Control Script
This script subscribes to an MQTT topic and toggles the physical relay. It uses the Paho MQTT v2.0 API, which requires explicit callback API versioning to prevent runtime deprecation warnings.
import paho.mqtt.client as mqtt
from gpiozero import LED, OutputDevice
from signal import pause
import logging
import sys
# --- Pin Definitions (BCM Numbering) ---
PIN_RELAY = 17
PIN_STATUS_LED = 27
# --- MQTT Configuration ---
BROKER_IP = '127.0.0.1' # Change to Pi's LAN IP if broker is external
BROKER_PORT = 1883
TOPIC_RELAY = 'pi5/gpio/relay'
# --- Hardware Initialization ---
# Target Board: Raspberry Pi 5 (4GB) / Pi 4 running Bookworm
relay = OutputDevice(PIN_RELAY, active_high=True, initial_value=False)
status_led = LED(PIN_STATUS_LED)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
def on_connect(client, userdata, flags, reason_code, properties):
"""Callback for when the client connects to the broker (Paho v2 API)."""
if reason_code == 0:
logging.info(f'Connected to broker. Subscribing to {TOPIC_RELAY}')
client.subscribe(TOPIC_RELAY, qos=1)
status_led.blink(on_time=0.5, off_time=0.5, n=3) # Visual handshake
else:
logging.error(f'Connection failed with reason code: {reason_code}')
sys.exit(1)
def on_message(client, userdata, msg):
"""Callback for incoming MQTT messages."""
try:
payload = msg.payload.decode('utf-8').strip().upper()
logging.info(f'Received payload: {payload} on topic: {msg.topic}')
if payload == 'ON':
relay.on()
status_led.on()
logging.info('Relay ENGAGED.')
elif payload == 'OFF':
relay.off()
status_led.off()
logging.info('Relay DISENGAGED.')
else:
logging.warning(f'Unknown command: {payload}. Expected ON or OFF.')
except Exception as e:
logging.error(f'Error processing message: {e}')
def main():
# Paho MQTT v2.0 requires explicit CallbackAPIVersion
client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
client.on_connect = on_connect
client.on_message = on_message
# Add automatic reconnect logic with exponential backoff
client.reconnect_delay_set(min_delay=1, max_delay=60)
try:
logging.info(f'Attempting connection to {BROKER_IP}:{BROKER_PORT}...')
client.connect(BROKER_IP, BROKER_PORT, keepalive=60)
client.loop_forever()
except KeyboardInterrupt:
logging.info('Shutting down gracefully...')
except Exception as e:
logging.critical(f'Fatal broker connection error: {e}')
finally:
relay.close()
status_led.close()
if __name__ == '__main__':
main()
Debugging: Exact Error Strings and Ranked Fixes
When remote hardware access fails, the issue is almost always network permissions or pin factory conflicts. Before digging into code, check these three things first:
- Broker Status: Run
systemctl status mosquitto. If it's dead, your config file likely has a syntax error. - Firewall Rules: Run
sudo ufw status. Port 1883 must be open if you are accessing the Pi from another machine on the LAN. - Pin Factory Backend: Ensure you aren't running the script inside a Docker container without the
--device /dev/gpiomemflag, which blocks hardware access.
Common Error Strings
ConnectionRefusedError: [Errno 111] Connection refusedRanked Causes:
1. Mosquitto service is not running (
sudo systemctl start mosquitto).2. Mosquitto is bound only to localhost, and you are running the script from a remote machine. Add
listener 1883 to mosquitto.conf.3. UFW firewall is blocking port 1883 (
sudo ufw allow 1883/tcp).
gpiozero.exc.PinFactoryFallback: Falling back from lgpio: 'gpiochip4' is not a valid chipRanked Causes:
1. You are running the script on a non-Pi machine (like a Windows PC) where
gpiozero defaults to mock pins. Move the script to the Pi.2. You are using an outdated OS (Bullseye or older) on a Pi 5. The Pi 5 requires Bookworm and the
lgpio backend. Upgrade your OS.3. A custom Device Tree overlay is hogging the GPIO chip. Check
/boot/firmware/config.txt for conflicting dtoverlay lines.
Extending vs Simplifying the Build
Depending on your deployment environment, you may need to scale this setup up or strip it down.
How to Extend (For Production / Outdoor Deployments)
- Add TLS Encryption: If exposing MQTT to the internet via port forwarding, generate self-signed certificates and configure Mosquitto to use port 8883 with
certfileandkeyfiledirectives. Never expose plaintext MQTT to the WAN. - Add Telemetry: Wire a BME280 I2C sensor to BCM 2 (SDA) and BCM 3 (SCL). Add a secondary thread in the Python script that publishes sensor data to
pi5/sensors/tempevery 60 seconds. - Use Tailscale: Instead of opening router ports, install Tailscale on the Pi and your remote laptop. Access the broker securely via the Tailscale IP address (e.g.,
100.x.y.z) without touching your router's firewall.
How to Simplify (For Quick Bench Testing)
- Ditch MQTT for Flask: If you only need to click a button on a web page to toggle the relay, install
flaskand create a single/toggleHTTP GET route. It removes the need for a dedicated broker service. - Use Home Assistant: If you already run Home Assistant, skip the custom Python script entirely. Use the built-in MQTT integration and configure the Pi as an ESPHome/MQTT discovery node.
For dedicated, low-latency embedded hardware control, MQTT remains the undisputed standard. Flash Bookworm, wire your relays to the correct BCM pins, and let the broker handle the routing.






