If you need a reliable, low-cost thin client for remote work or shop-floor HMI displays, building a dedicated RDP client for Raspberry Pi is the most robust approach. Rather than relying on a full desktop environment that can freeze or drift out of kiosk mode, the best practice is to pair xfreerdp (the FreeRDP X11 client) with a lightweight Python GPIO script. This gives you physical hardware buttons to force-reconnect dropped sessions and LED indicators for connection status, turning a standard single-board computer into an industrial-grade appliance.
This guide targets the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS Bookworm (64-bit). The code and wiring leverage the Pi 5's RP1 I/O controller via the gpiozero library.
Project Overview & Bill of Materials
To build this kiosk, you need components that can handle continuous 24/7 operation without thermal throttling or power brownouts. The Pi 5 requires a solid power delivery profile to maintain stable USB and HDMI outputs when driving an RDP session.
| Component | Exact Variant / Specification | Notes |
|---|---|---|
| Microcontroller | Raspberry Pi 5 (8GB RAM) | 4GB works, but 8GB handles high-res RDP caching better. |
| Power Supply | Official Raspberry Pi 27W USB-C PD | Do not use generic phone chargers; Pi 5 will throttle USB current. |
| Cooling | Active Cooler (Official) | Mandatory for Pi 5 under continuous graphical load. |
| Storage | 32GB+ A2-rated microSD (e.g., SanDisk Extreme) | A2 rating ensures fast random I/O for OS boot and logging. |
| Reconnect Switch | 12mm Momentary Tactile Pushbutton | Normally open (NO). Panel-mount preferred for enclosures. |
| Status LED | 5mm Green Diffused LED | Indicates active RDP session. |
| Resistors | 330Ω (1/4W) and 10kΩ (1/4W) | 330Ω for LED current limiting; 10kΩ for physical pull-up (optional if using internal). |
Hardware Wiring & Pin Mapping
While the RDP client itself is software-driven, adding physical GPIO controls elevates this from a hobby project to a deployable appliance. If the network drops, the user shouldn't need a keyboard to kill a frozen X11 window; they just press the physical reconnect button.
The Raspberry Pi 5 uses the RP1 southbridge for GPIO. The pin numbers below refer to the standard BCM numbering scheme, which gpiozero uses natively.
| BCM Pin | Physical Pin | Component | Wiring Notes |
|---|---|---|---|
| GPIO 17 | 11 | Reconnect Button | Switch between GPIO 17 and GND. Uses internal pull-up. |
| GPIO 27 | 13 | Status LED (Anode) | Connect through 330Ω resistor to LED anode (long leg). |
| GND | 9 | LED Cathode / Switch | Common ground for the button and LED cathode (short leg). |
Software Setup: Configuring the RDP Client for Raspberry Pi
We use xfreerdp (part of the FreeRDP project) because it supports modern RDP features like H.264/AVC444 graphics compression and Network Level Authentication (NLA), which Microsoft's legacy clients struggle with on ARM Linux. You can review the full FreeRDP command-line options on their official wiki.
- Update the OS and install dependencies:
sudo apt update && sudo apt upgrade -y
sudo apt install freerdp2-x11 python3-gpiozero python3-lgpio -y - Test a basic manual connection:
xfreerdp /v:192.168.1.100 /u:YourUser /p:YourPass /f /cert:ignore /gfx:avc444
Note: The/gfx:avc444flag forces H.264 hardware-accelerated decoding, drastically reducing CPU usage on the Pi 5. - Disable the desktop environment (Optional but recommended for kiosks):
sudo raspi-config-> System Options -> Boot / Auto Login -> Console Autologin. This prevents the heavy Wayfire/Pixde desktop from consuming RAM. - Configure auto-start via systemd: We will handle this via the Python script below, which acts as a watchdog.
Python GPIO Control & Kiosk Script
This script targets the Raspberry Pi 5 (Bookworm 64-bit). It launches the RDP session as a subprocess. If the session crashes or the user presses the physical GPIO 17 button, the script cleanly kills the old process and spawns a new one.
import subprocess
import time
import logging
from gpiozero import Button, LED
from signal import pause
# --- Pin Definitions (BCM Numbering) ---
PIN_RECONNECT_BTN = 17
PIN_STATUS_LED = 27
# --- Hardware Initialization ---
# pull_up=True enables the internal 3.3V pull-up resistor
reconnect_btn = Button(PIN_RECONNECT_BTN, pull_up=True, bounce_time=0.05)
status_led = LED(PIN_STATUS_LED)
# --- RDP Configuration ---
RDP_HOST = '192.168.1.100'
RDP_USER = 'admin'
RDP_PASS = 'securepassword123'
RDP_CMD = [
'xfreerdp',
f'/v:{RDP_HOST}',
f'/u:{RDP_USER}',
f'/p:{RDP_PASS}',
'/f', # Fullscreen
'/cert:ignore', # Bypass self-signed cert warnings
'/bpp:32', # 32-bit color depth
'/gfx:avc444', # Hardware accelerated H.264
'/audio-mode:1', # Mute local audio (saves bandwidth)
'/network:lan', # Optimize for LAN settings
'+clipboard', # Enable clipboard sharing
'/log-level:WARN' # Keep logs clean
]
# --- Logging Setup ---
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler('/var/log/rdp_kiosk.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger('RDP_Kiosk')
rdp_process = None
def kill_existing_session():
global rdp_process
if rdp_process and rdp_process.poll() is None:
logger.info('Terminating existing RDP session...')
rdp_process.terminate()
try:
rdp_process.wait(timeout=5)
except subprocess.TimeoutExpired:
rdp_process.kill()
rdp_process = None
def start_rdp_session():
global rdp_process
kill_existing_session()
status_led.on()
logger.info(f'Launching RDP session to {RDP_HOST}')
try:
rdp_process = subprocess.Popen(
RDP_CMD,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True
)
except FileNotFoundError:
logger.error('xfreerdp not found. Did you install freerdp2-x11?')
status_led.blink(on_time=0.2, off_time=0.2)
except Exception as e:
logger.error(f'Failed to launch RDP: {e}')
status_led.off()
def on_button_press():
logger.info('Physical reconnect button pressed.')
start_rdp_session()
# --- Event Binding & Watchdog ---
reconnect_btn.when_pressed = on_button_press
if __name__ == '__main__':
logger.info('RDP Kiosk Controller initialized. Waiting for button press or auto-start.')
# Auto-start on boot
start_rdp_session()
# Watchdog loop to monitor if xfreerdp crashes silently
try:
while True:
if rdp_process and rdp_process.poll() is not None:
stderr_out = rdp_process.stderr.read()
logger.warning(f'RDP session exited with code {rdp_process.returncode}')
if stderr_out:
logger.error(f'STDERR: {stderr_out.strip()}')
status_led.off()
# Do not auto-restart immediately to prevent boot-loops on auth failure.
# Wait for physical button press.
time.sleep(2)
except KeyboardInterrupt:
logger.info('Shutting down kiosk controller.')
kill_existing_session()
status_led.off()
Debugging: Exact Error Strings and Ranked Causes
When deploying an RDP client for Raspberry Pi in a kiosk environment, you will inevitably run into connection failures. Because we are running headless or in a minimal X11 wrapper, you must check the log file (/var/log/rdp_kiosk.log) or the console output. Here are the exact error strings xfreerdp throws and how to fix them.
The First Three Things to Check When It Fails
- Network Routing: Run
ping 192.168.1.100from the Pi. If it fails, check your Pi's static IP assignment or Wi-Fi credentials. - Windows Firewall / Port 3389: Ensure the host Windows machine allows inbound TCP traffic on port 3389. Windows Defender blocks this by default on 'Public' network profiles.
- NLA (Network Level Authentication) Mismatch: If the host requires NLA but the Pi's FreeRDP build lacks the correct TLS libraries, the handshake will fail. Try adding
-sec-nlato the command array to force standard RDP security for testing.
Ranked Causes for Specific Error Strings
Error String: ERRINFO_CONNECT_TRANSPORT_FAILED
Meaning: The TCP socket could not be established.
Ranked Causes:
1. The Windows host is asleep or powered off.
2. A firewall is dropping port 3389.
3. The IP address in the Python script is incorrect.
Error String: ERRINFO_SECURITY_NEGOTIATION_FAILED
Meaning: The TLS/NLA handshake was rejected by the server.
Ranked Causes:
1. The Windows host enforces NLA, but the Pi's FreeRDP version is outdated.
2. The user credentials in the script are wrong or the password expired.
3. FIPS compliance is enabled on the Windows host, rejecting the Pi's TLS cipher suite.
Error String: Authentication failure (often accompanied by ERRINFO_LOGOFF_BY_USER)
Meaning: The server explicitly rejected the login.
Ranked Causes:
1. Typo in the RDP_USER or RDP_PASS variables.
2. The account is locked out due to too many failed attempts.
3. The account lacks 'Allow log on through Remote Desktop Services' rights in Windows Local Security Policy.
sudo apt update && sudo apt upgrade) so that the underlying OpenSSL libraries support TLS 1.3, which modern Windows RDP listeners prefer.
FAQ: RDP Client for Raspberry Pi
Can I use Microsoft's official Remote Desktop client for Raspberry Pi?
No. Microsoft does not compile or distribute the official Windows Remote Desktop client (mstsc.exe or the modern UWP app) for ARM64 Linux. The official Raspberry Pi OS documentation and the broader Linux ecosystem rely entirely on FreeRDP (via xfreerdp) or Remmina (which uses FreeRDP as its backend) for RDP connectivity. FreeRDP is fully compliant with modern RDP 10.8 protocols and supports features like multi-monitor and USB redirection.
How do I extend this build to support multiple monitor outputs?
The Raspberry Pi 5 features dual micro-HDMI ports capable of driving two 4K displays at 60Hz. To extend the RDP session across both monitors, you must configure X11 to treat both outputs as a single unified framebuffer. First, use xrandr to position the displays (e.g., xrandr --output HDMI-1 --right-of HDMI-2). Then, append the /multimon flag to the RDP_CMD array in the Python script. FreeRDP will automatically detect the combined resolution and request a multi-monitor session from the Windows host.
Why does my RDP session drop when the Raspberry Pi goes to sleep?
By default, Raspberry Pi OS may blank the display or suspend USB/network interfaces to save power, which severs the RDP TCP socket. To prevent this, you must disable display power management signaling (DPMS). Run xset s off, xset -dpms, and xset s noblank in your .xinitrc or startup script before launching the Python kiosk controller. Additionally, ensure the Ethernet interface is not set to aggressive power-saving modes in /boot/firmware/config.txt.
How can I simplify the build if I don't need physical GPIO buttons?
If you are deploying this in a secure rack where physical tampering isn't an issue, you can strip out the gpiozero hardware logic entirely. Replace the Python script with a simple bash wrapper using a while true loop that restarts xfreerdp if it exits. Alternatively, use a window manager like matchbox or openbox with an autostart file that launches xfreerdp directly. However, you lose the ability to gracefully kill a frozen X11 window without SSH-ing into the Pi, which is why the physical button is highly recommended for remote or shop-floor deployments.






