Crea tu servidor de Inteligencia Artificial con una computadora antigua

Por: Julio César Sandria Reynoso
Maestro en Inteligencia Artificial
6 de octubre de 2026

Guía Técnica para la configuración y puesta en marcha de un servidor de Inteligencia Artificial con Ubuntu Server, Ollama y NVIDIA en una computadora antigua.

Este documento detalla el proceso completo de configuración, instalación, optimización y prueba de un servidor de Inteligencia Artificial local desplegado sobre Ubuntu Server 24.04.5 LTS, aprovechando hardware dedicado e integración mediante API remota y scripts en Python.

1. Especificaciones del Hardware del Servidor

La computadora usada como servidor de inteligencia artificial en el Instituto de Robótica de Xalapa cuenta con el siguiente perfil técnico optimizado para cargas de trabajo locales:

ComponenteDetalle TécnicoEstado / Nota de rendimiento
Procesador (CPU)Intel(R) Pentium(R) CPU G4400 @ 3.30GHz (2 núcleos / 2 hilos, Skylake)Sin soporte para instrucciones AVX/AVX2. Operación orientada a gestión y soporte de RAM.
Memoria RAM32 GB DDR4 Synchronous 2667 MHz (2 módulos de 16 GB)Excelente capacidad para almacenar modelos de lenguaje de tamaño intermedio.
Tarjeta Gráfica (GPU)NVIDIA GeForce GTX 750 Ti (Arquitectura Maxwell, GM107, 2 GB VRAM)Controlador propietario instalado con éxito. Soporte funcional para operaciones base de CUDA.
AlmacenamientoSSD ADATA SP580 (120 GB) + Disco mecánico adicional de 500 GBEspacio suficiente para sistema operativo, dependencias y almacenamiento de modelos LLM.
RedRealtek PCI Express Gigabit EthernetConectividad estable para servicio local de IA y peticiones en red.

Cabe mencionar que el equipo usado fue ampliado con memoria RAM y la reinstalación de la tarjeta gráfica que tuvo años atrás y que no se usó al preparar el artículo precedente: Instalación de una IA Generativa con Ubuntu y Ollama.

2. Configuración del Entorno Gráfico y Controladores NVIDIA

Para habilitar el soporte de hardware en la tarjeta gráfica NVIDIA GTX 750 Ti, se superaron incidencias iniciales de comunicación con el kernel mediante una purga limpia e instalación del controlador estable.

Resultado Exitoso: Al ejecutar el comando nvidia-smi, el sistema reportó correctamente la versión de controlador 580.178.04 y CUDA 13.0 vinculada a la GeForce GTX 750 Ti.

# Purgar instalaciones previas conflictivas
sudo apt purge nvidia* -y
sudo apt autoremove -y

# Instalar el controlador propietario estable
sudo apt install nvidia-driver-535 -y
sudo reboot

3. Instalación y Configuración del Servicio Ollama

Ollama fue instalado y configurado como un demonio del sistema mediante systemd para garantizar su ejecución automática en segundo plano y su disponibilidad ante reinicios.

# Descarga e instalación automatizada de Ollama
curl -fsSL https://ollama.com/install.sh | sh

# Creación del archivo de servicio systemd (/etc/systemd/system/ollama.service)
[Unit]
Description=Ollama Service
After=network-online.target

[Service]
ExecStart=/usr/local/bin/ollama serve
User=ollama
Group=ollama
Restart=always
RestartSec=3
Environment="PATH=$PATH"

[Install]
WantedBy=default.target

4. Exposición de la API en la Red Local

Para permitir que otros equipos de la red local se comuniquen con el servidor de IA, se configuró la variable de entorno OLLAMA_HOST modificando el servicio systemd:

sudo systemctl edit ollama.service

# Contenido de la anulación (override.conf):
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"

# Recargar y reiniciar el servicio
sudo systemctl daemon-reload
sudo systemctl restart ollama

5. Desarrollo de Clientes y Ejemplos de Programación en Python

Se configuró un entorno de desarrollo en la computadora cliente (macOS) utilizando entornos virtuales (venv) y la librería oficial ollama para consumir el modelo qwen2.5:1.5b desplegado en el servidor.

Programa 1: Respuesta en Streaming en Tiempo Real

import ollama

client = ollama.Client(host='http://192.168.1.50:11434')

stream = client.chat(
    model='qwen2.5:1.5b',
    messages=[
        {
            'role': 'user',
            'content': 'Escribe una historia corta sobre un robot curioso.',
        },
    ],
    stream=True,
)

print('IA: ', end='', flush=True)
for chunk in stream:
    print(chunk['message']['content'], end='', flush=True)

print()

Programa 2: Uso de Prompt de Sistema (Roles)

import ollama

client = ollama.Client(host='http://192.168.1.50:11434')

stream = client.chat(
    model='qwen2.5:1.5b',
    messages=[
        {
            'role': 'system',
            'content': 'Eres un profesor experto en robótica industrial. Responde siempre de forma técnica, concisa y orientada a la enseñanza.',
        },
        {
            'role': 'user',
            'content': 'Explícame brevemente qué es la cinemática directa.',
        },
    ],
    stream=True,
)

print('IA: ', end='', flush=True)
for chunk in stream:
    print(chunk['message']['content'], end='', flush=True)

print()

Programa 3: Chat Continuo con Historial y Streaming

import ollama

client = ollama.Client(host='http://192.168.1.50:11434')

mensajes = [
    {
        'role': 'system',
        'content': 'Eres un profesor experto en robótica industrial y optimización. Responde de forma técnica y concisa.'
    }
]

print("Iniciando chat continuo con Ollama. Escribe 'salir' para terminar.\n")

while True:
    texto_usuario = input("Tú: ")
    if texto_usuario.lower() in ['salir', 'exit', 'quit']:
        break

    mensajes.append({'role': 'user', 'content': texto_usuario})

    stream = client.chat(
        model='qwen2.5:1.5b',
        messages=mensajes,
        stream=True
    )

    print("IA: ", end='', flush=True)
    respuesta_completa = ""

    for chunk in stream:
        contenido = chunk['message']['content']
        print(contenido, end='', flush=True)
        respuesta_completa += contenido

    print("\n")
    mensajes.append({'role': 'assistant', 'content': respuesta_completa})

6. Monitoreo Avanzado y Análisis Comparativo del Sistema con btop

Para garantizar la estabilidad y comprobar la correcta distribución de la carga de trabajo entre la CPU, la RAM y la tarjeta gráfica, se utilizó la herramienta btop. A continuación se detalla el comportamiento del servidor bajo dos escenarios distintos de operación.

Instalación de btop en Ubuntu Server

# Actualizar repositorios e instalar btop
sudo apt update
sudo apt install btop -y

# Ejecutar el monitor
btop

Análisis Escenario A: Servidor en Reposo o con Peticiones Ligeras

Durante la operación normal o al recibir peticiones estándar de chat, el modelo qwen2.5:1.5b permanece residente en la memoria RAM (ocupando aproximadamente 1.3 GiB de forma estable en el proceso llama-server). El uso de la CPU se mantiene bajo y la tarjeta gráfica registra un uso mínimo.

Screenshot

[Imagen 1: Captura de btop mostrando el servidor en estado de reposo / baja carga con llama-server usando ~1.3 GiB de RAM y CPU en reposo]

Análisis Escenario B: Servidor bajo Carga Masiva (Procesamiento de Textos Extensos)

Para comprobar la respuesta del servidor bajo una carga pesada, se ejecutó un script en el equipo cliente que envía un bloque de texto masivo para calcular la matriz de atención y el contexto (*KV Cache*).

Programa de Prueba con Bloque de Texto Extenso

import ollama

client = ollama.Client(host='http://192.168.1.50:11434')

# Creamos un bloque de texto grande para forzar el uso de memoria de contexto
bloque_texto = (
    "La robótica móvil autónoma y los sistemas de optimización combinatoria "
    "requieren algoritmos eficientes de navegación y búsqueda de rutas como A* "
    "y Dijkstra, integrando mapeo SLAM y sensores LiDAR en entornos dinámicos. "
) * 150

print("Enviando documento extenso al servidor de IA...")

stream = client.chat(
    model='qwen2.5:1.5b',
    messages=[
        {
            'role': 'system',
            'content': 'Eres un asistente técnico analítico. Resume y extrae los conceptos principales del texto proporcionado.'
        },
        {
            'role': 'user',
            'content': f'Por favor, analiza a detalle el siguiente documento:\n\n{bloque_texto}'
        }
    ],
    stream=True
)

