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.
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:
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 |
python3-serial
o 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__)"
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
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()
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.
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
)
# 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
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())
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)
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()
# 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
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 |
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()
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')
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()
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()
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'})
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.