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. |
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)
- 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. - 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.
- 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).
- Connect Data: Plug the yellow Dupont wire into the OUT pin on the sensor and connect it to Physical Pin 11 (BCM GPIO 17).
- Verify Connections: Gently tug each wire. Ensure no stray copper strands from the Dupont connectors are bridging adjacent GPIO pins.
- 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
- 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.
- 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.
- 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-lgpiois installed viaapt, notpip.
Ranked Causes for Exact Error Strings
Error 1: gpiozero.exc.PinFactoryFallback: Falling back from lgpio...
- Cause A (Most Likely): The
lgpioPython bindings are missing or installed viapipinstead of the system package manager, causing a C-library mismatch with the RP1 chip. - Fix: Run
sudo apt install python3-lgpioand remove any pip-installed versions (pip3 uninstall lgpio). - Cause B: You are running the script without sufficient permissions to access the
/dev/gpiochipinterface. - Fix: Ensure your user is in the
gpiogroup (sudo usermod -aG gpio $USER) and reboot.
Error 2: OSError: [Errno 16] Device or resource busy
- Cause: Another process (like a lingering
lircddaemon or a previous crashed instance of your Python script) has locked GPIO 17. - Fix: Run
sudo killall lircdandsudo 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
TOLERANCEconstant in the Python script from0.30to0.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.






