If you want to add a remote control to your Raspberry Pi for indoor, line-of-sight applications, the direct answer is to use a VS1838B 38kHz IR receiver wired to a 3.3V GPIO pin, decoded via a Python edge-detection script. Skip the legacy lirc daemon entirely. Modern Raspberry Pi OS (Bookworm) uses Wayland and systemd user sessions, which breaks LIRC’s ancient uinput mapping and causes endless configuration headaches. By reading the raw NEC protocol pulses directly in Python, you get a responsive, daemon-free Raspberry Pi remote setup that works reliably on both the Pi 4 and the new Pi 5.

The Remote Protocol Decision Matrix

Before soldering, you need to pick the right wireless protocol for your environment. Hobbyists often default to whatever module is in their parts bin, which leads to range and interference failures. Use this decision path to select your hardware:

Protocol Hardware Module Range & Line of Sight Best Use Case Drawbacks
Infrared (IR) VS1838B (38kHz) ~10m, Strict Line-of-Sight Media centers, indoor room control, single-device triggering. Sunlight and CFL bulbs cause noise; blocked by walls.
433MHz RF RXB6 / SYN480R ~30m, Through Walls Garage doors, outdoor gates, multi-room smart home nodes. Heavy 433MHz noise floor in urban areas; no native ACK.
Bluetooth (BLE) Onboard Pi BT / ESP32 ~15m, Omnidirectional Gaming, mobile app integration, two-way telemetry. Requires pairing/bonding logic; higher latency for simple triggers.
Decision Termination: If your project is an indoor media player, a wall-mounted dashboard, or a single-room automation hub, choose the VS1838B IR Receiver. It requires no pairing, costs under $2, and the NEC protocol is universally supported by cheap universal remotes. We will build the IR solution for this guide.

Hardware Spec Sheet & Pin Mapping

This build targets the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS (Bookworm, 64-bit), but the hardware and code are fully backward-compatible with the Raspberry Pi 4 Model B. The Pi 5 uses the new RP1 southbridge chip, which changes how GPIO pin factories operate in Python, a critical detail we address in the code block below.

Parts List

  • Microcontroller: Raspberry Pi 5 (4GB) or Pi 4 Model B
  • IR Receiver: VS1838B 38kHz Infrared Receiver Module (the 3-pin breakout board with the built-in pull-up resistor and LED indicator, not the bare bulb)
  • Remote: Any standard NEC-protocol IR remote (e.g., generic Arduino IR remote kits or old TV remotes)
  • Wiring: 3x Female-to-Female Dupont jumper wires (22 AWG stranded)

Pin Mapping Table

The VS1838B module must be powered by 3.3V. Never connect the VCC pin to 5V. While some bare VS1838B bulbs tolerate 5V, the breakout modules often route VCC directly to the sensor's logic pin. Feeding 5V into a Pi 5 GPIO will permanently destroy the RP1 chip's pin pad.

VS1838B Module Pin Raspberry Pi Physical Pin BCM GPIO Number Wire Color (Suggested)
VCC (or +) Pin 1 (3.3V Power) N/A Red
GND (or -) Pin 6 (Ground) N/A Black
OUT (or S) Pin 11 GPIO 17 Yellow

Wiring the Receiver (Step-by-Step)

  1. De-energize the Pi: Shut down the Raspberry Pi completely (sudo shutdown -h now) and unplug the USB-C power supply. Never hot-wire GPIO pins while the Pi is booted.
  2. Connect Ground: Plug the black Dupont wire into the GND pin on the VS1838B and connect the other end to Physical Pin 6 on the Pi.
  3. Connect Power: Plug the red Dupont wire into the VCC pin on the sensor and connect it to Physical Pin 1 (3.3V). Verify visually that you are on the 3.3V rail, not the 5V rail (Pin 2 or 4).
  4. Connect Data: Plug the yellow Dupont wire into the OUT pin on the sensor and connect it to Physical Pin 11 (BCM GPIO 17).
  5. Verify Connections: Gently tug each wire. Ensure no stray copper strands from the Dupont connectors are bridging adjacent GPIO pins.
  6. Boot and Test Power: Plug the Pi back in and boot it up. When idle, the LED on the VS1838B breakout should remain off. If you point a remote at it and press a button, the LED should flicker faintly.

