Connecting a barcode scanner to a Raspberry Pi usually forces a choice between USB HID (plug-and-play keyboard emulation) and UART/TTL (raw serial data). For headless kiosks, automated inventory rigs, or low-latency embedded systems, USB HID is a headache because it requires an active X11/Wayland session or complex evdev hooking to capture keystrokes. UART is the professional choice: it delivers raw byte streams directly to your Python script, independent of the OS display server.

This guide walks through wiring a GM65-S UART 2D scan engine to a Raspberry Pi 4 Model B, capturing the data via Python, and debugging the inevitable serial port permission errors that trip up most builders on modern Raspberry Pi OS.

Project Spec Sheet & Parts List

This build targets the Raspberry Pi 4 Model B (4GB or 8GB) running Raspberry Pi OS (Bookworm or later, 64-bit). The Pi 5 routes UART differently due to the RP1 southbridge chip; we address Pi 5 compatibility in the FAQ.

Component Exact Model / Variant Est. Price (2026) Notes
Microcontroller Raspberry Pi 4 Model B (4GB) $55.00 Target board for this code and pinout.
Scanner Module GM65-S UART 2D Scan Engine $28.00 OEM module. Supports 1D/2D, 3.3V logic.
Wiring Female-to-Female Jumper Wires (20cm) $4.00 Need 4 wires: VCC, GND, TX, RX.
Power Supply Official Pi 27W USB-C PD Supply $12.00 Required to prevent brownouts under scan load.
SD Card SanDisk Extreme 32GB A2 V30 $9.00 Minimum recommended for Pi OS Bookworm.

Hardware Wiring & Pin Mapping

The GM65-S operates natively at 3.3V logic, which perfectly matches the Raspberry Pi GPIO. Warning: If you substitute this with a generic 5V Arduino-targeted scanner module, you must use a logic level converter on the Pi's RX pin (GPIO 15), or you will permanently damage the Pi's SoC.

⚡ Safety & Power Callout: The GM65-S draws roughly 100mA during active scanning. While the Pi's 3.3V rail can technically supply this, it is safer to power the scanner from the Pi's 5V pin (Pin 2) if your specific module has an onboard 3.3V voltage regulator. The wiring table below assumes a direct 3.3V module connection.
Scanner Pin Raspberry Pi 4 Pin (BCM) Physical Pin # Function
VCC 3.3V Power Pin 1 Power (3.3V)
GND GND Pin 6 Common Ground
TXD GPIO 15 (RXD) Pin 10 Scanner sends data to Pi
RXD GPIO 14 (TXD) Pin 8 Pi sends commands to Scanner

Remember the golden rule of UART: TX always connects to RX, and RX always connects to TX. Never connect TX to TX.

Python UART Scanner Code

We use the pyserial library to read the hardware UART port. On the Pi 4, the primary hardware UART is mapped to /dev/serial0. Install the library via your virtual environment or system package manager: pip install pyserial.

import serial
import time
import sys

# ---------------------------------------------------------
# PIN DEFINITIONS (Raspberry Pi 4 Model B)
# Pi GPIO 14 (TXD / Physical Pin 8)  -> Scanner RXD
# Pi GPIO 15 (RXD / Physical Pin 10) -> Scanner TXD
# Pi 3.3V (Physical Pin 1)           -> Scanner VCC
# Pi GND (Physical Pin 6)            -> Scanner GND
# ---------------------------------------------------------

# The primary hardware UART on Pi 4 (when Bluetooth is disabled)
UART_PORT = '/dev/serial0'
BAUD_RATE = 9600  # GM65-S default baud rate
TIMEOUT_SEC = 1.0

def init_scanner():
    """Initialize serial connection with error handling."""
    try:
        ser = serial.Serial(
            port=UART_PORT,
            baudrate=BAUD_RATE,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            bytesize=serial.EIGHTBITS,
            timeout=TIMEOUT_SEC
        )
        print(f"[INFO] Successfully opened {UART_PORT} at {BAUD_RATE} baud.")
        return ser
    except serial.SerialException as e:
        print(f"[FATAL] Could not open serial port: {e}")
        sys.exit(1)

def read_barcodes(ser):
    """Continuously read and decode barcode data."""
    print("[INFO] Waiting for barcode scans... (Press Ctrl+C to stop)")
    try:
        while True:
            # Read until a newline character is found
            raw_data = ser.readline()
            
            if raw_data:
                try:
                    # Decode bytes to string and strip trailing CR/LF
                    barcode = raw_data.decode('utf-8').strip()
                    if barcode:
                        timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
                        print(f"[{timestamp}] SCANNED: {barcode}")
                        
                        # TODO: Insert database lookup or MQTT publish here
                        
                except UnicodeDecodeError:
                    print("[WARN] Received non-UTF-8 byte sequence, ignoring.")
                    
    except KeyboardInterrupt:
        print("\n[INFO] Stopping scanner...")
    finally:
        if ser.is_open:
            ser.close()
            print("[INFO] Serial port closed.")

