Project Verdict & Architecture Decision

To control physical hardware from an Android device using a Raspberry Pi 3, the most reliable and lowest-latency architecture is a local MQTT broker (Mosquitto) running on the Pi, paired with a Python gpiozero script subscribing to topics, and an Android MQTT dashboard app publishing JSON commands. This setup bypasses the latency and privacy risks of cloud routing while keeping the Android app logic simple.

Architecture Decision Path

RequirementArchitecture ChoiceVerdict
Need < 50ms latency and 100% offline capabilityLocal MQTT (Mosquitto on Pi LAN)DEFAULT PICK
Need remote access over cellular networksCloud MQTT (HiveMQ) + Pi forwardingUse only if WAN access is mandatory
Need video streaming to AndroidWebRTC / RTSPOut of scope for simple GPIO switching
Difficulty: Intermediate | Time: 45 minutes | Cost: ~$45 (assuming Pi 3 is already owned)

Exact Parts List

  • Board: Raspberry Pi 3 Model B+ (1GB RAM) running Raspberry Pi OS Bookworm (64-bit)
  • Storage: 16GB SanDisk Extreme microSD (A1 rated for database/broker logging)
  • Relay Module: 4-Channel 5V Relay Module with optocoupler isolation (Songle SRD-05VDC-SL-C)
  • Wiring: 18 AWG stranded silicone wire, female-to-female Dupont jumpers
  • Power: 5V 2.5A Micro-USB power supply (CanaKit or official Pi foundation)
  • Android Client: Any MQTT app (we recommend IoT MQTT Panel from the Play Store)

Hardware Wiring & Pin Mapping

The Raspberry Pi 3 Model B+ GPIO header provides 3.3V logic, but most standard 4-channel relay modules require 5V for the relay coils and 3.3V-compatible logic for the optocoupler inputs. We will power the relay VCC from the Pi's 5V rail, and use BCM GPIO pins for the control signals.

Hardware Warning: Never power inductive loads (like motors or large solenoids) directly from the Pi's 5V rail. The relay module switches the external load, but the relay coils themselves draw ~70mA each. Four coils firing simultaneously draw ~280mA, which is within the Pi 3's ~600mA 5V rail limit, but leaves little headroom for USB peripherals.

Pin Mapping Table (BCM Numbering)

Relay Module PinRaspberry Pi 3 Pin (Physical)BCM GPIOFunction
VCCPin 25V PowerPowers relay coils
GNDPin 9GroundCommon ground
IN1Pin 11GPIO 17Relay 1 Control
IN2Pin 13GPIO 27Relay 2 Control
IN3Pin 15GPIO 22Relay 3 Control
IN4Pin 16GPIO 23Relay 4 Control

Raspberry Pi 3 Software Setup & Python Code

This code targets the Raspberry Pi 3 Model B+ running Raspberry Pi OS Bookworm. It uses gpiozero (which defaults to the lgpio pin factory in Bookworm) and paho-mqtt v2.0.

Step 1: Install Dependencies

SSH into your Pi 3 and install the Mosquitto broker and Python libraries:

sudo apt update
sudo apt install mosquitto mosquitto-clients python3-gpiozero python3-pip -y
pip3 install paho-mqtt --break-system-packages

Step 2: The Python Control Script

Save the following code as android_relay_hub.py. Notice the use of active_high=False in the OutputDevice initialization; most optocoupler relay boards are "Active LOW", meaning the relay engages when the GPIO pin is pulled to ground (0V).

import paho.mqtt.client as mqtt
from gpiozero import OutputDevice
from signal import pause
import json
import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# Pin definitions (BCM numbering) - Active LOW for standard optocoupler relays
RELAY_PINS = {
    'relay_1': OutputDevice(17, active_high=False, initial_value=False),
    'relay_2': OutputDevice(27, active_high=False, initial_value=False),
    'relay_3': OutputDevice(22, active_high=False, initial_value=False),
    'relay_4': OutputDevice(23, active_high=False, initial_value=False)
}

MQTT_BROKER = 'localhost'
MQTT_PORT = 1883
MQTT_TOPIC = 'home/pi3/relays/control'

def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code == 0:
        logging.info('Connected to Mosquitto broker')
        client.subscribe(MQTT_TOPIC)
    else:
        logging.error(f'Connection failed with code: {reason_code}')

def on_message(client, userdata, msg):
    try:
        payload = json.loads(msg.payload.decode('utf-8'))
        target = payload.get('target')
        state = payload.get('state')
        
        if target in RELAY_PINS and state in ['ON', 'OFF']:
            if state == 'ON':
                RELAY_PINS[target].on()
                logging.info(f'{target} engaged')
            else:
                RELAY_PINS[target].off()
                logging.info(f'{target} disengaged')
        else:
            logging.warning(f'Invalid payload structure: {payload}')
    except json.JSONDecodeError:
        logging.error('Received malformed JSON payload from Android client')
    except Exception as e:
        logging.error(f'Unexpected error processing message: {e}')