Python IR Pulse Decoder (Pi 4 & Pi 5 Compatible)

Standard IR libraries like lirc or older Python wrappers rely on kernel modules that fail on the Pi 5's RP1 architecture. Instead, we use gpiozero with the lgpio pin factory to read raw edge transitions and decode the NEC IR protocol timing directly in user space.

Prerequisite: Install the required libraries via terminal:
sudo apt update && sudo apt install python3-gpiozero python3-lgpio

#!/usr/bin/env python3
"""
Raspberry Pi Remote IR Decoder (NEC Protocol)
Targets: Raspberry Pi 5 / Pi 4 (Bookworm OS)
Library: gpiozero with lgpio pin factory (RP1 compatible)
"""

import time
from gpiozero import DigitalInputDevice, Button
from gpiozero.pins.lgpio import LGPIOFactory
import sys

# Force the LGPIO factory for Pi 5 RP1 chip compatibility
# This prevents the 'Cannot determine SoC type' error on newer OS builds
try:
    factory = LGPIOFactory()
except Exception as e:
    print(f"Failed to initialize LGPIO factory: {e}")
    sys.exit(1)

# Pin Definitions
IR_PIN = 17  # BCM GPIO 17 (Physical Pin 11)

# Initialize IR sensor with explicit pin factory
ir_receiver = DigitalInputDevice(IR_PIN, pull_up=True, pin_factory=factory)

# NEC Protocol Timing Constants (in seconds)
NEC_HEADER_PULSE = 0.009   # 9ms
NEC_HEADER_SPACE = 0.0045  # 4.5ms
NEC_BIT_PULSE = 0.0005625  # 562.5us
NEC_ONE_SPACE = 0.0016875  # 1.6875ms
NEC_ZERO_SPACE = 0.0005625 # 562.5us
TOLERANCE = 0.30           # 30% tolerance for Linux kernel scheduling jitter

def check_timing(measured, expected):
    return abs(measured - expected) <= (expected * TOLERANCE)

def decode_nec_frame():
    """Listens for an IR signal and decodes the 32-bit NEC frame."""
    # Wait for the initial 9ms pulse (pin goes LOW when IR is received)
    ir_receiver.wait_for_active(timeout=None)
    pulse_start = time.monotonic()
    
    ir_receiver.wait_for_inactive(timeout=0.02)
    pulse_duration = time.monotonic() - pulse_start
    
    if not check_timing(pulse_duration, NEC_HEADER_PULSE):
        return None  # Not a valid NEC header pulse
        
    # Measure the header space
    space_start = time.monotonic()
    ir_receiver.wait_for_active(timeout=0.02)
    space_duration = time.monotonic() - space_start
    
    if not check_timing(space_duration, NEC_HEADER_SPACE):
        return None  # Could be a repeat code or noise

    # Decode 32 bits (Address, Inverted Address, Command, Inverted Command)
    decoded_bits = 0
    for i in range(32):
        ir_receiver.wait_for_inactive(timeout=0.01)
        ir_receiver.wait_for_active(timeout=0.01)
        space_duration = time.monotonic() - space_start
        
        if check_timing(space_duration, NEC_ONE_SPACE):
            decoded_bits |= (1 << i)
        elif not check_timing(space_duration, NEC_ZERO_SPACE):
            return None  # Invalid bit spacing
            
        space_start = time.monotonic()
        
    # Extract command byte (bits 16-23)
    command = (decoded_bits >> 16) & 0xFF
    return command