if __name__ == "__main__":
    scanner_port = init_scanner()
    read_barcodes(scanner_port)

Debugging: Serial Port Exceptions

If you run the script and immediately hit an error, do not guess. The pyserial library throws specific exceptions that tell you exactly what the OS is blocking. Below are the exact error strings and their ranked causes.

Error 1: Permission Denied

Exact Error String:
serial.serialutil.SerialException: [Errno 13] could not open port '/dev/serial0': [Errno 13] Permission denied: '/dev/serial0'

Cause: Your current user (usually pi or your custom username) is not in the dialout group, which governs access to serial TTY devices in Linux.

Fix: Run sudo usermod -a -G dialout $USER, then reboot or log out and back in for the group change to take effect.

Error 2: No Such File or Directory

Exact Error String:
serial.serialutil.SerialException: [Errno 2] could not open port '/dev/serial0': [Errno 2] No such file or directory: '/dev/serial0'

Cause: The hardware UART is disabled, or the Bluetooth module is hogging the primary UART (/dev/ttyAMA0), leaving /dev/serial0 unmapped.

🔍 The First 3 Things to Check When UART Fails:
  1. raspi-config settings: Run sudo raspi-config -> Interface Options -> Serial Port. Set "Would you like a login shell to be accessible over serial?" to No. Set "Would you like the serial port hardware to be enabled?" to Yes.
  2. Boot Config Overlay: In Raspberry Pi OS Bookworm, the config file moved. Open /boot/firmware/config.txt (not /boot/config.txt) and ensure these two lines are at the bottom:
    enable_uart=1
    dtoverlay=disable-bt
  3. Reboot and Verify: After rebooting, run ls -l /dev/serial0. It should show a symlink pointing to ttyAMA0. If it points to ttyS0 (the mini-UART), your core frequency will fluctuate and corrupt barcode data.

For deeper configuration details, always refer to the official Raspberry Pi configuration documentation.

Extending or Simplifying the Build

Not every project requires bare-metal UART. Before you finalize your kiosk design, evaluate whether you should simplify or extend this architecture.

How to Simplify: The USB HID Route

If you are building a desktop inventory app with a monitor attached, ditch the UART wiring. Buy a standard $15 USB handheld barcode scanner (like the NADAMOO or Inateck USB models). Plug it into the Pi's USB port. It will act as a standard keyboard. You can read it in Python using the keyboard or pynput libraries, or simply let it type directly into a focused web browser input field. Zero OS configuration required.

How to Extend: MQTT and I2C Feedback

For a standalone warehouse scanner, you need visual feedback and network integration.

  • Add I2C OLED: Wire a 0.96" SSD1306 OLED display to the Pi's I2C bus (GPIO 2/3). Update the Python script to print the last scanned barcode to the screen using the adafruit-circuitpython-ssd1306 library.
  • Push to MQTT: Instead of just printing to the console, use the paho-mqtt library to publish the scanned string to an MQTT broker (e.g., Mosquitto) on topic warehouse/scanner/01. This allows a central Node-RED or Python backend to process inventory updates in real-time.

Frequently Asked Questions

Can I use a USB barcode scanner with Raspberry Pi without writing code?

Yes. A USB barcode scanner emulates a standard HID keyboard. If you have a web browser open on the Pi with a text field focused, scanning a barcode will simply "type" the numbers and hit "Enter". No Python or backend code is required for basic data entry. However, this fails on headless (no-monitor) Pi setups, which is why UART is preferred for embedded deployments.

Why is my Raspberry Pi barcode scanner reading garbage characters?

Garbage characters (e.g., ÿÿÿ or random symbols) almost always indicate a baud rate mismatch. The GM65-S defaults to 9600 baud, but if it was previously configured via setup barcodes to 115200, your Python script will misinterpret the timing. Check the scanner's datasheet and scan the "Restore Factory Defaults" barcode, then verify your BAUD_RATE variable in the Python script matches.

How do I trigger the GM65-S scanner to read on command instead of continuous mode?

Out of the box, many embedded modules ship in "continuous read" or "induction" mode, meaning the laser stays on and drains power. To switch to "command trigger" mode, scan the specific configuration barcode in the GM65-S user manual labeled Command Trigger Mode. Once set, you must send the hex command 0x7E 0x00 0x08 0x01 0x00 0x02 0x01 0xAB 0xCD over the serial port via ser.write() to trigger a single scan event.

Will this UART setup work on the Raspberry Pi 5?

The physical wiring (GPIO 14/15) remains the same on the Pi 5, but the underlying architecture changed due to the RP1 I/O controller. On the Pi 5, the primary UART is /dev/ttyAMA0, and Bluetooth is routed to a separate internal UART. You do not need to disable Bluetooth on the Pi 5 to use the GPIO UART. However, you must still enable the UART in /boot/firmware/config.txt using uart0=on (the Pi 5 syntax) rather than enable_uart=1. Consult the PySerial documentation to verify port mapping on newer kernel versions.