Files
RoboticaPython/modulo_esp32.py
T

245 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import serial
import math
import logging
logger = logging.getLogger(__name__)
class ChasisESP32:
"""
Interfaz UART con el ESP32.
Protocolo de TX (RPi → ESP32):
M:s1,f1,s2,f2,s3,f3,s4,f4\\n
s = signo → -1 | 0 | 1
f = fracción de VEL_MAX → 0.000 – 1.000
S:\\n → Stop inmediato
C:\\n → Iniciar autocalibración
Protocolo de RX (ESP32 → RPi):
T:t1,t2,t3,t4\\n → Odometría acumulada (ticks con signo)
K:v1,v2,v3,v4\\n → VEL_MAX_TICKS calibrada por motor (ticks/s)
D:...\\n → Mensajes de debug del ESP32
"""
# ── Geometría ──────────────────────────────────────────────────────────────
TICKS_POR_VUELTA = 20.0
RADIO_RUEDA = 0.03 # metros
ANCHO_EJE = 0.11 # metros (distancia entre ruedas del mismo eje)
# VEL_MAX_TICKS se actualiza cuando el ESP32 envía la trama K:
# Valor inicial conservador: 200 ticks/s
# M1=FL, M2=FR, M3=RL, M4=RR
_VEL_MAX_TICKS = [200.0, 200.0, 200.0, 200.0]
def __init__(self, puerto: str = '/dev/ttyAMA0', baudrate: int = 921600,
imu=None):
"""
Args:
puerto: Puerto serie del ESP32.
baudrate: Velocidad de comunicación.
imu: Instancia de modulo_imu.IMU ya iniciada, o None.
- Rotación (theta): 100% del IMU si está disponible.
- Traslación (x, y): 100% de los ticks de encoders siempre.
"""
self.METROS_POR_TICK = (2.0 * math.pi * self.RADIO_RUEDA) / self.TICKS_POR_VUELTA
try:
self.puerto = serial.Serial(puerto, baudrate, timeout=0.05)
print(f"✅ Conexión UART activa en {puerto}")
except Exception as e:
print(f"❌ Error UART: {e}")
self.puerto = None
# Referencia al IMU (puede ser None → se usa odometría de ruedas para theta)
self._imu = imu
self.x, self.y, self.theta = 0.0, 0.0, 0.0
self.ticks_anteriores = [0, 0, 0, 0]
self.primera_lectura = True
# Velocidad lineal estimada en m/s (calculada con Δt real entre tramas)
self._velocidad_ms = 0.0
self._t_anterior = None # tiempo (perf_counter) de la última trama T:
# ── Velocidad máxima (promedio por lado) ───────────────────────────────────
@property
def vel_max_izq(self) -> float:
"""Ticks/s máximos del lado izquierdo (promedio M1 y M3)."""
return (self._VEL_MAX_TICKS[0] + self._VEL_MAX_TICKS[2]) / 2.0
@property
def vel_max_der(self) -> float:
"""Ticks/s máximos del lado derecho (promedio M2 y M4)."""
return (self._VEL_MAX_TICKS[1] + self._VEL_MAX_TICKS[3]) / 2.0
# ── Lectura de odometría y tramas de control ───────────────────────────────
def leer_odometria(self) -> bool:
"""
Lee una línea del ESP32 y la procesa.
Retorna True si se actualizó la odometría.
"""
if not (self.puerto and self.puerto.in_waiting > 0):
return False
try:
linea = self.puerto.readline().decode('utf-8', errors='ignore').strip()
except Exception:
return False
# ── T: odometría ──────────────────────────────────────────────────────
if linea.startswith("T:"):
partes = linea[2:].split(',')
if len(partes) != 4:
return False
try:
ticks_actuales = [int(p) for p in partes]
except ValueError:
return False
import time as _time
if self.primera_lectura:
self.ticks_anteriores = ticks_actuales
self.primera_lectura = False
self._t_anterior = _time.perf_counter()
return True
ahora = _time.perf_counter()
dt = ahora - self._t_anterior
self._t_anterior = ahora
delta = [ticks_actuales[i] - self.ticks_anteriores[i] for i in range(4)]
self.ticks_anteriores = ticks_actuales
# ── Traslación: 100% encoders ─────────────────────────────────────
# Cada lado promedia sus dos motores (delantero + trasero)
# M1(FL) y M3(RL) → izquierda | M2(FR) y M4(RR) → derecha
d_izq = ((delta[0] + delta[2]) / 2.0) * self.METROS_POR_TICK
d_der = ((delta[1] + delta[3]) / 2.0) * self.METROS_POR_TICK
d_centro = (d_izq + d_der) / 2.0
# Velocidad lineal (m/s) con Δt real entre tramas
if dt > 0:
self._velocidad_ms = d_centro / dt
# ── Rotación: 100% IMU (o diferencial de encoders si no hay IMU) ──
if self._imu is not None:
theta_nuevo = self._imu.theta_rad
else:
d_theta = (d_der - d_izq) / self.ANCHO_EJE
theta_nuevo = self.theta + d_theta
# ── Posición: integrar d_centro en la dirección del IMU ───────────
# Se usa el promedio de theta anterior y nuevo para mayor precisión
theta_medio = (self.theta + theta_nuevo) / 2.0
self.x += d_centro * math.cos(theta_medio)
self.y += d_centro * math.sin(theta_medio)
self.theta = theta_nuevo
return True
# ── K: VEL_MAX calibrada por el ESP32 ─────────────────────────────────
elif linea.startswith("K:"):
partes = linea[2:].split(',')
if len(partes) == 4:
try:
nuevos = [float(p) for p in partes]
# Validar que los valores sean razonables (10–2000 ticks/s)
if all(10.0 < v < 2000.0 for v in nuevos):
self._VEL_MAX_TICKS = nuevos
print(
f"\n✅ VEL_MAX actualizada → "
f"M1={nuevos[0]:.1f} M2={nuevos[1]:.1f} "
f"M3={nuevos[2]:.1f} M4={nuevos[3]:.1f} ticks/s"
)
else:
print(f"\n⚠️ VEL_MAX recibida fuera de rango: {nuevos} — ignorada")
except ValueError:
pass
# ── D: debug del ESP32 ────────────────────────────────────────────────
elif linea.startswith("D:"):
print(f"\r[ESP32] {linea} ", end="")
return False
# ── Propiedades de diagnóstico ─────────────────────────────────────────────
@property
def velocidad_ms(self) -> float:
"""Velocidad lineal actual en m/s (calculada desde los ticks de encoders)."""
return self._velocidad_ms
@property
def theta_imu_deg(self) -> float:
"""Ángulo del IMU en grados (nan si no hay IMU conectado)."""
if self._imu is None:
return float('nan')
return math.degrees(self._imu.theta_rad)
# ── Envío de velocidad diferencial ─────────────────────────────────────────
def enviar_velocidad(self, v: float, w: float) -> None:
"""
Convierte velocidad lineal (m/s) y angular (rad/s) al protocolo M:.
Cinemática diferencial:
v_izq = v - w * (ANCHO_EJE / 2)
v_der = v + w * (ANCHO_EJE / 2)
Cada rueda se envía con:
s = signo (-1, 0, 1)
f = |vel_rueda_ticks| / VEL_MAX_TICKS del motor (0.0 – 1.0)
Trama resultante:
M:s1,f1,s2,f2,s3,f3,s4,f4\\n
"""
if not self.puerto:
return
# Velocidades en m/s para cada lado
v_izq_ms = v - w * (self.ANCHO_EJE / 2.0)
v_der_ms = v + w * (self.ANCHO_EJE / 2.0)
# Convertir a ticks/s
v_izq_ticks = v_izq_ms / self.METROS_POR_TICK
v_der_ticks = v_der_ms / self.METROS_POR_TICK
# Velocidades por motor (izq = M1, M3 / der = M2, M4)
vel_motores = [v_izq_ticks, v_der_ticks, v_izq_ticks, v_der_ticks]
# Umbral mínimo de movimiento: 0.5 ticks/s ≈ movimiento nulo
UMBRAL = 0.5
partes = []
todos_cero = True
for i, vel in enumerate(vel_motores):
if abs(vel) < UMBRAL:
s, f = 0, 0.0
else:
s = 1 if vel > 0 else -1
# Normalizar respecto a la VEL_MAX de cada motor específico
f = min(abs(vel) / self._VEL_MAX_TICKS[i], 1.0)
todos_cero = False
partes.append(f"{s},{f:.3f}")
if todos_cero:
cmd = "S:\n"
else:
cmd = "M:" + ",".join(partes) + "\n"
print(f"\r[UART TX] → {cmd.strip():<55}", end="", flush=True)
try:
self.puerto.write(cmd.encode('utf-8'))
except Exception as e:
print(f"\n❌ Error al escribir UART: {e}")
# ── Calibración ───────────────────────────────────────────────────────────
def calibrar_pid(self) -> None:
"""Solicita autocalibración al ESP32. La respuesta K: actualizará VEL_MAX."""
if self.puerto:
try:
self.puerto.write(b"C:\n")
print("\n⚙️ Solicitud de calibración enviada al ESP32.")
except Exception as e:
print(f"\n❌ Error al enviar calibración: {e}")