Skip to main content

Serial Communication Protocol

Serial communication is the backbone of communication between the Raspberry Pi (vision processing) and VEX Brain (robot control). In this lesson, you’ll learn how to establish reliable bidirectional communication.

Learning Objectives

By the end of this lesson, you will be able to:
  • Configure serial ports on both Raspberry Pi and VEX Brain
  • Implement basic read and write operations
  • Handle connection errors and timeouts
  • Design a message framing protocol
  • Debug serial communication issues
All code examples in this lesson are from the actual course implementation in course/comm_class/raspberry_comm/

Serial Communication Basics

Hardware Setup

Raspberry Pi Side:
  • USB Serial Adapter (Windows: COM7, Linux: /dev/ttyUSB0)
  • Or direct GPIO UART pins: /dev/ttyAMA0
VEX Brain Side:
  • Built-in serial port: /dev/serial1
  • Accessible from VEX Python API
Wiring:
Raspberry Pi TX  →  VEX Brain RX
Raspberry Pi RX  ←  VEX Brain TX
GND             ↔   GND
Critical: Connect TX to RX and RX to TX (crossover). Also ensure both devices share a common ground (GND).

Communication Parameters

Both devices must agree on:
  • Baud Rate: 115200 bits/second (fast enough for real-time control)
  • Data Bits: 8
  • Parity: None
  • Stop Bits: 1
  • Flow Control: None

Basic Serial Operations

Writing Data (Transmit)

Let’s start with a simple example of sending data from Raspberry Pi to VEX Brain. Raspberry Pi Code (write_data.py:1-16):
import serial

class SerialCommunication:
    def __init__(self):
        # Windows COM port (use "/dev/ttyACM0" on Raspberry Pi)
        self.com = serial.Serial("COM7", 115200, write_timeout=10)

    def writing_data(self, command: str) -> None:
        self.com.write(command.encode('ascii'))
        print(f'SENDING DATA: {command}')

if __name__ == "__main__":
    serial_comm = SerialCommunication()
    serial_comm.writing_data('e')  # Send single character
Key Points:
  • serial.Serial() opens the port immediately
  • write() requires bytes, so we encode() strings
  • write_timeout=10 prevents indefinite blocking
Device Path Differences:
  • Windows: COM7, COM8, etc.
  • Linux: /dev/ttyUSB0, /dev/ttyACM0
  • Raspberry Pi: /dev/ttyAMA0 (GPIO pins) or /dev/ttyACM0 (USB)
VEX Brain Code (vex_brain_comm/src/main.py:18-27):
from vex import *

brain = Brain()

def read_data():
    try:
        s = open('/dev/serial1', 'rb')  # Read binary mode
    except:
        raise Exception('serial port not available')

    while True:
        data = s.read(1)  # Read one byte at a time
        brain.screen.print_at("RX: {}".format(str(data)), x=1, y=95)
Key Points:
  • VEX uses file operations: open() instead of library
  • Binary mode ('rb') for raw byte access
  • read(1) blocks until one byte is available

Reading Data (Receive)

Raspberry Pi Code (read_data.py:1-17):
import serial

class SerialCommunication:
    def __init__(self):
        self.com = serial.Serial("COM7", 115200, write_timeout=10)

    def reading_data(self) -> None:
        while True:
            data = self.com.read()  # Read one byte
            print(f'READING DATA: {data}')

if __name__ == "__main__":
    serial_comm = SerialCommunication()
    serial_comm.reading_data()
VEX Brain Code (vex_brain_comm/src/main.py:28-37):
def write_data():
    try:
        s = open('/dev/serial1', 'wb')  # Write binary mode
    except:
        raise Exception('serial port not available')

    while True:
        s.write('A')  # Send character
        brain.screen.print_at("TX: A", x=1, y=95)
        sleep(1000)  # Wait 1 second
These basic examples read/write single bytes. In practice, you’ll send complete messages, which requires a framing protocol.

Message Framing Protocol

The Challenge

Serial communication is a continuous stream of bytes. How do you know where one message ends and the next begins? Problem Example:
Received bytes: b'HelloWorld'
Is this: "Hello" + "World" or "HelloWorld"?

Solution: Newline Delimiter

We use the newline character (\n) as a message delimiter:
Message 1: Hello\n
Message 2: World\n
Each message is terminated with \n, making boundaries clear.

Implementation: Buffered Reading

Instead of processing bytes immediately, accumulate them in a buffer until we receive a complete message. From json_data.py:18-20:
class SerialCommunication:
    def __init__(self):
        self.message_end = b'\n'  # Message delimiter
        self.buffer = bytearray()  # Accumulate bytes here
        # ... other initialization
Reading Logic (json_data.py:50-68):
def _read_loop(self):
    """Thread loop for read messages from VEX"""
    while not self._stop_event.is_set():
        if self.com and self.com.in_waiting:  # Bytes available?
            try:
                char = self.com.read()  # Read one byte
                
                if char == self.message_end:  # Complete message!
                    message = self.buffer.decode()  # Convert to string
                    self.buffer = bytearray()  # Reset buffer
                    
                    # Process the complete message
                    try:
                        data = json.loads(message)  # Parse JSON
                        self._process_message(data)
                    except json.JSONDecodeError:
                        log.error(f'error message decode: {message}')
                else:
                    self.buffer.extend(char)  # Add to buffer
                    
            except Exception as e:
                log.error(f'error read serial port: {e}')
        time.sleep(0.01)  # Small delay to avoid busy-waiting