print("IA: ", end='', flush=True)
for chunk in stream:
    print(chunk['message']['content'], end='', flush=True)

print("\n\nProceso finalizado.")

Al ejecutar este script, el comportamiento de los recursos en btop cambia drásticamente:

  • Uso del CPU al 100% (C0 y C1): Los dos núcleos del Intel Pentium G4400 se saturan por completo realizando los cálculos secuenciales del modelo.
  • Proceso Activo (llama-server): El consumo de CPU del proceso asciende al 99.0%, absorbiendo toda la capacidad de procesamiento disponible.
  • Actividad Gráfica (GPU): El uso de la GPU se incrementa (alrededor del 38%), mientras que la ocupación de la VRAM se mantiene en 1.1 GiB / 2.0 GiB, apoyando parcialmente en el procesamiento.
  • Temperatura: La temperatura de la CPU experimenta un incremento controlado (por ejemplo, a 47 °C), manteniéndose en un rango totalmente seguro.
Screenshot

[Imagen 2: Captura de btop mostrando el servidor bajo carga masiva con CPU al 100%, proceso llama-server al 99% de CPU y GPU activa]

7. Preguntas Frecuentes y Análisis de Arquitectura (Hardware y Rendimiento)

Pregunta: ¿Valió la pena ampliarle la RAM a la computadora de 8GB a 32GB si en reposo modelos ligeros como qwen2.5:1.5bapenas consumen 1.3 GiB? ¿Y qué hay de la tarjeta gráfica GTX 750 Ti que se consideraba tecnología antigua?

Respuesta Técnica: Sí, ampliar la RAM a 32 GB y conservar la GTX 750 Ti fue una excelente decisión de arquitectura por los siguientes motivos:

  • Escalabilidad de Modelos: Aunque un modelo de 1.5B parámetros es ligero, modelos de mayor tamaño (como 7B u 8B) requieren entre 4.5 GB y 5.5 GB solo para cargar sus pesos. Con 8 GB totales, el sistema operativo (Ubuntu Server) se quedaría sin margen y generaría errores críticos por falta de memoria o paginación en disco (*Swap*). Con 32 GB hay total holgura.
  • Gestión del KV Cache y Contextos Largos: Al procesar documentos masivos, código fuente extenso o historiales de chat kilométricos, la memoria temporal para la matriz de atención (*KV Cache*) crece de manera importante, la cual es absorbida sin problemas por la RAM.
  • El Rol Real de la GTX 750 Ti: Aunque sus 2 GB de VRAM limitan la capacidad de alojar modelos completos (lo que obliga a delegar el cálculo pesado a la CPU al 100%), la tarjeta gráfica no está de adorno. Funciona como un coprocesador acelerador (registrando picos de uso del 38% bajo carga) para resolver operaciones matriciales específicas, además de gestionar la salida gráfica de forma independiente sin consumir la valiosa RAM del sistema.

8. Estrategia de Acceso Multiusuario y Herramientas Cliente en Red Local

Pregunta: Ya tenemos este servidor en un entorno escolar y daremos acceso a entre 2 y 5 colaboradores máximo. ¿Qué herramientas podemos instalar en las computadoras para que accedan al servidor y utilicen la IA configurada para analizar archivos y correr comandos de forma automatizada?

