The biggest trap when building a raspberry pi google assistant with screen in 2026 is attempting to run the official Google Assistant SDK on Raspberry Pi OS Bookworm. Bookworm uses PipeWire and Wayland by default, which fundamentally breaks the SDK’s hardcoded ALSA audio hooks and X11 GUI expectations. You will spend days fighting audio routing daemons instead of building your project.
The direct, decision-forward answer: Use a Raspberry Pi 4 Model B (4GB) running Raspberry Pi OS (Legacy, 64-bit) Bullseye, paired with a 5-inch HDMI/USB touchscreen and a dedicated USB audio DAC. This guide provides the exact hardware matrix, pin mappings, and complete PyQt6 Python code to get a push-to-talk smart display running reliably on your workbench.
The Hardware Decision: Screen & Board Selection
Choosing the right display dictates your CPU overhead, GPIO availability, and driver stability. Here is the decision matrix for Pi-based smart displays:
| Screen Type | Example Model | Pros & Cons | Verdict |
|---|---|---|---|
| SPI (3.5") | ILI9486 / XPT2046 | Cheap ($25). High CPU overhead for frame-buffer rendering. Touch calibration is a nightmare on custom kernels. | Reject for GUI assistants. |
| DSI (Official 7") | Raspberry Pi Touch Display | Native integration ($60+). Blocks the physical GPIO header, requiring a messy ribbon cable workaround for buttons. | Reject if using physical buttons. |
| HDMI + USB (5") | Waveshare 5" HDMI Touch | Standard DRM video drivers, standard USB HID touch ($55). Leaves GPIO header completely exposed for hardware buttons. | WINNER: Use this. |
Concrete Pick: Waveshare 5" HDMI Touchscreen (800x480). It uses standard Linux framebuffer drivers, meaning your PyQt6 UI will render without custom SPI overlay configurations.
Parts List & Pin Mapping
This spec sheet assumes you are building a robust bench prototype. Prices reflect typical 2026 electronics distributor rates.
Time to Build: 2.5 hours hardware, 1.5 hours software configuration.
| Component | Exact Variant / Model | Est. Cost | Why this specific part? |
|---|---|---|---|
| Compute Board | Raspberry Pi 4 Model B (4GB RAM) | $55.00 | 4GB prevents OOM kills when PyQt6 and the Assistant gRPC stream run concurrently. |
| Display | Waveshare 5" HDMI LCD (B) | $55.00 | Capacitive touch via USB, standard HDMI video. No SPI CPU tax. |
| Audio DAC | Sabrent USB-SA (USB Sound Card) | $8.00 | Critical: Bypasses Pi HDMI audio routing. The Assistant SDK expects a standard ALSA PCM device; HDMI audio often defaults to unsupported sample rates. |
| MicroSD | SanDisk Extreme 32GB A2 | $12.00 | A2 rating ensures the OS doesn't stutter during audio buffer writes. |
| Push-to-Talk | 12mm Momentary Tactile Switch | $0.50 | Wired to GPIO 17. Pull-up enabled in software. |
GPIO Pin Mapping
| Function | Pi GPIO (BCM) | Physical Pin | Wiring Notes |
|---|---|---|---|
| Push-to-Talk Button | GPIO 17 | Pin 11 | Button connects Pin 11 to Pin 9 (GND). Internal pull-up used. |
| Status LED (Listening) | GPIO 27 | Pin 13 | Pin 13 -> 330Ω Resistor -> LED Anode -> LED Cathode -> Pin 14 (GND). |
Assembly & OS Configuration
Do not use the standard Raspberry Pi Imager default. You must select the Legacy OS to maintain ALSA compatibility with the Google SDK.
- Flash the OS: Open Raspberry Pi Imager. Select Raspberry Pi 4 -> Raspberry Pi OS (Other) -> Raspberry Pi OS (Legacy, 64-bit) Bullseye with Desktop. (Source: Raspberry Pi Legacy Images).
- Configure Headless/SSH: In the Imager settings gear, enable SSH (password auth), set your WiFi, and set a username (e.g.,
pi). - Hardware Assembly: Mount the Pi to the back of the Waveshare screen using the included M2.5 standoffs. Connect the short HDMI-to-HDMI cable and the micro-USB-to-USB-A cable for touch. Plug the Sabrent USB DAC into a USB 2.0 port (black), leaving the USB 3.0 ports (blue) free to avoid 2.4GHz WiFi interference.
- Force USB Audio: Boot the Pi, open a terminal, and edit the ALSA config:
sudo nano /usr/share/alsa/alsa.conf. Changedefaults.ctl.card 0anddefaults.pcm.card 0to1(assuming the USB DAC registers as card 1. Verify withaplay -l). - Install Dependencies:
sudo apt update sudo apt install python3-pip python3-venv portaudio19-dev libffi-dev libssl-dev python3 -m venv ~/assistant-env source ~/assistant-env/bin/activate pip install google-assistant-library==1.1.4 google-auth-oauthlib PyQt6 RPi.GPIO - Google Cloud Auth: Follow the official Google Assistant SDK Python guide to generate your
credentials.jsonfrom the Google Cloud Console, then rungoogle-oauthlib-tool --scope https://www.googleapis.com/auth/assistant-sdk-prototype --save --headless --client-secrets /path/to/client_secret.json.
The Code: PyQt6 UI & Assistant Backend
This script targets the Raspberry Pi 4B (4GB). It initializes a PyQt6 window to render the UI state on the touchscreen, while the Google Assistant library runs in a background thread. The physical button on GPIO 17 triggers the conversation.
import sys
import os
import threading
import google.oauth2.credentials
from google.assistant.library import Assistant
from google.assistant.library.event import EventType
from PyQt6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
from PyQt6.QtCore import Qt, pyqtSignal, QObject
import RPi.GPIO as GPIO
# --- PIN DEFINITIONS ---
PTT_PIN = 17 # Push-to-Talk Button (Active LOW)
LED_PIN = 27 # Status LED (Active HIGH)
# --- QT SIGNAL BRIDGE ---
class AssistantSignals(QObject):
update_ui = pyqtSignal(str)
set_led = pyqtSignal(bool)
class GoogleAssistantApp(QWidget):
def __init__(self, signals):
super().__init__()
self.signals = signals
self.init_ui()
self.signals.update_ui.connect(self.update_status_text)
self.signals.set_led.connect(self.update_led)
def init_ui(self):
self.setWindowTitle('Pi Assistant')
self.setGeometry(0, 0, 800, 480) # Waveshare 5" native resolution
self.setStyleSheet("background-color: #121212; color: white;")
layout = QVBoxLayout()
self.status_label = QLabel("Ready. Press Button.", self)
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setStyleSheet("font-size: 32px; font-weight: bold;")
layout.addWidget(self.status_label)
self.setLayout(layout)
def update_status_text(self, text):
self.status_label.setText(text)
def update_led(self, state):
GPIO.output(LED_PIN, GPIO.HIGH if state else GPIO.LOW)
def process_event(signals, event):
"""Handles Google Assistant SDK events and bridges them to the UI."""
try:
if event.type == EventType.ON_CONVERSATION_TURN_STARTED:
signals.update_ui.emit("Listening...")
signals.set_led.emit(True)
elif event.type == EventType.ON_RECOGNIZING_SPEECH_FINISHED:
signals.update_ui.emit(f"You: {event.args['text']}")
elif event.type == EventType.ON_RESPONDING_STARTED:
signals.update_ui.emit("Thinking...")
elif event.type == EventType.ON_CONVERSATION_TURN_FINISHED:
signals.update_ui.emit("Ready. Press Button.")
signals.set_led.emit(False)
elif event.type == EventType.ON_ASSISTANT_ERROR:
signals.update_ui.emit(f"Error: {event.args}")
except Exception as e:
print(f"Event processing error: {e}")
def main():
# GPIO Setup
GPIO.setmode(GPIO.BCM)
GPIO.setup(PTT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(LED_PIN, GPIO.OUT, initial=GPIO.LOW)
app = QApplication(sys.argv)
signals = AssistantSignals()
ui = GoogleAssistantApp(signals)
ui.show()
# Auth & Assistant Setup
try:
cred_path = os.path.expanduser('~/.config/google-oauthlib-tool/credentials.json')
with open(cred_path, 'r') as f:
credentials = google.oauth2.credentials.Credentials(token=None, **json.load(f))
assistant = Assistant(credentials)
assistant.start()
# Event Loop Thread
def event_loop():
for event in assistant:
process_event(signals, event)
threading.Thread(target=event_loop, daemon=True).start()
except FileNotFoundError:
print("FATAL: credentials.json not found. Run google-oauthlib-tool first.")
sys.exit(1)
except Exception as e:
print(f"FATAL: Assistant init failed: {e}")
sys.exit(1)
# Hardware Button Polling (Non-blocking via QTimer would be better,
# but simple thread for bench prototype)
def button_poll():
while True:
if GPIO.input(PTT_PIN) == GPIO.LOW:
assistant.start_conversation()
import time
time.sleep(0.5) # Debounce
threading.Thread(target=button_poll, daemon=True).start()
sys.exit(app.exec())
if __name__ == '__main__':
import json
main()
google-assistant-library relies on the legacy event stream. If Google fully deprecates the v1alpha2 endpoints in a future SDK update, you will need to migrate the assistant.start() logic to the gRPC embeddedassistant API. For 2026, the library wrapper remains the most stable path for Bullseye.
Debugging: First 3 Things to Check When It Fails
Embedded audio projects fail in predictable ways. If your assistant crashes or stays silent, check these three exact error strings in your terminal output.
1. The Audio Underrun
Exact Error String: ALSA lib pcm.c:8526:(snd_pcm_recover) underrun occurred followed by robotic, stuttering voice output.
- Cause: The Google Assistant SDK requests a 16kHz/48kHz sample rate, but the Pi's default ALSA config is trying to force a 44.1kHz resample through the CPU, causing buffer starvation.
- Fix: Create a
~/.asoundrcfile and force the USB DAC to handle the conversion natively:pcm.!default { type plug slave.pcm "hw:1,0" slave.rate 48000 }
2. The OAuth Token Expiration
Exact Error String: google.auth.exceptions.RefreshError: ('invalid_grant', 'Token has been expired or revoked.')
- Cause: Google Cloud SDK tokens expire or get revoked if you change your Google account password or if the OAuth consent screen is set to "Testing" mode (which limits tokens to 7 days).
- Fix: Delete the cached token:
rm ~/.config/google-oauthlib-tool/credentials.json. Go to your Google Cloud Console, ensure your app is published (not in Testing mode), and re-run thegoogle-oauthlib-toolcommand.
3. The Headless GUI Crash
Exact Error String: qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found.
- Cause: You flashed the "Lite" (headless) version of Raspberry Pi OS, or you are SSH'd in without X11 forwarding. PyQt6 requires a display server.
- Fix: You must run this script directly on the Pi's desktop environment via the connected Waveshare screen. If you must launch it via SSH for debugging, use
export DISPLAY=:0before running the Python script.
Extending and Simplifying the Build
Once the base raspberry pi google assistant with screen is stable, you have two clear paths depending on your end goal.
To Simplify (Headless Kiosk):
If you don't actually need the screen and just want a voice-activated smart speaker, strip out the PyQt6 dependencies. Replace the UI signals with simple print() statements, remove the QApplication loop, and run the script as a systemd service. This cuts RAM usage from ~450MB down to ~120MB, allowing you to downgrade to a Raspberry Pi Zero 2 W.
To Extend (Home Automation Relays):
To make the assistant control physical 120V/240V loads (like a workbench lamp or soldering fume extractor), add a 4-channel optocoupler relay board.
Map GPIO 22, 23, 24, and 25 to the relay IN pins. In the process_event function, parse event.args['text'] for custom keywords (e.g., "turn on the fume extractor") and trigger the GPIO HIGH/LOW states accordingly. This keeps the logic local and bypasses the latency of cloud-based smart home routines.