How It Works:
  1. Check for data: com.in_waiting returns number of bytes ready
  2. Read one byte: Process character by character
  3. Check for delimiter: Is it \n?
    • Yes: Decode buffer as complete message → process it → reset buffer
    • No: Append byte to buffer → continue reading
  4. Error handling: Catch decode errors (malformed messages)
Why read one byte at a time?This ensures we detect the delimiter immediately. Reading multiple bytes at once might include parts of the next message, complicating parsing.

VEX Brain Equivalent

From vex_brain_comm/src/main.py:39-69:
def json_data():
    buffer = bytearray()
    try:
        s = open('/dev/serial1', 'rb+')  # Read/write mode
    except:
        raise Exception('serial port error')
    
    while True:
        char = s.read(1)  # Read one byte
        
        if char == b'\n':  # Complete message
            message = buffer.decode()
            buffer = bytearray()  # Reset
            
            try:
                msg = json.loads(message)  # Parse JSON
                msg_type = msg['type'].lower()
                data = msg.get('data', {})
                
                # Process message (handle specific types)
                if msg_type == 'test_service' and data.get('state') == 'successfully':
                    # ... respond with confirmation
                    pass
            except:
                raise Exception('json error')
        else:
            buffer.extend(char)  # Accumulate bytes
The pattern is identical: accumulate bytes until delimiter, then process.

Connection Management

Establishing Connection

From json_data.py:21-39:
def connect(self) -> bool:
    """serial connection"""
    if self.is_connected:
        return True  # Already connected

    try:
        self.com = serial.Serial("COM7", 115200, write_timeout=10)
        self.is_connected = True

        # Start background reading thread
        self._stop_event.clear()
        self._read_thread = Thread(target=self._read_loop)
        self._read_thread.daemon = True  # Exit when main program exits
        self._read_thread.start()
        return True

    except Exception as e:
        log.error(f'Error connecting to serial port: {str(e)}')
        return False
Key Features:
  • Check if already connected (avoid duplicate connections)
  • Use threading for non-blocking reads
  • daemon=True: Background thread won’t prevent program exit
  • Return bool to indicate success/failure
Why Threading?Reading from serial is blockingread() waits until data arrives. Without threading, your main program would freeze. A background thread monitors the serial port continuously while your main code processes vision or handles other tasks.

Graceful Shutdown

From json_data.py:83-91:
def close(self):
    """close serial connection"""
    self._stop_event.set()  # Signal thread to stop
    
    if self._read_thread:
        self._read_thread.join(timeout=1.0)  # Wait up to 1 second
        
    if self.com and self.com.is_open:
        self.com.close()
        self.is_connected = False
        log.info('Serial connection closed')
Shutdown Steps:
  1. Set stop event (thread checks _stop_event.is_set() in loop)
  2. Wait for thread to finish with join(timeout=1.0)
  3. Close serial port
  4. Update connection status

Error Handling

Common Issues

Port Not Available:
try:
    self.com = serial.Serial("COM7", 115200, write_timeout=10)
except serial.SerialException as e:
    log.error(f'Cannot open port: {e}')
    # Try alternative ports or notify user
Disconnection During Operation:
try:
    self.com.write(encoded_message)
except serial.SerialTimeoutException:
    log.error('Write timeout - device may be disconnected')
    self.is_connected = False
Malformed Messages:
try:
    data = json.loads(message)
except json.JSONDecodeError:
    log.error(f'Invalid JSON: {message}')
    # Continue reading next message
Robust error handling is critical. Don’t let a single malformed message crash your entire robot control system!

Debugging Serial Communication

Testing Tools

  1. Serial Monitor (Arduino IDE, PuTTY, screen)
    # Linux/Mac
    screen /dev/ttyUSB0 115200
    
    # Windows (PuTTY)
    # Set Serial, COM7, Speed 115200
    
  2. Loopback Test: Connect TX to RX on same device
    • Should receive everything you send
    • Confirms port works
  3. Logging: Add verbose output
    log.info(f'Sending: {message}')
    log.info(f'Received: {data}')
    

Common Problems

IssueSymptomSolution
Baud rate mismatchGarbled charactersEnsure both sides use 115200
TX/RX swappedNo data receivedSwap TX and RX connections
Missing groundIntermittent errorsConnect GND pins
Port in use”Port busy” errorClose other programs using port
Start simple! Test basic character transmission before moving to complex JSON messages. Verify hardware before debugging software.

Practice Exercise

Echo Server

Implement a simple echo protocol: Raspberry Pi:
  1. Send a message to VEX Brain
  2. Wait for echo response
  3. Verify received message matches sent message
VEX Brain:
  1. Read incoming message
  2. Send it back immediately
Success Criteria:
  • Messages transmitted without corruption
  • Round-trip time < 100ms
  • Handles 10 messages/second reliably

Extension Challenge

Add a checksum to detect transmission errors:
def calculate_checksum(message: str) -> int:
    return sum(ord(c) for c in message) % 256

message = "Hello"
checksum = calculate_checksum(message)
full_message = f"{message}:{checksum}\n"

Summary

You’ve learned:
  • ✓ Serial port configuration and basic read/write
  • ✓ Message framing using newline delimiters
  • ✓ Buffered reading for complete message extraction
  • ✓ Threaded communication for non-blocking operation
  • ✓ Connection management and error handling
  • ✓ Debugging techniques for serial issues

Next Steps

Now that you can send and receive raw messages, the next lesson covers structured communication using JSON for type-safe robot commands.

JSON Messaging

Learn to structure robot commands with JSON message formats
Course Repository: All code examples are in course/comm_class/raspberry_comm/
  • write_data.py: Basic transmit example
  • read_data.py: Basic receive example
  • json_data.py: Complete implementation with framing

Build docs developers (and LLMs) love