Manual de Programación para Puerto Serie en Linux con Python 3

1. Introducción

Este manual proporciona una guía completa para trabajar con puertos serie (COM) en sistemas Linux utilizando Python 3. Cubre desde los conceptos básicos hasta técnicas avanzadas de comunicación serial.

Nota: Este manual asume que tienes conocimientos básicos de Python 3 y familiaridad con la línea de comandos de Linux.

2. Conceptos Básicos de Comunicación Serie

2.1 ¿Qué es un puerto serie?

Un puerto serie es una interfaz de comunicación que envía y recibe datos un bit a la vez. Aunque menos común en computadoras modernas, sigue siendo esencial para:

2.2 Parámetros de configuración

Parámetro Descripción Valores comunes
Baud rate Velocidad de transmisión (bits por segundo) 9600, 19200, 38400, 57600, 115200
Data bits Número de bits de datos 5, 6, 7, 8
Parity Control de paridad None, Even, Odd, Mark, Space
Stop bits Bits de parada 1, 1.5, 2
Flow control Control de flujo None, XON/XOFF, RTS/CTS

3. Configuración del Entorno

3.1 Requisitos

3.2 Instalación de PySerial

# Instalación mediante pip
pip install pyserial

# Instalación desde repositorios en distribuciones basadas en Debian/Ubuntu
sudo apt-get install python3-serial

# Verificación de la instalación
python3 -c "import serial; print(serial.__version__)"

3.3 Permisos de puerto serie

En Linux, los puertos serie generalmente aparecen como /dev/ttyS* (serial tradicional) o /dev/ttyUSB* (convertidores USB-serial).

Para dar permisos al usuario actual:

# Agregar usuario al grupo dialout (común para dispositivos seriales)
sudo usermod -a -G dialout $USER

# O dar permisos temporales (menos seguro)
sudo chmod 666 /dev/ttyUSB0

# Para que los cambios de grupo surtan efecto, cierra sesión y vuelve a entrar

4. Uso Básico de PySerial

4.1 Abrir una conexión serial

import serial

# Configuración básica del puerto
ser = serial.Serial(
    port='/dev/ttyUSB0',  # Nombre del puerto
    baudrate=9600,       # Velocidad en baudios
    parity=serial.PARITY_NONE,  # Paridad
    stopbits=serial.STOPBITS_ONE,  # Bits de parada
    bytesize=serial.EIGHTBITS,  # Bits de datos
    timeout=1            # Timeout en segundos
)

print(f"Puerto {ser.name} abierto: {ser.is_open}")

# Siempre cerrar el puerto al terminar
ser.close()

4.2 Lectura y escritura básica

import serial

ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=1)

# Enviar datos
data_to_send = "Hola dispositivo!\n"
ser.write(data_to_send.encode('utf-8'))  # Encode convierte str a bytes

# Leer datos
received_data = ser.readline()  # Lee hasta encontrar un \n
print("Recibido:", received_data.decode('utf-8'))  # Decode convierte bytes a str

ser.close()

Nota: En Python 3, las cadenas son Unicode y los datos seriales son bytes. Siempre usa .encode() al enviar y .decode() al recibir.

5. Configuración Avanzada

5.1 Parámetros adicionales

ser = serial.Serial(
    port='/dev/ttyS0',
    baudrate=115200,
    bytesize=serial.EIGHTBITS,
    parity=serial.PARITY_EVEN,
    stopbits=serial.STOPBITS_ONE,
    timeout=0.5,          # Timeout de lectura (segundos)
    write_timeout=0.5,    # Timeout de escritura
    xonxoff=False,        # Control de flujo software
    rtscts=False,         # Control de flujo hardware (RTS/CTS)
    dsrdtr=False,         # Control de flujo hardware (DSR/DTR)
    inter_byte_timeout=0.1 # Timeout entre bytes
)

5.2 Control de señales (handshaking)

# Habilitar RTS/CTS (hardware flow control)
ser = serial.Serial('/dev/ttyUSB0', 9600, rtscts=True)

# Control manual de señales
ser.rts = True  # Activar RTS
ser.dtr = False # Desactivar DTR

