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.

Difficulty Rating: Intermediate (Requires Linux CLI, basic soldering, and Google Cloud Console setup).
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.

  1. 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).
  2. Configure Headless/SSH: In the Imager settings gear, enable SSH (password auth), set your WiFi, and set a username (e.g., pi).
  3. 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.
  4. Force USB Audio: Boot the Pi, open a terminal, and edit the ALSA config: sudo nano /usr/share/alsa/alsa.conf. Change defaults.ctl.card 0 and defaults.pcm.card 0 to 1 (assuming the USB DAC registers as card 1. Verify with aplay -l).
  5. 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
  6. Google Cloud Auth: Follow the official Google Assistant SDK Python guide to generate your credentials.json from the Google Cloud Console, then run google-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()
Callout Tip: The 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 ~/.asoundrc file 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 the google-oauthlib-tool command.

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=:0 before 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.

Safety Warning: Never wire GPIO pins directly to mains voltage. Use an opto-isolated relay board rated for your specific AC load. Mains wiring must comply with local electrical codes; if you are unsure about junction box requirements or grounding, consult a licensed electrician. Defeating ground pins or bypassing fuses to fit a relay into a standard wall box is a severe fire and shock hazard.

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.