def main():
    print(f"Listening for Raspberry Pi remote inputs on GPIO {IR_PIN}...")
    print("Press Ctrl+C to exit.")
    
    try:
        while True:
            cmd = decode_nec_frame()
            if cmd is not None:
                print(f"[RECEIVED] NEC Command Hex: 0x{cmd:02X} | Decimal: {cmd}")
                
                # Example Action Mapping
                if cmd == 0x45:  # Common 'CH-' button on cheap remotes
                    print("-> Action: Triggering Relay 1")
                elif cmd == 0x46: # Common 'CH' button
                    print("-> Action: Triggering Relay 2")
                    
    except KeyboardInterrupt:
        print("\nShutting down IR listener.")
    finally:
        ir_receiver.close()

if __name__ == "__main__":
    main()

Debugging: Exact Errors and Signal Noise

When working with raw IR decoding on a non-real-time OS like Linux, you will encounter edge cases. If your script fails or returns garbage data, follow this decision path.

The First Three Things to Check

  1. Ambient IR Noise: The VS1838B is highly sensitive to 38kHz noise from sunlight and Compact Fluorescent (CFL) bulbs. If the script prints random commands without you pressing a button, cup your hand over the sensor. If the noise stops, you need to shade the sensor or move it away from the window.
  2. Logic Level Mismatch: Verify with a multimeter that the VCC pin on the sensor is reading 3.3V relative to GND. If it reads 5V, you are backfeeding the Pi's GPIO pin and risking silicon damage.
  3. Pin Factory Fallback: If the script crashes immediately on startup, it is almost always a pin factory mismatch between the Pi 4 and Pi 5 architectures. Ensure python3-lgpio is installed via apt, not pip.

Ranked Causes for Exact Error Strings

Error 1: gpiozero.exc.PinFactoryFallback: Falling back from lgpio...

  • Cause A (Most Likely): The lgpio Python bindings are missing or installed via pip instead of the system package manager, causing a C-library mismatch with the RP1 chip.
  • Fix: Run sudo apt install python3-lgpio and remove any pip-installed versions (pip3 uninstall lgpio).
  • Cause B: You are running the script without sufficient permissions to access the /dev/gpiochip interface.
  • Fix: Ensure your user is in the gpio group (sudo usermod -aG gpio $USER) and reboot.

Error 2: OSError: [Errno 16] Device or resource busy

  • Cause: Another process (like a lingering lircd daemon or a previous crashed instance of your Python script) has locked GPIO 17.
  • Fix: Run sudo killall lircd and sudo killall python3, then restart your script.

Error 3: Script runs, but prints None or random decimals instead of valid Hex codes

  • Cause: Linux kernel scheduling latency is stretching your pulse timings beyond the 30% tolerance threshold defined in the code.
  • Fix: Increase the TOLERANCE constant in the Python script from 0.30 to 0.45. Alternatively, use a dedicated hardware decoder like the Raspberry Pi Pico wired via I2C to handle the microsecond timing, passing clean hex codes to the Pi 5.

Extending and Simplifying the Build

Once you have the raw hex codes printing to the terminal, you can map them to physical or digital actions.

Adding Physical Relays

To control mains appliances (like a lamp or fan), do not wire them directly to the Pi. Use a 5V opto-isolated relay module (like the Songle SRD-05VDC-SL-C). Wire the relay's IN pin to BCM GPIO 27. In the Python script, import gpiozero.OutputDevice, initialize the relay, and call relay.on() when your specific remote hex code is detected. Safety Note: Always treat mains voltage with extreme caution; if you are unsure about AC wiring, use a pre-built smart plug and trigger it via MQTT instead.

Simplifying with Media Center Integration

If your goal is simply to use the remote to control Kodi or VLC, you don't need custom Python scripts. Instead, use the gpiozero library to map the decoded hex codes directly to keyboard keystrokes using the evdev Python library. This injects standard media keys (Play/Pause, Volume Up) into the OS, allowing any media player to respond natively without writing application-specific logic.

By bypassing legacy daemons and reading the RP1 GPIO pins directly, you maintain full control over your Raspberry Pi remote setup, ensuring it survives future OS updates and kernel migrations.