# Leer estado de señales
print("CTS:", ser.cts)  # Clear To Send
print("DSR:", ser.dsr)  # Data Set Ready
print("RI:", ser.ri)    # Ring Indicator
print("CD:", ser.cd)    # Carrier Detect

6. Técnicas de Comunicación

6.1 Lecturas robustas

def read_until(ser, terminator=b'\n', max_bytes=1000):
    """Lee hasta encontrar el terminador o alcanzar max_bytes"""
    data = bytearray()
    while len(data) < max_bytes:
        byte = ser.read(1)
        if not byte:
            break  # Timeout
        data.extend(byte)
        if data.endswith(terminator):
            break
    return bytes(data)

# Uso:
ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=1)
response = read_until(ser, terminator=b'END\r\n')
print("Respuesta completa:", response.decode())

6.2 Protocolo simple de solicitud-respuesta

def send_command(ser, command, response_terminator=b'\n', timeout=1):
    """Envía un comando y espera una respuesta"""
    original_timeout = ser.timeout
    ser.timeout = timeout
    
    # Enviar comando
    ser.write(command.encode() + b'\r\n')  # Añadir CR+LF
    
    # Leer respuesta
    response = bytearray()
    while True:
        byte = ser.read(1)
        if not byte:  # Timeout
            break
        response.extend(byte)
        if response.endswith(response_terminator):
            break
    
    ser.timeout = original_timeout
    return response.decode().strip()

# Ejemplo de uso
response = send_command(ser, "AT", response_terminator=b'OK\r\n')
print("Respuesta AT:", response)

6.3 Comunicación asíncrona

import threading

class SerialAsyncReader:
    def __init__(self, port, baudrate):
        self.ser = serial.Serial(port, baudrate, timeout=0.1)
        self.running = False
        self.callback = None
        self.thread = None
    
    def start(self, callback):
        """Inicia el hilo de lectura con la función callback"""
        if self.running:
            return
        
        self.callback = callback
        self.running = True
        self.thread = threading.Thread(target=self._read_loop)
        self.thread.start()
    
    def _read_loop(self):
        """Bucle de lectura en segundo plano"""
        while self.running:
            if self.ser.in_waiting:
                data = self.ser.read(self.ser.in_waiting)
                self.callback(data)
    
    def stop(self):
        """Detiene el hilo de lectura"""
        self.running = False
        if self.thread:
            self.thread.join()
        self.ser.close()

# Ejemplo de uso
def data_received(data):
    print("Datos recibidos:", data.decode('utf-8', errors='replace'))

reader = SerialAsyncReader('/dev/ttyUSB0', 9600)
reader.start(data_received)

# Para detener:
# reader.stop()

7. Depuración y Problemas Comunes

7.1 Herramientas de diagnóstico

# Ver dispositivos seriales disponibles
ls /dev/tty*

# Ver información de dispositivos USB
lsusb

# Ver mensajes del kernel sobre el dispositivo
dmesg | grep tty

# Conectarse al puerto serie con screen (salir con Ctrl+A \)
screen /dev/ttyUSB0 9600

7.2 Problemas comunes y soluciones

Problema Solución
PermissionError al abrir el puerto Verificar permisos del usuario (sección 3.3)
Datos corruptos o incompletos Verificar baudrate, paridad y bits de parada
Timeout en lecturas Ajustar timeout o implementar lógica de lectura específica
El programa se bloquea Usar timeouts o implementar lectura asíncrona
Dispositivo no aparece en /dev Verificar conexión física y drivers

8. Ejemplos Completos

8.1 Comunicación con Arduino

import serial
import time

def arduino_communication():
    try:
        ser = serial.Serial('/dev/ttyACM0', 9600, timeout=1)
        time.sleep(2)  # Esperar a que Arduino se reinicie
        
        while True:
            # Enviar comando
            command = input("Ingrese comando (ON/OFF/EXIT): ").upper()
            if command == "EXIT":
                break
            
            ser.write(f"{command}\n".encode())
            
            # Leer respuesta
            response = ser.readline().decode().strip()
            print("Respuesta Arduino:", response)
            
    except serial.SerialException as e:
        print(f"Error serial: {e}")
    finally:
        if 'ser' in locals() and ser.is_open:
            ser.close()
        print("Conexión cerrada")

if __name__ == "__main__":
    arduino_communication()

8.2 Logger de datos seriales a archivo