if __name__ == '__main__':
    # paho-mqtt v2.0 requires explicit API version declaration
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id='pi3_android_hub')
    client.on_connect = on_connect
    client.on_message = on_message
    
    try:
        client.connect(MQTT_BROKER, MQTT_PORT, 60)
        client.loop_start()
        logging.info('Hub active. Waiting for Android MQTT commands...')
        pause()
    except KeyboardInterrupt:
        logging.info('Shutting down safely...')
    except Exception as e:
        logging.critical(f'Broker connection failed: {e}')
    finally:
        client.loop_stop()
        for pin_name, device in RELAY_PINS.items():
            device.off()
            device.close()

Android Client Configuration

On your Android device, download IoT MQTT Panel (or a similar MQTT client). Configure the connection to your Pi 3's local IP address (e.g., 192.168.1.50) on port 1883.

Create a toggle switch widget in the app and set its publish topic to home/pi3/relays/control. The payload must be formatted as JSON. For example, to turn on Relay 2, configure the switch to publish:

{"target": "relay_2", "state": "ON"}

When you toggle the switch on your Android screen, the payload hits the Mosquitto broker on the Pi 3 in roughly 5-15ms over a standard 2.4GHz Wi-Fi network, triggering the Python callback and clicking the physical relay.

Debugging: When the Relays Don't Click

Embedded hardware debugging requires a systematic approach. If your Android commands aren't triggering the physical relays, check these exact error strings and follow the ranked causes.

The First Three Things to Check

  1. Broker Status: Run systemctl status mosquitto. If it's dead, the Python script will instantly fail to connect.
  2. 5V Rail Voltage: Measure Pin 2 to Pin 6 with a multimeter while triggering a relay. If it drops below 4.7V, the Pi 3's polyfuse is tripping or your power supply is inadequate. The optocoupler LEDs won't fire reliably under 4.8V.
  3. Optocoupler Jumper: Look at the relay board. There is a jumper connecting VCC and JD-VCC. For standard Pi 3 wiring, this jumper must be in place. If removed, the board expects a separate 5V logic supply.

Ranked Error Causes & Fixes

Exact Error StringRankRoot CauseFix
gpiozero.exc.PinFactoryFallback: Falling back from rpigpio: No access to /dev/mem 1 Legacy RPi.GPIO is being called without root, or lgpio is missing in Bookworm. Ensure you are using gpiozero (which defaults to lgpio in Bookworm). Run sudo apt install python3-lgpio. Do not use sudo to run the script; run it as the standard pi user.
ConnectionRefusedError: [Errno 111] Connection refused 2 Mosquitto is not running, or it is bound only to localhost but the Python script is trying to reach a remote IP. Start the broker: sudo systemctl start mosquitto. Verify localhost is used in the Python script if running on the same Pi.
Warning: Received malformed JSON payload from Android client 3 The Android MQTT app is sending a plain string (e.g., "ON") instead of the expected JSON dictionary. Reconfigure the Android app widget payload to strictly output {"target": "relay_1", "state": "ON"}.

Scaling the Build: Extend or Simplify

Once the baseline Android-to-Pi 3 relay hub is stable, you will inevitably need to adapt it to your specific environment. Here is how to modify the build without rewriting the core architecture.

How to Simplify (For Single-Device Testing)

If you don't need 4 channels and just want to test the Android MQTT link with a simple indicator, strip out the relay module entirely. Swap the hardware for a single 2N2222 NPN transistor, a 1kΩ base resistor, and a 5V active piezo buzzer. Connect the buzzer's positive leg to the Pi's 5V rail, the negative leg to the transistor's collector, the emitter to GND, and the base to GPIO 17 via the resistor. The exact same Python code will drive the transistor, allowing you to verify the Android MQTT payload logic without dealing with optocoupler isolation quirks.

How to Extend (Adding Remote Sensor Nodes)

The true power of MQTT is decoupling the controller from the sensor. To extend this build into a multi-room system, leave the Pi 3 and Android app exactly as they are. Add an ESP32-WROOM-32 node in another room running a PIR motion sensor. Program the ESP32 to publish to home/sensors/motion. Add a second subscription in your Pi 3 Python script:

client.subscribe('home/sensors/motion')

When the ESP32 detects motion, the Pi 3 receives the payload, processes it, and automatically toggles relay_1 while simultaneously publishing a state update back to the Android app via a secondary topic (e.g., home/pi3/relays/status). This turns your basic Android remote control into a fully automated, event-driven smart home edge server.

For deeper reading on the underlying protocols, refer to the Eclipse Mosquitto documentation for broker configuration, and the gpiozero OutputDevice API for advanced relay toggling patterns. Always consult the official Raspberry Pi hardware guides when calculating power draw limits on the 5V rail.