Respuesta Técnica: Para un grupo reducido de 2 a 5 usuarios concurrentes conectados al servidor Ollama (http://192.168.1.50:11434), existen excelentes alternativas dependiendo del flujo de trabajo:

  • Open WebUI (Interfaz tipo ChatGPT centralizada): Se despliega fácilmente mediante Docker en el servidor Ubuntu. Permite crear cuentas individuales para cada colaborador con historiales privados, soporte para carga y análisis de archivos (PDFs, documentos, código) mediante RAG, y acceso directo desde el navegador web de cada computadora (ej. http://192.168.1.50:3000) sin requerir hardware pesado local.
  • AnythingLLM (Especializado en bases documentales): Excelente para indexar directorios completos de archivos institucionales o académicos y consultar información de forma privada y estructurada.
  • Continue.dev (Para automatización de código y comandos): Extensión de código abierto para editores como VS Code que permite a los colaboradores modificar código, explicar scripts y generar comandos de terminal directamente desde su entorno de desarrollo, utilizando la potencia del servidor central.

9. Referencias Bibliográficas y Documentación Técnica

  • Ollama Documentation & API Guides: Documentación oficial sobre la API REST y configuración de red. Disponible en: Ollama FAQ & Configuration Guide.
  • Open WebUI Project Guide: Guía de despliegue mediante contenedores y gestión multiusuario. Disponible en: Open WebUI Documentation.
  • Continue.dev Open-Source Assistant: Documentación técnica sobre asistentes de código y editores locales. Disponible en: Continue Docs.
  • NVIDIA Driver & CUDA Documentation: Guías de controladores y plataforma de computación acelerada. Disponible en: NVIDIA CUDA Platform.

10. Conclusiones y Validación

El servidor rsa-userver24-xps se encuentra completamente operativo. La arquitectura implementada permite:

  • Ejecutar modelos de lenguaje eficientes como qwen2.5:1.5b utilizando los recursos optimizados del servidor.
  • Monitorear y comparar el rendimiento del hardware bajo distintas cargas mediante btop y nvidia-smi.
  • Atender peticiones descentralizadas desde múltiples equipos de la red local mediante la API REST y scripts avanzados en Python con streaming, roles, historial y pruebas de rendimiento.
  • Escalar el acceso a múltiples colaboradores mediante interfaces web y herramientas de asistencia técnica e integración documental.

Uso de pantalla 2.8″ TFT ILI9341 con Arduino Nano

Radar iRoX

Por: Julio Cesar Sandria Reynoso, David Rojas Bosada y Fernando Méndez Reyes
4 de octubre de 2026

Arduino Nano Genérico

Desde hace unos meses, nuestro alumno del Instituto de Robótica de Xalapa (iRoX), David, nos trajo el reto de hacer funcionar unas pantallas de 2.8 pulgadas TFT con chip ILI9341 para su proyecto del robot BB8. Nuestro alumno Fernando, llevó a cabo las primeras investigaciones, la hizo funcionar y le proporcionó a David el esquema para hacer una placa PCB muy sencilla. En las últimas semanas, llegamos al punto de ya requerir instalar las pantallas en la unidad BB8 y retomamos el tema de las pantallas.

En este artículo publicamos lo necesario para replicar el inicio del uso de la pantalla 2.8″ TFT 240xRGBx320 V1.1 con circuito integrado ILI9341 en PCB roja, con Arduino Nano (genérico) programado en Arduino IDE 2.3.10.

En una placa de prototipos PCB se soldaron los componentes para colocar Arduino Nano y la Pantalla 2.8 TFT, haciendo las divisiones de voltaje de 5V a 3.3V con resistencias de 2.5 y 5 Kohms.

Como se menciona al principio, empezamos a trabajar con esta pantalla algunos meses atrás (4 ó 5), por lo que al retomar el proyecto, usando la placa PCB ya soldada, y colocándole el Arduino Nano y la pantalla, los ejemplos de código en C++ en Arduino IDE que sabíamos que ya habían funcionado, pues no funcionaban. Dado que ya no teníamos a la mano el diagrama original, rehicimos el esquema electrónico para analizar bien las conexiones entre Arduino Nano y la Pantalla.

NOTA: Para probar los programas de ejemplo de Adafruit hay que instalar en Arduino IDE las librerías Adafruit ILI9341 y Adafruit GFX.

Una vez analizado cómo se había soldado la placa PCB y algunos ejemplos de programas disponibles de Adafruit para una pantalla 2.8 TFT con chip ILI9341, llegamos al siguiente programa para mostrar un «Hola iRoX» en la pantalla.

/*
Instituto de Robótica de Xalapa - 4-Octubre-2026
Programa: Pantalla-2.8-TFT_Hola_iRoX.ino (C++)
Basado en: Simple "Hello World" for ILI9341 LCD
https://wokwi.com/arduino/projects/308024602434470466
y complementado con ejemplos de Adafruit ILI9341
Autor: Julio César Sandria Reynoso
Colaboradores: David Rojas Bosada, Fernando Méndez Reyes
Hardware:
+ Pantalla 2.8" TFT 240xRGBx320 V1.1 (PCB Roja) chip ILI9341
+ Arduino NANO
+ PCB propio con divisores de voltaje de 5V a 3.3V
*/

#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"

// Asegura las siguientes conexiones al Arduino NANO:
// Pines 8 a 13 con divisores de voltaje de 5V a 3.3V
// NOTA: las macros (#define) tienen los nombres de pin
// de la pantalla y pueden variar respecto a los
// ejemplo originales de ADAFRUIT.
// PIN TFT VCC 5V Arduino
// PIN TFT GND GND Arduino
#define TFT_RESET 8
#define TFT_DC 9
#define TFT_CS 10
#define TFT_SDI 11
#define TFT_LED 12
#define TFT_SCK 13
Adafruit_ILI9341 tft = Adafruit_ILI9341(
TFT_CS, TFT_DC, TFT_SDI, TFT_SCK, TFT_RESET, TFT_LED);

void setup() {
tft.begin();
}

void loop(void) {
for(uint8_t rotacion=0; rotacion<4; rotacion++) {
tft.setRotation(rotacion);
texto(rotacion);
delay(1000);
}
}

void texto(uint8_t rotacion) {
tft.fillScreen(ILI9341_BLACK);
tft.setCursor(10, 20);
tft.setTextColor(ILI9341_RED);
tft.setTextSize(3);
tft.println("Hola iRoX!");

tft.setCursor(10, 60);
tft.setTextColor(ILI9341_GREEN);
tft.setTextSize(2);
tft.println("Julio Sandria");

tft.setCursor(10, 100);
tft.setTextColor(ILI9341_YELLOW);
tft.setTextSize(1);
tft.print("rotacion: ");
tft.println(rotacion);
}

Este programa ejecuta un ciclo for(…) dentro de la función loop(), imprimiendo el texto de la función texto(…) en cuatro orientaciones de la pantalla, que seguimos llamando rotaciones para conservar la terminología que usa la librería Adafruit ILI9341, con el método setRotation() del objeto tft.

Y ya habiendo dominado las conexiones y algunos programas ejemplos de Adafruit ILI9341, nos pusimos a jugar un rato haciendo programas con ayuda de una inteligencia artificial generativa, cargando el programa al Arduino Nano y obteniendo algunas imágenes como la siguiente:

El programa funciona como un radar simulando que detecta objetos y mostrándolos como círculos rojos. A continuación está el programa que trabajamos Deepseek, David Rojas y Julio Sandria.

/*
  Instituto de Robótica de Xalapa - 4-Octubre-2026
  Programa: BB8_pant-2.8_radar_horizontal.ino (C++)
  Desarrollado por Julio César Sandria Reynoso, David Rojas Bosada y DeepSeek.
  Basado en: Pantalla-2.8-TFT_Hola_iRoX.ino

  Hardware:
  + Pantalla 2.8" TFT 240xRGBx320 V1.1 (PCB Roja) chip ILI9341
  + Arduino NANO
  + PCB propio con divisores de voltaje de 5V a 3.3V
*/

#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"

// Asegura las siguientes conexiones al Arduino NANO:
// Pines 8 a 13 con divisores de voltaje de 5V a 3.3V
// NOTA: las macros (#define) tienen los nombres de pin
//       de la pantalla y pueden variar respecto a los
//       ejemplo originales de ADAFRUIT.
// PIN TFT VCC    5V Arduino
// PIN TFT GND    GND  Arduino
#define TFT_RESET 8
#define TFT_DC    9
#define TFT_CS   10
#define TFT_SDI  11
#define TFT_LED  12
#define TFT_SCK  13
Adafruit_ILI9341 tft = Adafruit_ILI9341(
  TFT_CS, TFT_DC, TFT_SDI, TFT_SCK, TFT_RESET, TFT_LED);

// ---- Colores ----
#define RADAR_BG      0x0000   // Negro
#define RADAR_GRID    0x0320   // Verde oscuro
#define RADAR_RING    0x0640   // Verde medio
#define RADAR_TEXT    0x07E0   // Verde brillante
#define RADAR_BLIP    0xF800   // Rojo
#define RADAR_BLIP2   0xFFE0   // Amarillo
#define PANEL_BG      0x0000   // Fondo del panel
#define PANEL_LINE    0x0640   // Línea separadora (verde)
#define PANEL_WHITE   0xFFFF   // Blanco
#define HUD_COLOR     0x07E0   // Verde brillante para el HUD

// ---- Layout horizontal (320x240) ----
const int16_t SCREEN_W = 320;
const int16_t SCREEN_H = 240;

const int16_t PANEL_W  = 70;
const int16_t RADAR_AREA_X0 = PANEL_W;
const int16_t RADAR_AREA_W  = SCREEN_W - PANEL_W;  // 250 px

// Centro del radar dentro del área disponible
const int16_t CX = RADAR_AREA_X0 + RADAR_AREA_W / 2;  // ≈195
const int16_t CY = SCREEN_H / 2 + 6;                  // ≈126
const int16_t R  = 95;

// ---- Barrido ----
int16_t angle = 0;
const int16_t SWEEP_STEP = 3;
const int16_t TRAIL = 20;

// ---- Objetivos ----
struct Target {
  int16_t angle;
  int16_t dist;
  uint16_t color;
};

Target targets[] = {
  {  35,  85, RADAR_BLIP  },
  { 120,  55, RADAR_BLIP2 },
  { 200,  70, RADAR_BLIP  },
  { 275,  40, RADAR_BLIP2 },
  { 320,  90, RADAR_BLIP  },
};
const uint8_t NUM_TARGETS = sizeof(targets) / sizeof(targets[0]);
int16_t targetLastHit[NUM_TARGETS];

// ---- HUD ----
int16_t lastHudAngle = -1;   // Para no redibujar si no cambia

// ============================================================
// Utilidades
// ============================================================
float deg2rad(int16_t deg) { return deg * PI / 180.0; }

int16_t polarX(int16_t deg, int16_t radius) {
  return CX + (int16_t)(cos(deg2rad(deg)) * radius);
}
int16_t polarY(int16_t deg, int16_t radius) {
  return CY + (int16_t)(sin(deg2rad(deg)) * radius);
}

// Devuelve el ancho en píxeles de una cadena con el tamaño de texto actual.
// Útil para alinear a la derecha sin depender del font.
int16_t textWidthPx(const char* s, uint8_t size) {
  return strlen(s) * 6 * size;  // fuente estándar Adafruit: 6x8 px por carácter
}

// ============================================================
// Panel lateral izquierdo
// ============================================================
void drawSidePanel() {
  // Fondo del panel
  tft.fillRect(0, 0, PANEL_W, SCREEN_H, PANEL_BG);

  // Línea separadora vertical
  tft.drawFastVLine(PANEL_W - 1, 0, SCREEN_H, PANEL_LINE);

  // Encabezado
  tft.setTextSize(1);
  tft.setTextColor(RADAR_TEXT);
  tft.setCursor(6, 6);
  tft.print("== iRoX ==");

  tft.drawFastHLine(4, 18, PANEL_W - 8, PANEL_LINE);

  // Etiquetas con línea en blanco entre "Rojas" e "iRoX"
  struct Row { const char* text; uint16_t color; uint8_t gap; };
  Row rows[] = {
    { "BB8",     RADAR_TEXT, 0 },
    { "David",   RADAR_TEXT, 0 },
    { "Rojas",   RADAR_TEXT, 6 },   // <-- espacio extra (línea en blanco) después
    { "iRoX",    RADAR_TEXT, 0 },
    { "Julio",   RADAR_TEXT, 0 },
    { "Sandria", RADAR_TEXT, 0 },
  };
  const uint8_t n = sizeof(rows) / sizeof(rows[0]);

  int16_t y = 28;
  for (uint8_t i = 0; i < n; i++) {
    tft.setTextColor(rows[i].color);
    tft.setTextSize(1);
    tft.setCursor(6, y);
    tft.print(rows[i].text);
    y += 14 + rows[i].gap;
  }

  // ---- Después de "Sandria": línea separadora + fecha ----
  y += 2;                                     // pequeño respiro
  tft.drawFastHLine(4, y, PANEL_W - 8, PANEL_LINE);
  y += 6;

  tft.setTextColor(RADAR_TEXT);
  tft.setTextSize(1);
  tft.setCursor(6, y);
  tft.print("Octubre");
  y += 12;

  tft.setCursor(6, y);
  tft.print("3, 2026");

  // Pie del panel
  tft.drawFastHLine(4, SCREEN_H - 14, PANEL_W - 8, PANEL_LINE);
  tft.setTextColor(RADAR_GRID);
  tft.setCursor(6, SCREEN_H - 10);
  tft.print("v1.0");
}

// ============================================================
// HUD: indicador de ángulo dentro del panel
// ============================================================
void drawHudFrame() {
  // Etiqueta "AZ" y línea base; el valor lo actualiza updateHudAngle()
  tft.setTextSize(1);
  tft.setTextColor(HUD_COLOR);

  // Zona del HUD (parte baja del panel, encima del pie)
  int16_t yHud = SCREEN_H - 46;

  // Título del HUD
  tft.setCursor(6, yHud);
  tft.print("AZIMUTH");

  // Caja del valor
  tft.drawRect(4, yHud + 12, PANEL_W - 8, 20, HUD_COLOR);
}

void updateHudAngle(int16_t a) {
  if (a == lastHudAngle) return;
  lastHudAngle = a;

  int16_t yHud = SCREEN_H - 46;
  int16_t boxX = 4;
  int16_t boxY = yHud + 12;
  int16_t boxW = PANEL_W - 8;
  int16_t boxH = 20;

  // Borra el interior de la caja
  tft.fillRect(boxX + 1, boxY + 1, boxW - 2, boxH - 2, PANEL_BG);

  // Formatea "137" + símbolo de grado. El símbolo ° se dibuja como un pequeño círculo
  char buf[8];
  snprintf(buf, sizeof(buf), "%3d", a);

  tft.setTextSize(2);
  tft.setTextColor(HUD_COLOR);

  // Centrar el número dentro de la caja
  int16_t numW = textWidthPx(buf, 2);
  int16_t tx = boxX + (boxW - numW) / 2 - 2;  // -2 para dejar sitio al símbolo °
  int16_t ty = boxY + (boxH - 16) / 2 + 1;

  tft.setCursor(tx, ty);
  tft.print(buf);

  // Dibujar el símbolo de grado como un pequeño círculo
  int16_t degX = tx + numW + 2;
  int16_t degY = ty + 2;
  tft.drawCircle(degX, degY, 2, HUD_COLOR);
}

// ============================================================
// Fondo estático del radar
// ============================================================
void drawRadarBackground() {
  // Borra solo el área del radar
  tft.fillRect(RADAR_AREA_X0, 0, RADAR_AREA_W, SCREEN_H, RADAR_BG);

  // Círculos concéntricos
  for (int r = 25; r <= R; r += 25) {
    tft.drawCircle(CX, CY, r, RADAR_RING);
  }

  // Líneas radiales cada 30°
  for (int a = 0; a < 360; a += 30) {
    tft.drawLine(CX, CY, polarX(a, R), polarY(a, R), RADAR_GRID);
  }

  // Etiquetas de grados cada 30°
  tft.setTextSize(1);
  tft.setTextColor(RADAR_TEXT);
  for (int a = 0; a < 360; a += 30) {
    int16_t x = polarX(a, R + 10);
    int16_t y = polarY(a, R + 10);
    char buf[5];
    snprintf(buf, sizeof(buf), "%d", a);
    tft.setCursor(x - 6, y - 4);
    tft.print(buf);
  }

  // Cruz central
  tft.drawFastHLine(CX - 4, CY, 8, RADAR_TEXT);
  tft.drawFastVLine(CX, CY - 4, 8, RADAR_TEXT);

  // Título "RADAR" alineado a la DERECHA del área del radar
  tft.setTextSize(2);
  tft.setTextColor(RADAR_TEXT);
  const char* title = "RADAR";
  int16_t titleW = textWidthPx(title, 2);
  int16_t titleX = SCREEN_W - titleW - 6;   // 6 px de margen derecho
  int16_t titleY = 6;
  tft.setCursor(titleX, titleY);
  tft.print(title);
}

// ============================================================
// Barrido
// ============================================================
void drawSweep(int16_t currentAngle) {
  tft.drawLine(CX, CY, polarX(currentAngle, R), polarY(currentAngle, R), RADAR_TEXT);

  for (int i = 1; i <= TRAIL; i++) {
    int16_t a = currentAngle - i * SWEEP_STEP;
    if (a < 0) a += 360;

    uint16_t c;
    if      (i < TRAIL / 3)     c = 0x07E0;
    else if (i < 2 * TRAIL / 3) c = 0x03E0;
    else                        c = 0x01A0;

    tft.drawLine(CX, CY, polarX(a, R), polarY(a, R), c);
  }
}

void eraseSweep(int16_t currentAngle) {
  int16_t a = currentAngle - TRAIL * SWEEP_STEP;
  if (a < 0) a += 360;
  tft.drawLine(CX, CY, polarX(a, R), polarY(a, R), RADAR_BG);
  redrawOverlayAtAngle(a);
}

void redrawOverlayAtAngle(int16_t deg) {
  for (int r = 25; r <= R; r += 25) {
    tft.drawPixel(polarX(deg, r), polarY(deg, r), RADAR_RING);
  }
  for (int a = 0; a < 360; a += 30) {
    int16_t diff = abs(deg - a);
    if (diff < 3 || diff > 357) {
      tft.drawLine(CX, CY, polarX(a, 25), polarY(a, 25), RADAR_GRID);
      break;
    }
  }
}

// ============================================================
// Blips
// ============================================================
void drawBlips() {
  for (uint8_t i = 0; i < NUM_TARGETS; i++) {
    int16_t deg = targets[i].angle;
    int16_t r   = targets[i].dist;
    int16_t x   = polarX(deg, r);
    int16_t y   = polarY(deg, r);

    int16_t diff = abs(angle - deg);
    if (diff > 180) diff = 360 - diff;
    if (diff < SWEEP_STEP * 2) targetLastHit[i] = angle;

    if (targetLastHit[i] >= 0) {
      int16_t age = angle - targetLastHit[i];
      if (age < 0) age += 360;

      if (age < 60) {
        tft.fillCircle(x, y, 3, targets[i].color);
      } else if (age < 120) {
        tft.drawCircle(x, y, 3, targets[i].color);
      } else {
        tft.fillCircle(x, y, 3, RADAR_BG);
        for (int rr = 25; rr <= R; rr += 25) {
          if (abs(r - rr) < 3) tft.drawPixel(x, y, RADAR_RING);
        }
        targetLastHit[i] = -1;
      }
    }
  }
}

// ============================================================
// Setup / Loop
// ============================================================
void setup() {
  Serial.begin(9600);
  Serial.println("Radar ILI9341 horizontal iniciando...");

  tft.begin();
  tft.setRotation(1);   // 1 = apaisado (320x240)

  drawSidePanel();
  drawHudFrame();
  drawRadarBackground();
  updateHudAngle(0);

  for (uint8_t i = 0; i < NUM_TARGETS; i++) targetLastHit[i] = -1;

  Serial.println("Radar listo.");
}

void loop() {
  eraseSweep(angle);
  drawSweep(angle);
  drawBlips();
  updateHudAngle(angle);

  angle += SWEEP_STEP;
  if (angle >= 360) angle -= 360;

  delay(20);
}

Si tienes un proyecto por hacer con Lego Mindstorms, Arduino, Esp32, Raspberry Pi, Jetson Nano o necesitas ayuda con la programación en C/C++, Python, Java, JavaScript, o con el uso de herramientas de inteligencia artificial, no dudes en contactarnos en el Instituto de Robótica de Xalapa a través del sitio web www.irox.mx.

Usar Google Drive en Ubuntu 22.04 LTS

Por Julio César Sandria Reynoso
30 de septiembre de 2026

Esta guía detalla los pasos para configurar y montar Google Drive de forma automática al iniciar el sistema en Ubuntu 22.04 LTS usando Rclone.

Rclone es un programa de línea de comandos para administrar archivos en almacenamiento en la nube. Es una alternativa completa a las interfaces web de los proveedores de servicios en la nube. Más de 70 productos de almacenamiento en la nube son compatibles con rclone, incluyendo almacenes de objetos S3, servicios de almacenamiento de archivos para empresas y consumidores, así como protocolos de transferencia estándar (https://rclone.org/).

Paso 1: Configurar la cuenta remota en Rclone

Abre la terminal y ejecuta el asistente interactivo:

rclone config
  1. Presiona n para crear un nuevo remoto.
  2. Asigna un nombre al remoto (ejemplo: uxgdrive).
  3. Selecciona la opción de Google Drive en la lista de proveedores.
  4. Deja en blanco el Client ID y Client Secret (presiona Enter).
  5. Selecciona el nivel de acceso 1 (Acceso total / Full access).
  6. Confirma el inicio de sesión en el navegador web para otorgar los permisos a Rclone.
  7. Guarda la configuración y sal del menú.

Paso 2: Habilitar permisos en /etc/fuse.conf

Para que el servicio de sistema pueda montar la carpeta con la opción --allow-other, habilita la opción global en FUSE:

sudo sed -i 's/#user_allow_other/user_allow_other/g' /etc/fuse.conf

Paso 3: Crear la carpeta de destino

Crea la carpeta dentro de tu usuario donde se visualizarán los archivos de Google Drive, reemplazando «julio» por tu usuario:

mkdir -p /home/julio/uxgdrive

Paso 4: Crear el servicio de systemd

Crea el archivo del servicio de sistema ejecutando:

sudo nano /etc/systemd/system/rclone-gdrive.service

Pega el siguiente contenido dentro del archivo. Reemplaza «julio» por tu usuario y «uxgdrive» por el nombre que le quieras dar a tu Google Drive, puede ser gdrive. Aunque, si en algún momento configuras dos o más cuentas en tu Ubuntu, bien vale la pena diferenciarlos desde el principio.

[Unit]
Description=Rclone Google Drive Mount
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=julio
ExecStart=/usr/bin/rclone mount uxgdrive: /home/julio/uxgdrive \
    --config /home/julio/.config/rclone/rclone.conf \
    --vfs-cache-mode full \
    --allow-other \
    --dir-cache-time 72h \
    --vfs-cache-max-age 72h
ExecStop=/usr/bin/fusermount -u /home/julio/uxgdrive
Restart=on-failure
RestartSec=10

[Install]
WantedBy=default.target

Guarda con Ctrl + O, confirma con Enter y sal con Ctrl + X.

Paso 5: Recargar y activar el servicio

Recarga el demonio de systemd e inicia el servicio:

sudo systemctl daemon-reload
sudo systemctl enable rclone-gdrive
sudo systemctl start rclone-gdrive

Paso 6: Comprobar el estado del montaje

Asegúrate de que el servicio esté activo y en verde:

sudo systemctl status rclone-gdrive

¡Listo! Ya puedes ver y administrar todos tus archivos.

NOTA: Esta guía fue elaborada con ayuda de Google Gemini.

Instalación de una IA Generativa con Ubuntu y Ollama

Página de inicio de Open WebUi

Por: Julio C. Sandria R.

Introducción

En este artículo comparto la bitácora del proceso que realicé con la asistencia de Google Gemini para desplegar un entorno de inteligencia artificial generativa (IAG) local, privado y autohospedado. El sistema se instaló utilizando Ollama como motor de inferencia y Open WebUI como interfaz gráfica, ejecutándose sobre una computadora de escritorio antigua con Ubuntu Server 24.04 LTS, limitada a 8 GB de RAM, un procesador Intel Pentium CPU G4400 @ 3.30GHz y un disco duro mecánico de 372 GB.

La idea principal fue buscar alternativas para ejecutar de forma local una IA Generativa con la finalidad de reducir costos. Al utilizar herramientas comerciales de forma gratuita (como ChatGPT, Claude o DeepSeek), es común agotar los límites de uso o tokens disponibles, lo que obliga a pagar suscripciones mensuales o anuales para continuar.

Pregunta: ¿Qué es la inteligencia artificial generativa?

La interfaz de consola ejecutada en una terminal funciona con los tres modelos de lenguaje descargados, pero aquí se muestra solo con el modelo más pequeño (TinyLlama). Una vez iniciado Ollama, al responder por primera vez a la pregunta, tardó poco más de 3 minutos en comenzar a mostrar texto. La segunda vez que se hizo la misma consulta, empezó a responder a los 16 segundos. Esto ocurre porque en la primera ejecución el sistema debe cargar por completo el archivo del modelo en la memoria RAM. Por cierto, a la misma pregunta respondió de forma diferente, como se puede apreciar en la siguiente imagen.

Por su parte, la interfaz web Open WebUI solo logró ejecutarse de forma estable con el modelo de lenguaje más pequeño. Los tiempos de respuesta variaron desde varios segundos hasta minutos para iniciar la redacción en español. A la misma pregunta realizada previamente en la terminal, ofreció una respuesta completamente diferente después de 5 minutos de procesamiento.

Durante este proceso se cambió el disco duro mecánico por una unidad de estado sólido (ssd), con lo cual se mejoró el tiempo de inicio de respuesta de Ollama a 1 ó 2 segundos. En la tabla siguiente se muestran los modelos probados y algunas de sus características. Un parámetro es una variable ajustable que define cómo va a actuar un sistema o qué resultado va a entregar. Esto nos da una idea de qué tan grande es el modelo de lenguaje, medido en billones de parámetros.

Modelo Parámetros Consumo / Características Principales Rendimiento en Hardware Limitado (8GB RAM)
TinyLlama 1.1B Modelo minimalista y ultraligero; diseñado para CPU, no soporta function calling nativo. Respuesta más rápida y estable; ideal para pruebas de concepto tras deshabilitar herramientas avanzadas.
Qwen 2.5 1.5B Arquitectura compacta optimizada para lógica y razonamiento en español. Rendimiento muy justo; excelente lógica pero requiere buena optimización del entorno.
Llama 3.2 3B Modelo equilibrado de Meta con gran capacidad de comprensión y generación general. Con el cambio a disco SSD, mejoró notablemente su velocidad de carga frente al disco mecánico anterior.

Reflexión final: Como resumen humano, utilizar una computadora antigua para correr una IAG de manera local es una experiencia de aprendizaje e implementación muy interesante, pero no es práctica para el uso cotidiano debido a los tiempos de espera. Para obtener un rendimiento fluido y respuestas ágiles, se requiere infraestructura más robusta: mayor cantidad de memoria RAM, un procesador más potente, el uso indispensable de unidades de estado sólido (SSD) en lugar de discos mecánicos, e idealmente una tarjeta gráfica dedicada con memoria VRAM.

Las líneas a continuación corresponden al resumen técnico generado por Gemini al finalizar toda la interacción de instalación y configuración:

Conceptos Clave

  • LLM (Large Language Model): Sistema de inteligencia artificial entrenado para comprender, generar y procesar lenguaje natural.
  • Ollama: Herramienta ligera y de código abierto diseñada para descargar, configurar y ejecutar modelos de lenguaje localmente en tu propia infraestructura.
  • Docker: Tecnología de virtualización basada en contenedores para empaquetar aplicaciones con todas sus dependencias de forma aislada y eficiente.

1. Actualización del Sistema

Antes de comenzar con la instalación de los servicios, es recomendable actualizar los paquetes del sistema operativo:

sudo apt update && sudo apt upgrade -y

2. Instalación y Configuración de Ollama

Ollama es el motor backend encargado de ejecutar los modelos de lenguaje localmente. Se instala mediante su script oficial:

curl -fsSL https://ollama.com/install.sh | sh

Para verificar que el servicio se encuentra activo y funcionando correctamente:

sudo systemctl status ollama

Con el siguiente comando puedes verificar que quedó instalado Ollama y se ejecuta con el modelo reducido TinyLlama (1.1B):

ollama run tinyllama

Toma en cuenta que la primera vez que usas un modelo, se descarga de internet a tu servidor y después se carga el modelo completo en la memoria RAM. Ante el prompt de ollama «>>>» escribe un saludo o una pregunta y contestará. Para salir de ollama usa el comando /bye.

Para instalar Qwen 2.5 (1.5B) usa el siguiente comando:

ollama run qwen2.5:1.5b

Con el siguiente comando ejecutas Ollama con el modelo Llama 3.2 (3B):

ollama run llama3.2:3b

Exponer Ollama a la red local: Por defecto, Ollama solo escucha en localhost. Para permitir conexiones desde otros equipos de la red, edita la configuración del servicio:

sudo SYSTEMD_EDITOR=nano sudo systemctl edit ollama.service

Añade las siguientes líneas dentro de la sección [Service]:

[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"

Guarda los cambios y reinicia el demonio:

sudo systemctl daemon-reload
sudo systemctl restart ollama

Permite el tráfico en el cortafuegos si es necesario:

sudo ufw allow 11434/tcp
sudo ufw enable
sudo ufw reload

Una vez habilitado el cortafuegos puedes abrir en un navegador la dirección http://IP_SERVIDOR:11434 reemplazando IP_SERVIDOR por la dirección IP de tu Servidor, y deberás ver la respuesta en una sola línea: Ollama is running. Con esto verificas que Ollama ya escucha peticiones como servidor web.

3. Instalación de Docker

Docker permite ejecutar Open WebUI en un contenedor aislado optimizando el uso de recursos:

sudo apt update
sudo apt install docker.io docker-compose-v2 -y
sudo systemctl enable --now docker

4. Despliegue de Open WebUI

Open WebUI provee la interfaz gráfica web accesible desde cualquier navegador, emulando una experiencia similar a ChatGPT:

sudo docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main

Una vez completado, puedes acceder desde tu navegador mediante:

http://IP_DE_TU_SERVIDOR:3000

Cuando se abra la página principal de Open WebIU oprime el botón Get started, llena el formulario con tu nombre, correo y contraseña que son para crear el usuario administrador y listo, ya puedes usar tu propio ChatBot en tu propio servidor.

5. Modelos recomendados y solución para modelos ultraligeros

Debido a la limitación de memoria RAM en servidores de 8 GB, se evaluó el comportamiento de distintos modelos:

  • Llama 3.2 (3B) y Qwen 2.5 (1.5B): Al probar modelos de mayor tamaño en la interfaz web, se presentaron saturaciones de procesamiento y bloqueos de ejecución en el hardware disponible.
  • TinyLlama (1.1B): Se implementó como modelo ultraligero de prueba (ollama run tinyllama), logrando una respuesta rápida y fluida directamente en el motor.

Ajuste crítico de compatibilidad en Open WebUI: Para lograr que el modelo ultraligero respondiera correctamente a través de la interfaz web sin arrojar errores internos, fue necesario acceder a la configuración del modelo dentro de Open WebUI y desmarcar la opción «Builtin Tool» en el panel de capacidades. Esto evita que la interfaz intente forzar llamadas a funciones o herramientas no soportadas por la arquitectura minimalista de TinyLlama.

Referencias y Enlaces de Interés

  1. Ollama Documentation and Official Repository. Disponible en: https://ollama.com
  2. Open WebUI Official Documentation. Disponible en: https://github.com/open-webui/open-webui

Uso de módulo WiFi Esp8266 con Arduino UNO e IoT en Thingsboard

/* AmbiSensor - Sensor Ambiental (simulación) - IoT (Internet of Things)
   Julio César Sandria Reynoso - Mayo 2026
   Este programa genera aleatoriamente datos ambientales de
   Temperatura, Humedad, CO2 e Iluminación, y los envía a
   un servidor en la nube para poder ver los datos en tiempo
   real desde cualquier dispositivo 
  Hardware: Arduino UNO, Módulo WiFi Esp8266, Fuente de poder para protoboard,
  protoboard, 2 resistencias de 1K, cables dupont.
*/

// Incluimos la librería WiFiEspAT que es más reciente que WiFiEsp
#include <WiFiEspAT.h>

// Si usamos Arduino Mega o Leonardo USAR_MEGA = true
// Si usamos Arduino Uno o Nano USAR_MEGA = false
// const bool USAR_MEGA = true; // true = Arduino Mega | false = Arduino Uno

// Configuración dinámica de puertos Serial según la placa
// usando directivas del precompilador
#if defined(ARDUINO_AVR_MEGA2560) || defined(__AVR_ATmega2560__)
  // Si Arduino IDE detecta un Arduino Mega o Leonardo
  // Conecta pin 18 (TX1) a pin RX del Esp, con divisor de voltaje a 3.3V
  // Conecta pin 19 (RX1) a pin TX del Esp
  #define EspSerial Serial1
#else
  #include <SoftwareSerial.h>
  // En Arduino Uno o Nano:
  // Conecta pin 2 (RX) a pin TX del Esp
  // Conecta pin 3 (TX) a pin RX del Esp, con divisor de voltaje a 3.3V
  SoftwareSerial softSerial(2, 3); 
  #define EspSerial softSerial
#endif

// Credenciales Wi-Fi
const char* ssid     = "nombre-red-wifi";
const char* password = "clave-red-wifi";

// Configuración del servidor ThingsBoard
const char* server   = "thingsboard.cloud";
const int port       = 80;
const char* token    = "TOKEN";

// Variables de ambiente
float temp = 25;          // En grados Celcius
float humedad = 62;       // En porcentaje (%)
int co2 = 450;            // En ppm (partes por millón)
int luz = 780;            // En luxes o valor analógico (0-1023)

void setup() {
  Serial.begin(9600);
  Serial.println("Iniciando programa AmbiSensor - Sensor Ambiental...");

  // Inicializar la comunicación serial con el módulo WiFi Esp8266
  EspSerial.begin(9600); // Velocidades preferentes 115200 (Mega) o 9600 (Uno)

  Serial.println("Inicializando modulo Wi-Fi...");
  WiFi.init(&EspSerial);

  if (WiFi.status() == WL_NO_MODULE) {
    Serial.println("No se encontró el módulo Wi-Fi AT");
    while (true); // Detener el programa si no hay módulo
  }

  // Conectar a la red Wi-Fi
  Serial.print("Conectando a Wi-Fi: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\n¡Conectado a la red!");
}

void loop() {
  // Asegurar que seguimos conectados antes de enviar datos
  if (WiFi.status() == WL_CONNECTED) {
    
    WiFiClient client;
    
    // Abrir conexión TCP con el servidor
    if (client.connect(server, port)) {
      Serial.println("Conectado al servidor de ThingsBoard");

      // *** AQUÍ SE DEBEN TOMAR LECTURAS DE LOS SENSORES REALES ***
      // Para este ejemplo, vamos simulando lecturas aleatorias
      temp += 2-random(5);        // En grados Celcius
      humedad += 2-random(5);     // En porcentaje (%)
      co2 += 7-random(15);        // En ppm (partes por millón)
      luz += 7-random(15);        // En luxes o valor analógico (0-1023)

      // 2. Preparar un JSON con múltiples variables
      // El formato final será: 
      // {"temperature":24.5,"humidity":62.5,"co2":450,"light":780}
      String sDatos = "{";
      sDatos += "\"temperature\":" + String(temp) + ",";
      sDatos += "\"humidity\":" + String(humedad) + ",";
      sDatos += "\"co2\":" + String(co2) + ",";
      sDatos += "\"light\":" + String(luz);
      sDatos += "}";

      // 3. Enviar la petición HTTP POST
      client.print("POST /api/v1/");
      client.print(token);
      client.println("/telemetry HTTP/1.1");
      
      // Encabezados
      client.print("Host: ");
      client.println(server);
      client.println("Content-Type: application/json");
      client.print("Content-Length: ");
      client.println(sDatos.length());
      client.println("Connection: close"); // Buena práctica para liberar memoria en el servidor
      
      // Línea en blanco obligatoria
      client.println(); 
      
      // Cuerpo de los datos (sDatos)
      client.print(sDatos);
      client.println();

      Serial.println("Datos enviados: " + sDatos);

      // 4. Leer la respuesta del servidor (para asegurarnos 
      // del 200 OK, que significa datos recibidos correctamente)
      unsigned long timeout = millis();
      while (client.available() == 0) {
        if (millis() - timeout > 5000) {
          Serial.println(">>> ¡Timeout!");
          client.stop();
          return;
        }
      }

      // Imprimir solo la primera línea (donde viene el código 200) 
      // para no saturar el monitor
      if (client.available()) {
        String status_line = client.readStringUntil('\r');
        Serial.println("Status: " + status_line);
      }

      // Cerrar la conexión
      client.stop();
    } else {
      Serial.println("Error: No se pudo conectar al servidor");
    }
  } else {
    Serial.println("Wi-Fi desconectado. Intentando reconectar...");
  }

  // Esperar 10 segundos antes del siguiente envío
  delay(10000);
}

Instalación de ROS 2 y Webots en Ubuntu 22.04 LTS

Por Julio César Sandria Reynoso
30 de abril de 2026

Introducción

En este artículo se describe la instalación de ROS 2 y Webots en Ubuntu 22.04 LTS en una computadora con arranque dual (dual boot) que ya tiene Windows 11 instalado. El Dual Boot es la opción más robusta para simulaciones pesadas porque permite que Ubuntu acceda al 100% del hardware (CPU, RAM y, sobre todo, la GPU) sin las capas de abstracción de una máquina virtual.

A continuación se muestra la hoja de ruta para instalar Ubuntu 22.04 LTS junto a Windows 11 de forma segura:

1. Preparación en Windows 11 (Crítico)

Antes de tocar cualquier partición, se debe preparar el terreno para evitar bloqueos del sistema.

  • Desactivar «Inicio Rápido» (Fast Boot): Windows bloquea el disco duro al apagarse si esto está activo, impidiendo que Ubuntu escriba en él.
    • Panel de Control > Hardware y sonido > Opciones de energía > Elegir el comportamiento de los botones de inicio.
  • Desactivar BitLocker: Si Windows 11 tiene el cifrado de disco BitLocker activado, se debe desactivar o tener a mano la clave de recuperación de 48 dígitos. De lo contrario, no se podrá redimensionar la partición.
  • Hacer espacio: Hacer clic derecho en el botón de Inicio > Administración de discos. Reducir el volumen de la unidad C: para dejar al menos 60-100 GB de «Espacio no asignado» para Ubuntu, ROS 2 y simulaciones de Webots.

2. Crear el medio de instalación

  1. Descargar el archivo ISO oficial de Ubuntu 22.04.x LTS.
  2. Usar una herramienta como Rufus o BalenaEtcher para «quemar» el ISO en una memoria USB de al menos 8 GB.

3. Configuración de la BIOS/UEFI

Reiniciar la computadora y entrar al BIOS (usualmente presionando F2, F12, Del o Esc al arrancar).

  • Secure Boot: En la mayoría de las computadoras desktop o laptops modernas, se puede dejarlo activado, pero si se tienen problemas para arrancar el USB, probar desactivarlo.
  • SATA Mode: Asegurarse de que esté en AHCI (si está en RAID, Ubuntu podría no ver el disco duro o SSD). Cuidado: cambiar esto puede requerir configurar Windows para arrancar en AHCI primero.

4. El proceso de instalación de Ubuntu

Arrancar la computadora desde la USB (Boot Menu) y selecciona «Install Ubuntu». Siguiendo estos pasos clave:

  1. Red y Actualizaciones: Conectar a Wi-Fi y marca la casilla «Instalar programas de terceros para hardware de gráficos y Wi-Fi». Esto es vital para que los drivers de NVIDIA (si los hay) funcionen con Webots.
  2. Tipo de instalación: Aquí viene lo más importante. Seleccionar «Instalar Ubuntu junto a Windows Boot Manager».
    • Si no aparece esta opción, elegir «Más opciones» para asignar manualmente el espacio no asignado creado en el paso 1.
  3. Particionado (si se elige Manual): Crear una partición root (punto de montaje /) con el resto del espacio (mínimo 50 GB). Formato: ext4.
  4. Finalizar: Seguir las instrucciones de ubicación y usuario.

5. Post-Instalación: El menú GRUB

Al reiniciar la computadora, se deberá ver una pantalla negra con letras blancas (el GRUB) que permite elegir entre Ubuntu y Windows Boot Manager.

6. Configuración para ROS 2 + Webots

Una vez en Ubuntu, lo primero que se debe hacer es optimizar el sistema para la simulación:

  1. Drivers de Video: Ir a «Software y actualizaciones» > «Más controladores» y asegurarse de usar el driver NVIDIA (Proprietary, tested). Sin esto, Webots irá muy lento.

7. Instalación de ROS 2 Humble

A continuación se detalla los comandos para que, una vez que se inicie sesión en Ubuntu 22.04, se deje listo para realizar simulaciones con ROS 2.

Abrir una terminal (Ctrl + Alt + T) y ejecutar estos bloques de comandos:

7.1. Configurar los Repositorios y las Claves

Primero, autorizar a Ubuntu para que confíe en los servidores de ROS 2.

$ sudo apt update
$ sudo apt install curl gnupg2 lsb-release -y
$ sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(source /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null

7.2. Instalar ROS 2 Humble (Desktop)

Instalar la versión completa que incluye Rviz2.

$ sudo apt update
$ sudo apt install ros-humble-desktop ros-dev-tools -y

7.3. Instalar Webots y el Driver de ROS 2

Cyberbotics mantiene un repositorio propio, pero la forma más estable de integrarlo con ROS 2 es instalando el paquete oficial del puente:

$ sudo apt install ros-humble-webots-ros2 -y

Nota: Este comando instalará automáticamente una versión compatible de Webots si no se tiene.

7.4. Configurar el Entorno (Automático)

Para no tener que escribir source /opt/ros/humble/setup.bash cada vez que se abre una terminal, se añade a la configuración personal:

$ echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
$ source ~/.bashrc

7.5. Verificación de Seguridad

Para confirmar que todo está en orden, intentar correr el ejemplo oficial de un robot móvil en un entorno simulado:

$ ros2 launch webots_ros2_universal_robot multirobot_launch.py

Al ejecutar este comando te preguntará si instalar Webots, a lo que debes responder ´Y´. Una vez que se instale se deberá abrir una ventana como la siguiente:

Finalmente, con esto queda listo tu entorno para realizar pruebas con ROS 2 y Webots en Ubuntu 22.04 LTS.

Matriz de 11×2 LEDs controlada con Arduino para un R2D2

Autor: Julio César Sandria Reynoso
Fecha: 9 de noviembre de 2024

Robot tipo R2D2

En este artículo se presenta un proyecto de creación de una matriz de 11×2 LEDs que se controlan con Arduino para colocarlos en una ranura de un robot similar al R2D2 de las películas Star Wars.

Semanas atrás, uno de los alumnos del Instituto de Robótica de Xalapa (iRoX) llevó su robot R2D2 para hacerle algunas mejoras. Una de esas mejoras consiste en ponerle LEDs en algunas de las ranuras de la cabeza.

Una de esas ranuras es rectangular de 9 cm X 1.2 cm, donde se puede colocar una matriz de 11×2 LEDs por dentro de la cabeza para que se pueda ver su encendido desde el exterior.

Cabeza de R2D2

La idea de poner varios LEDs fue para emular un tanto la forma en que trabaja el robot de las películas, encendiendo de diversas formas los LEDs de esa ranura.

Algunos de los alumnos participaron en el diseño de la sección rectangular para la colocación de los LEDs, inicialmente se consideró una tabla de MDF de 3mm cortada en láser para colocar allí los LEDs, pero se descartó porque con una placa fenólica perforada fue suficiente.

La disposición de los 22 LEDs, con resistencias, transistores y conexiones a Arduino se diseñó en Tinkercad. De hecho, en esta plataforma se hizo también la programación inicial y simulación de su funcionamiento, lo cual se muestra en el artículo Arreglo de 22 LEDS (11×2) con encendido aleatorio con Arduino en Tinkercad.

Arreglo de LEDs en 11 columnas y 2 filas conectados con transistores a Arduino UNO

Después del diseño del circuito en Tinkercad, se procedió a soldar los componentes en la placa fenólica. El material usado es el siguiente:

  • Placa fenólica perforada
  • 22 LEDs de diferentes colores
  • 11 resistencias de 220 Ohms
  • 2 transistores 2N2222
  • 2 resistencias de 1 KOhm
  • Cable UTP
  • Tira de pines macho 90° (14 conectores)
Componentes soldados a la placa fenólica

Una vez soldados todos los componentes como se ve en la imagen «Componentes soldados a la placa fenólica», se conectaron cables Dupont hembra-macho del circuito al Arduino UNO de la siguiente forma:

  • Los primeros 11 cables de izquierda a derecha se conectan a los pines digitales 10 a 0 de Arduino respectivamente.
  • El antepenúltimo cable (amarillo) se conecta al pin analógico A0.
  • El penúltimo cable (naranja) se conecta al pin analógico A1.
  • El último cable (rojo) se conecta a GND.

Una vez conectado todo, se cargó el siguiente programa C++ desde Arduino IDE al Arduino UNO.

// Programa C++ para Arduino
// Autor: Julio César Sandria Reynoso
// Fecha: Noviembre 2024
// Arreglo de LEDs de 11 columnas X 2 filas,
// enciende LEDs de forma aleatoria o con ciertos patrones
#define columnas 11
#define filas 2
int Columnas[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int Filas[] = {A0, A1};
int ledColumna[columnas];
int ledFila[columnas];

void setup() {
  for (int col = 0; col < columnas; col++)
    pinMode(Columnas[col], OUTPUT); 
  for (int fila = 0; fila < filas; fila++)
    pinMode(Filas[fila], OUTPUT);
}

void loop() {
  // Enciende un led iniciando en la primera fila de 
  // izquierda a derecha y terminando en la segunda 
  // fila a la izquierda
  for(int t=100; t>=40; t=t-20)
    ledPorFila(t,0);

  // Enciende de 1 a 5 leds de forma aleatoria durante un tiempo
  // de 100 + i*50 ms y apagándolos 0 ms
  for(int i=1; i<6; i++)
    ledsAleatorios(i,100+i*50,0);

  // Enciende 6 leds de forma aleatoria durante 50 iteraciones
  for(int i=0; i<50; i++)
    ledsAleatorios(6,350,0);
}

void ledPorFila(int tEncendido, int tApagado) {
  for(int i=columnas-1; i>=0; i--) {
    digitalWrite(Filas[0], HIGH);
    digitalWrite(Columnas[i], HIGH);
    delay(tEncendido);

    digitalWrite(Filas[0], LOW);
    digitalWrite(Columnas[i], LOW);
    delay(tApagado);
  }
  for(int i=0; i<columnas; i++) {
    digitalWrite(Filas[1], HIGH);
    digitalWrite(Columnas[i], HIGH);
    delay(tEncendido);

    digitalWrite(Filas[1], LOW);
    digitalWrite(Columnas[i], LOW);
    delay(tApagado);
  }
}

void ledsAleatorios(int nLeds, int tEncendido, int tApagado) {
  // Los arreglos ledFila[] y ledColumna[] tienen <columnas> elementos,
  // de los cuales solo se usan <nLeds> elementos en esta función.
  // Es posible que de forma aleatoria se repita un led a encender,
  // pero es imperceptible o poco notorio durante la ejecución.
  // Encendemos los nLeds indicados, durante <tEcendido> milisegundos
  for(int i=0; i<nLeds; i++) {
    ledFila[i] = random(0, filas);
    ledColumna[i] = random(0, columnas);
    digitalWrite(Columnas[ledColumna[i]], HIGH);
    digitalWrite(Filas[ledFila[i]], HIGH);
  }
  delay(tEncendido);
  // Apagamos todos los leds encendidos
  for(int i=0; i<nLeds; i++) {
    digitalWrite(Columnas[ledColumna[i]], LOW);
    digitalWrite(Filas[ledFila[i]], LOW);    
  }
  delay(tApagado);
}

Las siguientes fotografías son del circuito funcionando.

Al probar el programa se detectó que algunos LEDs no encendían, por lo que se hizo una revisión exhaustiva, encontrando algunos errores de soldadura, que se fueron resolviendo.

Con este tipo de proyectos, donde los alumnos practican los temas que se van viendo en el iRoX, los alumnos se sienten motivados a aprender aún mucho más.

Si te interesa aprender más sobre programación en C++, Arduino, robótica u otros lenguajes de programación, visita el sitio web del Instituto de Robótica de Xalapa (www.irox.mx) y contáctanos. Si estás en una región cercana a Xalapa, como Veracruz, Boca del Río, Córdoba, Martínez de la Torre, etc., puedes visitar las instalaciones del iRoX entre semana o un sábado y hablar con nuestros profesores sobre proyectos de robótica o tecnología.

Agradecemos a los alumnos que están participando en este proyecto: David, Germán, Fernando, Carlo y Max.

Arreglo de 22 LEDS (11×2) con encendido aleatorio con Arduino en Tinkercad

Instituto de Robótica de Xalapa - iRoX

Autor: Julio César Sandria Reynoso
Fecha: 19 de octubre de 2024

En este artículo se presenta un proyecto en Tinkercad (www.tinkercad.com) haciendo un circuito para conectar 22 LEDs en un arreglo de 11 columnas y 2 filas, con encendido aleatorio de los LEDs.

Lo interesante de este proyecto es mostrar cómo, teniendo únicamente 20 pines de entrada/salida en Arduino UNO (14 digitales y 6 analógicos), podemos conectar 22, 30 ó 45 LEDs y controlarlos fácilmente mediante programación C++ en Arduino.

Arreglo de LEDs en 11 columnas y 2 filas conectados con transistores a Arduino UNO

En la imagen se puede ver que en forma de columna, se conectan dos LEDs a cada uno de los pines digitales 0 a 10 de Arduino. Por ejemplo, viendo los LEDs de derecha a izquierda, en los primeros dos LEDs los ánodos (+) se conectan entre si, después a una resistencia de 220 Ohms y la resistencia se conecta al pin 0 de Arduino. Y así sucesivamente se conectan los demás LEDs.

En la primera fila de 11 LEDs (los de arriba), se conectan entre si sus cátodos (-), y todos ellos se conectan a la patilla C (colector) del transistor NPN de la derecha. La patilla E (emisor) se conecta a tierra (GND de Arduino) y la patilla B (base) se conecta a una resistencia de 1 KOhm y ésta al pin analógico A0 de Arduino.

De forma similar, la segunda fila de 11 LEDs se conectan sus cátodos (-) al transistor NPN de la izquierda y la patilla B (base) se conecta con una resistencia de 1 Ohm de por medio, al pin A1 de Arduino.

Para encender los primeros dos LEDs de la derecha, se activa el pin 0 de Arduino con digitalWrite(0,HIGH), pero esto por si solo no enciende ninguno de los dos LEDs. Para encender el primer LED de la derecha-arriba, se debe activar el pin A0 de Arduino con digitalWrite(A0,HIGH). Con esto, se envía el voltaje necesario a la base del transistor de la derecha, lo que cierra el circuito entre el colector y emisor de dicho transistor, permitiendo el flujo de corriente desde el pin 0 de Arduino, pasando por el LED (derecha-arriba), el transistor derecho, hasta el GND de Arduino.

El programa siguiente implementa el encendido de forma aleatoria de un LED durante 15 veces y después el encendido de dos LEDs de forma aleatoria durante 20 veces. Esto se repite mientras se ejecute la simulación en Tinkercad.

// Programa C++ para Arduino
// Arreglo de LEDs de 11 columnas X 2 filas,
// enciende un LED de forma aleatoria y
// después enciende dos LEDs de forma aleatoria
int Columnas[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int Filas[] = {A0, A1};
int columnas = 11;
int filas = 2;

void setup() {
  for (int col = 0; col < columnas; col++)
    pinMode(Columnas[col], OUTPUT); 
  for (int fila = 0; fila < filas; fila++)
    pinMode(Filas[fila], OUTPUT);
}

void loop() {
  // 15 veces encendemos un led de forma aleatoria
  ledAleatorio(15);
  // 20 veces encendemos dos leds de forma aleatoria
  ledAleatorio2(20);
}

void ledAleatorio(int n) {
  int aFila;
  int aColumna;

  for(int i=0; i<n; i++) {
    aFila = random(0, filas);
    aColumna = random(0, columnas);

    digitalWrite(Filas[aFila], HIGH);
    digitalWrite(Columnas[aColumna], HIGH);
    delay(300);

    digitalWrite(Filas[aFila], LOW);
    digitalWrite(Columnas[aColumna], LOW);
    delay(200);
  }
}

void ledAleatorio2(int n) {
  int aFila;
  int aColumna;
  int a2Fila;
  int a2Columna;
  
  for(int i=0; i<n; i++) {
    aFila = random(0, filas);
    aColumna = random(0, columnas);
    a2Fila = random(0, filas);
    a2Columna = random(0, columnas);

    digitalWrite(Filas[aFila], HIGH);
    digitalWrite(Columnas[aColumna], HIGH);
    digitalWrite(Filas[a2Fila], HIGH);
    digitalWrite(Columnas[a2Columna], HIGH);
    delay(300);

    digitalWrite(Filas[aFila], LOW);
    digitalWrite(Columnas[aColumna], LOW);
    digitalWrite(Filas[a2Fila], LOW);
    digitalWrite(Columnas[a2Columna], LOW);
    delay(200);
  }
}

Si te interesa aprender más sobre programación en C++, Arduino, robótica u otros lenguajes de programación, visita el sitio web del Instituto de Robótica de Xalapa (www.irox.mx) y contáctanos. Si estás en una región cercana a Xalapa, como Veracruz, Boca del Río, Córdoba, Martínez de la Torre, etc., puedes visitar las instalaciones del iRoX entre semana o un sábado y hablar con nuestros profesores sobre proyectos de robótica o tecnología.

Simulador de robots móviles Webots

Webots es un simulador de robots móviles 3D de código abierto. Es una plataforma de simulación robótica diseñada para un uso profesional y ha sido ampliamente utilizada en la industria, la educación y la investigación.

Originalmente se desarrolló como una herramienta para investigar varios algoritmos de control en robótica móvil. Desde diciembre de 2018, Webots se liberó como un software de código abierto bajo la licencia Apache 2.0.

El sitio web de Webots es https://cyberbotics.com/ y la guía de uso está en https://cyberbotics.com/doc/guide/index.

Webots tiene una interfaz gráfica de usuario (GUI) con una ventana 3D para interactuar con la simulación 3D así como un editor de texto para editar el código fuente, entre otros elementos.