import serial
from datetime import datetime

def serial_logger(port, baudrate, logfile):
    try:
        ser = serial.Serial(port, baudrate, timeout=1)
        with open(logfile, 'a') as f:
            print(f"Iniciando logger en {port}... Presione Ctrl+C para detener")
            while True:
                if ser.in_waiting:
                    line = ser.readline().decode('utf-8', errors='replace').strip()
                    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                    log_entry = f"{timestamp} - {line}\n"
                    print(log_entry, end='')
                    f.write(log_entry)
                    f.flush()  # Asegurar que se escribe en disco
    except KeyboardInterrupt:
        print("\nLogger detenido por usuario")
    except Exception as e:
        print(f"Error: {e}")
    finally:
        if 'ser' in locals() and ser.is_open:
            ser.close()

if __name__ == "__main__":
    serial_logger('/dev/ttyUSB0', 115200, 'serial_log.txt')

8.3 Terminal serial interactivo

import serial
import threading

class SerialTerminal:
    def __init__(self, port, baudrate):
        self.ser = serial.Serial(port, baudrate, timeout=0.1)
        self.running = False
    
    def start(self):
        self.running = True
        # Hilo para leer datos del puerto serial
        read_thread = threading.Thread(target=self._read_loop)
        read_thread.daemon = True
        read_thread.start()
        
        # Bucle principal para entrada de usuario
        try:
            while self.running:
                user_input = input()
                if user_input.lower() == 'exit':
                    break
                self.ser.write((user_input + '\n').encode())
        except KeyboardInterrupt:
            pass
        finally:
            self.running = False
            self.ser.close()
    
    def _read_loop(self):
        while self.running:
            if self.ser.in_waiting:
                data = self.ser.read(self.ser.in_waiting)
                print(data.decode('utf-8', errors='replace'), end='', flush=True)

if __name__ == "__main__":
    print("Terminal Serial Interactivo")
    port = input("Ingrese puerto (ej. /dev/ttyUSB0): ")
    baudrate = int(input("Ingrese baudrate (ej. 9600): "))
    
    terminal = SerialTerminal(port, baudrate)
    terminal.start()

9. Mejores Prácticas

9.1 Manejo robusto de errores

import serial
from serial import SerialException

def safe_serial_operation():
    ser = None
    try:
        ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=1)
        
        # Operaciones con el puerto serie...
        ser.write(b'PING\n')
        response = ser.readline()
        print("Respuesta:", response.decode())
        
    except SerialException as e:
        print(f"Error de comunicación serial: {e}")
    except UnicodeDecodeError as e:
        print(f"Error decodificando datos: {e}")
    except Exception as e:
        print(f"Error inesperado: {e}")
    finally:
        if ser and ser.is_open:
            ser.close()

9.2 Configuración portable

import serial
import json

def load_serial_config(config_file):
    """Carga configuración serial desde archivo JSON"""
    with open(config_file) as f:
        config = json.load(f)
    
    # Valores por defecto
    defaults = {
        'baudrate': 9600,
        'bytesize': 8,
        'parity': 'none',
        'stopbits': 1,
        'timeout': 1,
        'xonxoff': False,
        'rtscts': False
    }
    
    # Aplicar configuración
    for key in defaults:
        if key not in config:
            config[key] = defaults[key]
    
    # Mapear valores de texto a constantes de PySerial
    parity_map = {
        'none': serial.PARITY_NONE,
        'even': serial.PARITY_EVEN,
        'odd': serial.PARITY_ODD,
        'mark': serial.PARITY_MARK,
        'space': serial.PARITY_SPACE
    }
    
    config['parity'] = parity_map.get(config['parity'].lower(), serial.PARITY_NONE)
    config['bytesize'] = int(config['bytesize'])
    
    return config

# Uso:
config = load_serial_config('serial_config.json')
ser = serial.Serial(config['port'], **{k: v for k, v in config.items() if k != 'port'})

10. Recursos Adicionales

10.1 Documentación oficial

10.2 Bibliotecas relacionadas

Nota final: La comunicación serial es fundamental en muchos proyectos de hardware. Este manual cubre los conceptos esenciales, pero cada dispositivo puede requerir ajustes específicos en los parámetros de comunicación o protocolos.