Como tener tu propia IA en casa

Todos hemos oído y probado las bondades de chat-gpt o usado github copilot con tremendo éxito, pero estos sistemas tienen un problema principal, que son de pago. Sus modelos son cerrados y hay que pagar una licencia para poder utilizarlos en cosas útiles. Sin embargo, existe otra manera de experimentar con la Inteligencia Artificial generativa en casa, sin pagar licencias y teniendo todo el control. Solo necesitas un equipo medianamente moderno, una GPU y una cantidad de memoria abundante (o no tanta, pero podrás jugar con menos modelos). Te cuento aquí como instalar tu propio servicio de IA en tu ordenador.

Eso si, te lo cuento solo para Linux, si tienes algún otro sistema operativo de esos de juguete tendrás que buscarte la vida (te dejo enlaces para que puedas hacerlo por tu cuenta).

Como modelo de AI vamos a utilizar llama, modelo opensource de Meta y lo vamos a instalar con ollama. Hay varias guías para instalarlo directamente en tu ordenador, pero las últimas versiones de ubuntu (yo tengo la 24.04) son ciertamente reticentes a instalar paquetes python en el sistema, por lo que la solución más sencilla será usar docker para ello. Vamos a suponer que tenemos una GPU nvidia, y la porción de docker-compose necesaria para instalarte ollama sería esta:

  ollama:
    volumes:
      - ./ollama:/root/.ollama
    container_name: ollama
    pull_policy: always
    tty: true
    ports:
      - "11434:11434"
    restart: unless-stopped
    image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest}
    deploy:
      resources:
        reservations:
          devices:
            - driver: ${OLLAMA_GPU_DRIVER-nvidia}
              count: ${OLLAMA_GPU_COUNT-1}
              capabilities:
                - gpu

Con esta configuración lo que hacemos es lanzar un servidor ollama accesible desde el puerto 11434 donde podemos usar el tty o el api. No voy a entrar en muchos detalles de cómo usar ollama, pero os recomiendo que le echéis un vistazo porque es la «madre del cordero» o de la llama, en este caso.

Para que esto funcione correctamente con la gpu hay que hacer un par de cositas previamente. Os recomiendo que miréis este repositorio para ver si se ha mejorado/modificado algo: https://github.com/valiantlynx/ollama-docker pero básicamente consiste en ejecutar lo siguiente:

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
  && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
    sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
    sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit

# Configure NVIDIA Container Toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

Dado que vamos a necesitar un interfaz para gestionar los modelos y tener chats y demás lo siguiente que vamos a incluir en nuestro docker-compose es open-webui, modificamos nuestro docker-compose.yml para agregar lo siguiente (yo ya he contruido la imagen y la he subido a docker hub):

  open-webui:
    image: yoprogramo/open-webui:${WEBUI_DOCKER_TAG-latest}
    container_name: open-webui
    volumes:
      - ./open-webui:/app/backend/data
    depends_on:
      - ollama
    ports:
      - ${OPEN_WEBUI_PORT-3000}:8080
    environment:
      - 'OLLAMA_BASE_URL=http://ollama:11434'
      - 'WEBUI_SECRET_KEY='
    extra_hosts:
      - host.docker.internal:host-gateway
    restart: unless-stopped

Y creamos un archivo .env con el siguiente contenido:

OLLAMA_GPU_COUNT=all
SCARF_NO_ANALYTICS=true
DO_NOT_TRACK=true
ANONYMIZED_TELEMETRY=false

Y lanzar las imágenes si todo ha ido bien:

docker compose up -d

Con esto ya tendríamos corriendo nuestro servidor ollama y open-webui en nuestro propio ordenador… Simplemente tenemos que acceder con el navegador a localhost:3000

Lo primero que tenéis que hacer, una vez creado un usuario en el sistema (si, el primer usuario que se crea es administrador) es descargarse algún modelo de IA, para eso hay que entrar en la página de administración y acceder a la opción que pone «Obtener un modelo de Ollama.com», escribir el deseado y darle al botón de la derecha para descargarlo. En la imagen por ejemplo nos descargamos el modelo llama3.1 de 70B (son cerca de 42Gb, así que deberías tener espacio de sobra).

Una vez descargado ya estamos listos para usarlo, vete a la opción «nuevo Chat», selecciona el modelo en el desplegable superior y chatea con tu nueva AI…

En próximas entregas ya entraremos en más cosas que podemos hacer con nuestra IA local, seguro que no nos deja indiferentes.

Depurando PHP con Xdebug y Docker

Hasta hace poco he estado depurando PHP al viejo estilo, poniendo error_log, print_r y demás mensajes aquí y allá. La verdad es que era algo poco dañino y que, si programas bien, tampoco da mucho trabajo. Sin embargo echaba de menos las facilidades de java en el eclipse donde podía depurar de manera increíblemente eficiente sin modificar nada de mi código. El caso es que utilizando docker como base del desarrollo/despliegue de PHP se hacía muy complicado configurar las cosas para depurar. Al final, por circunstancias que no vienen al caso, me animé a ver cómo podríamos depurar código php desplegado usando docker… Y os hago aquí un resumen:

El problema básico

Supongamos que queremos hacer un programa básico en php y desplegarlo para poder verlo en nuestro navegador. Por ahora nos vamos a limitar a este archivo

phpinfo.php

<?php
phpinfo();

Si, es un archivo muy simplón, pero primero tenemos que desplegarlo utilizando docker y verlo en un navegador. Para ello creamos un directorio para nuestro proyecto (llamemosle php-samples, por ejemplo) y dentro del mismo creamos un subdirectorio llamado web y dentro de este directorio creamos el archivo phpinfo.php con el contenido anterior.

La forma más rápida

En el directorio superior creamos un archivo llamado docker-compose.yml con este contenido:

services:
  php:
    image: yoprogramo/php8.2:1.0.2
    volumes:
        - ./web:/var/www/Website
    ports:
        - 8080:80

Ya puestos cargamos el proyecto en visual studio code y nos quedará algo así:

Desde el terminal, o desde el menú de la extensión docker si la tenéis instalada, levantamos el contenedor. Una vez levantado ya podremos acceder a la url:

http://localhost:8080/phpinfo.php

Vale, prueba superada, ya tenemos nuestro servidor ejecutando php, ahora crearemos otro archivo dentro de web con un programa en php un poco más complejo para poder depurarlo a gusto. Vamos a llamar a ese archivo index.php y le daremos este contenido:

<?php
function isPrime($num) {
    if ($num <= 1) return false;
    if ($num == 2) return true;
    if ($num % 2 == 0) return false;
    for ($i = 3; $i <= sqrt($num); $i += 2) {
        if ($num % $i == 0) return false;
    }
    return true;
}

$randomNumber = rand(0, 19);

echo "<table border='1'>";
echo "<tr><th>Número</th><th>Primos menores que $randomNumber</th></tr>";
echo "<tr><td>$randomNumber</td><td>";

for ($i = 0; $i < $randomNumber; $i++) {
    if (isPrime($i)) {
        echo "$i ";
    }
}

echo "</td></tr>";
echo "</table>";
?>

Ahora podemos acceder a la url index.php (o no poner ruta ya que es el archivo que se cargará por defecto)

Vale, pero todo este rollo era para poder depurar el código, para poder hacerlo tenemos que hacer tres cosas:

  • Cambiar la imagen docker por yoprogramo/php8.2:1.0.2-xdebug en el docker-compose.yml y añadir este apartado al servicio php:
extra_hosts:
  - "host.docker.internal:host-gateway"
  • Instalar el plugin «PHP Debug»
  • Configurar el visual studio code para php, para ello pinchamos en el icono de depuración (run and debug) y pinchamos donde pone «create a launch.json file)

Esto nos creará un archivo por defecto donde tendremos que modificar la configuración «Listen for Xdebug» para añadir esto:

"pathMappings": {
   "/var/www/Website": "${workspaceRoot}/web"
}

Pulsamos sobre el icono de iniciar depuración con es configuración (o pulsamos F5)

Con todo configurado solo nos queda reiniciar el docker con la nueva imagen y poner algún breakpoint en el código (eso se hace pinchando a la izquierda del número de línea donde queramos que se pare la ejecución, por ejemplo dentro del bucle de index.php

Ahora volvemos a cargar la página index.php y si todo ha ido bien la ejecución se parará y podremos ver en el editor los valores de las variables

Y continuar la ejecución o hacerlo paso a paso

Si queréis saber cual es el camino difícil o cómo hacerlo con vuestras propias imágenes docker, solo tenéis que preguntar…

Usando una fuente de texto en IoT

Una de las características de los dispositivos que se usan para IoT es la limitación de recursos que tiene. No tienen un sistema operativo disponible y todo lo que queramos que haga hay que programarlo prácticamente de cero. Eso incluye cosas tan peregrinas como definir las fuentes de texto que se utilizarán para escribir en pantalla. Esto es algo que, generalmente, damos por hecho en cualquier otro sistema que programamos, pero que ofrece la oportunidad de ver un poco mejor cómo funciona la informática por dentro.

En principio las pantallas en las que querramos utilizar estas fuentes (en las que queramos escribir algo) son bastante pequeñas, la que vemos en la siguiente imagen, por ejemplo, es de 128×64 pixels (SSD1306):

¿Cómo hacemos para escribir un caracter en una pantalla de estas características? Quitando los comandos propios de inicialización, limpieza y demás, lo único que hacemos es indicar qué pixel queremos que se ilumine y cual no (si hay interés ya desarrollaremos un poco más en profundidad los comandos que se le mandan) y, para ahorrar ciclos y ancho de banda, hay que enviarle los datos en bytes completos (8 bits) y cada bit representa un pixel que se ilumina o no.

Hay software ya desarrollado para convertir fuentes ttf en código c que se puede usar con ciertas librerías, pero vamos a escoger el camino difícil, supongamos que queremos utilizar los primeros 95 caracteres ASCII empezando por el espacio. Serían estos:

Quitando el 127 que no nos interesa porque no es imprimible, una vez decididos los caracteres que queremos imprimir tenemos que decidir el tamaño que queremos utilizar. En este caso queremos que tengan una altura de 8 pixels y, como van a ser de ancho fijo, tendrán una anchura de 8 pixels igualmente. Un carácter se representaría entonces de esta manera:

Y su representación en bits de la primera fila sería 00011000 o lo que es lo mismo 0x18 en hexadecimal. Esta letra, traducida a bytes quedaría: 0x18, 0x3C, 0x66, 0x66, 0x7E, 0x66,0x66,0x00 y con esta información ya sabríamos qué bytes mandar a la pantalla cuando tuviésemos que escribir la letra.

Hacer este proceso para cada letra sería muy pesado, más teniendo en cuenta que son 95 caracteres, pero como nosotros somos programadores os voy a contar un método para extraer estos datos de una mejor manera. Vamos a generar una imagen y vamos a pintar allí las letras que queremos. Para ello abriremos el programa gráfico favorito que tengamos (el mío es gimp) y crearemos una imagen justo del tamaño para que quepan todas nuestras letras (ni más ni menos). En este caso son 95*8 = 760 y 8 de altura, con el fondo negro.

Una vez creada esa imagen, creamos un texto en la fuente que queramos y escribimos los caracteres que queremos:

 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~

Ajustamos para que quepan todos (quitamos alisado y hints varios) y quedará algo así:

Luego cambiamos el tipo de imagen a indexada con 2 colores (solo blanco y negro) y lo exportamos a formato raw:

Esto lo que hace es generarnos un archivo .data que contiene 1 byte por pixel que, como hemos indexado solo contendrá ceros o unos… Y ahora toca programar.

El objetivo es obtener una ristra de bytes juntando los ceros y unos agrupándolos de 8 en ocho. Luego veremos cómo usarlos. Lo más sencillo es hacer un scrip en python, así que escribimos algo así:

def convert_to_hex(byte_data):
    # Agrupamos los datos de 8 en 8
    byte_data = [byte_data[i:i+8] for i in range(0, len(byte_data), 8)]
    # Convertimos cada byte a su representación en hexadecimal
    resu = []
    for i in range(len(byte_data)):
        byte = sum([byte_data[i][j] << j for j in range(8)])
        # Añadimos el valor en hexadecimal a la lista resu
        resu.append(f'0x{byte:02X}')

    return resu

Y ahora solo nos queda leer de un archivo y escribirlo en otro como código c:

def read_binary_file(file_path):
    with open(file_path, 'rb') as file:
        return file.read()

def write_hex_file(hex_data, output_path):
    with open(output_path, 'w') as file:
        # Separamos los valores por comas y ponemos un salto de linea cada 16 valores
        file.write ('static const char font_8x8[] = {\n\t')
        slice_size = 16
        for i in range(0, len(hex_data), slice_size):
            file.write(', '.join(hex_data[i:i+slice_size]))
            file.write(',\n\t')
        file.write('};')

Lo juntamos todo en una función principal pasándole como parámetro el archivo de entrada y el de salida:

def main(input_file, output_file):
    binary_data = read_binary_file(input_file)
    hex_data = convert_to_hex(binary_data)
    write_hex_file(hex_data, output_file)

Y ya podemos ejecutarlo, el resultado sería algo así:

static const char font_8x8[] = {
	0x00, 0x0C, 0x36, 0x36, 0x0C, 0x00, 0x1C, 0x06, 0x18, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60,
	0x3E, 0x0C, 0x1E, 0x1E, 0x38, 0x3F, 0x1C, 0x3F, 0x1E, 0x1E, 0x00, 0x00, 0x18, 0x00, 0x06, 0x1E,
	0x3E, 0x0C, 0x3F, 0x3C, 0x1F, 0x7F, 0x7F, 0x3C, 0x33, 0x1E, 0x78, 0x67, 0x0F, 0x63, 0x63, 0x1C,
	0x3F, 0x1E, 0x3F, 0x1E, 0x3F, 0x33, 0x33, 0x63, 0x63, 0x33, 0x7F, 0x1E, 0x03, 0x1E, 0x08, 0x00,
	0x0C, 0x00, 0x07, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x07, 0x0C, 0x30, 0x07, 0x0E, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x18, 0x07, 0x6E, 0x00,
	0x1E, 0x36, 0x36, 0x3E, 0x63, 0x36, 0x06, 0x0C, 0x0C, 0x66, 0x0C, 0x00, 0x00, 0x00, 0x30, 0x63,
	0x0E, 0x33, 0x33, 0x3C, 0x03, 0x06, 0x33, 0x33, 0x33, 0x0C, 0x0C, 0x0C, 0x00, 0x0C, 0x33, 0x63,
	0x1E, 0x66, 0x66, 0x36, 0x46, 0x46, 0x66, 0x33, 0x0C, 0x30, 0x66, 0x06, 0x77, 0x67, 0x36, 0x66,
	0x33, 0x66, 0x33, 0x2D, 0x33, 0x33, 0x63, 0x63, 0x33, 0x63, 0x06, 0x06, 0x18, 0x1C, 0x00, 0x0C,
	0x00, 0x06, 0x00, 0x30, 0x00, 0x36, 0x00, 0x06, 0x00, 0x00, 0x06, 0x0C, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x18, 0x0C, 0x3B, 0x00, 0x1E,
	0x36, 0x7F, 0x03, 0x33, 0x1C, 0x03, 0x06, 0x18, 0x3C, 0x0C, 0x00, 0x00, 0x00, 0x18, 0x73, 0x0C,
	0x30, 0x30, 0x36, 0x1F, 0x03, 0x30, 0x33, 0x33, 0x0C, 0x0C, 0x06, 0x3F, 0x18, 0x30, 0x7B, 0x33,
	0x66, 0x03, 0x66, 0x16, 0x16, 0x03, 0x33, 0x0C, 0x30, 0x36, 0x06, 0x7F, 0x6F, 0x63, 0x66, 0x33,
	0x66, 0x07, 0x0C, 0x33, 0x33, 0x63, 0x36, 0x33, 0x31, 0x06, 0x0C, 0x18, 0x36, 0x00, 0x18, 0x1E,
	0x06, 0x1E, 0x30, 0x1E, 0x06, 0x6E, 0x36, 0x0E, 0x30, 0x66, 0x0C, 0x33, 0x1F, 0x1E, 0x3B, 0x6E,
	0x3B, 0x3E, 0x3E, 0x33, 0x33, 0x63, 0x63, 0x33, 0x3F, 0x0C, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x00,
	0x36, 0x1E, 0x18, 0x6E, 0x00, 0x06, 0x18, 0xFF, 0x3F, 0x00, 0x3F, 0x00, 0x0C, 0x7B, 0x0C, 0x1C,
	0x1C, 0x33, 0x30, 0x1F, 0x18, 0x1E, 0x3E, 0x00, 0x00, 0x03, 0x00, 0x30, 0x18, 0x7B, 0x33, 0x3E,
	0x03, 0x66, 0x1E, 0x1E, 0x03, 0x3F, 0x0C, 0x30, 0x1E, 0x06, 0x7F, 0x7B, 0x63, 0x3E, 0x33, 0x3E,
	0x0E, 0x0C, 0x33, 0x33, 0x6B, 0x1C, 0x1E, 0x18, 0x06, 0x18, 0x18, 0x63, 0x00, 0x00, 0x30, 0x3E,
	0x33, 0x3E, 0x33, 0x0F, 0x33, 0x6E, 0x0C, 0x30, 0x36, 0x0C, 0x7F, 0x33, 0x33, 0x66, 0x33, 0x6E,
	0x03, 0x0C, 0x33, 0x33, 0x6B, 0x36, 0x33, 0x19, 0x07, 0x00, 0x38, 0x00, 0x00, 0x0C, 0x00, 0x7F,
	0x30, 0x0C, 0x3B, 0x00, 0x06, 0x18, 0x3C, 0x0C, 0x00, 0x00, 0x00, 0x06, 0x6F, 0x0C, 0x06, 0x30,
	0x7F, 0x30, 0x33, 0x0C, 0x33, 0x30, 0x00, 0x0C, 0x06, 0x00, 0x18, 0x0C, 0x7B, 0x3F, 0x66, 0x03,
	0x66, 0x16, 0x16, 0x73, 0x33, 0x0C, 0x33, 0x36, 0x46, 0x6B, 0x73, 0x63, 0x06, 0x3B, 0x36, 0x38,
	0x0C, 0x33, 0x33, 0x7F, 0x1C, 0x0C, 0x4C, 0x06, 0x30, 0x18, 0x00, 0x00, 0x00, 0x3E, 0x66, 0x03,
	0x33, 0x3F, 0x06, 0x33, 0x66, 0x0C, 0x30, 0x1E, 0x0C, 0x7F, 0x33, 0x33, 0x66, 0x33, 0x66, 0x1E,
	0x0C, 0x33, 0x33, 0x7F, 0x1C, 0x33, 0x0C, 0x0C, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x36, 0x1F,
	0x66, 0x33, 0x00, 0x0C, 0x0C, 0x66, 0x0C, 0x0C, 0x00, 0x0C, 0x03, 0x67, 0x0C, 0x33, 0x33, 0x30,
	0x33, 0x33, 0x0C, 0x33, 0x18, 0x0C, 0x0C, 0x0C, 0x3F, 0x0C, 0x00, 0x03, 0x33, 0x66, 0x66, 0x36,
	0x46, 0x06, 0x66, 0x33, 0x0C, 0x33, 0x66, 0x66, 0x63, 0x63, 0x36, 0x06, 0x1E, 0x66, 0x33, 0x0C,
	0x33, 0x1E, 0x77, 0x36, 0x0C, 0x66, 0x06, 0x60, 0x18, 0x00, 0x00, 0x00, 0x33, 0x66, 0x33, 0x33,
	0x03, 0x06, 0x3E, 0x66, 0x0C, 0x33, 0x36, 0x0C, 0x6B, 0x33, 0x33, 0x3E, 0x3E, 0x06, 0x30, 0x2C,
	0x33, 0x1E, 0x7F, 0x36, 0x3E, 0x26, 0x0C, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x36, 0x0C, 0x63,
	0x6E, 0x00, 0x18, 0x06, 0x00, 0x00, 0x0C, 0x00, 0x0C, 0x01, 0x3E, 0x3F, 0x3F, 0x1E, 0x78, 0x1E,
	0x1E, 0x0C, 0x1E, 0x0E, 0x0C, 0x06, 0x18, 0x00, 0x06, 0x0C, 0x1E, 0x33, 0x3F, 0x3C, 0x1F, 0x7F,
	0x0F, 0x7C, 0x33, 0x1E, 0x1E, 0x67, 0x7F, 0x63, 0x63, 0x1C, 0x0F, 0x38, 0x67, 0x1E, 0x1E, 0x3F,
	0x0C, 0x63, 0x63, 0x1E, 0x7F, 0x1E, 0x40, 0x1E, 0x00, 0x00, 0x00, 0x6E, 0x3B, 0x1E, 0x6E, 0x1E,
	0x0F, 0x30, 0x67, 0x1E, 0x33, 0x67, 0x1E, 0x63, 0x33, 0x1E, 0x06, 0x30, 0x0F, 0x1F, 0x18, 0x6E,
	0x0C, 0x36, 0x63, 0x30, 0x3F, 0x38, 0x18, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x1F, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00,
	};

Y ya estaría… ¿Cómo usaríamos esto en nuestra aplicación IoT? Pues básicamente incluiríamos la cabecera font_8x8.h y cuando necesitásemos localizar un carácter ASCII simplemente tendremos que restarle 32 al caracter, multiplicarlo por el número de caracteres de nuestra fuente (95) y ese sería el primer byte del caracter dentro del array. Como vamos a necesitar todas las líneas del caracter podemos hacer una función que las recupere de esta manera:

char getcharslicefrom8x8font(char c, int rowInChar)
{
    return font_8x8[(c - 32) + (rowInChar)*95];
}

Para escribir una cadena completa en una línea habría que localizar la posición inicial de la línea, recuperar todos los bytes de cada caracter que queramos escribir y mandarlos a esa línea de la pantalla (así explicado simplificadamente).

Tenéis todo el código de la conversión de bitmap a c en este repositorio: https://github.com/yoprogramo/font_to_c

Happy coding!

UPDATE: Ya se puede encontrar la fuente así creada en los repositorios de un par de proyectos muy interesantes: https://github.com/fhoedemakers/pico-infonesPlus/ y https://github.com/fhoedemakers/pico-smsplus

Cambiar el agua del aire acondicionado mediante IoT

Si tienes aire acondicionado, uno de los defectos que más se encuentran en estos aparatos es cómo deshacerse del agua de condensación del mismo, especialmente en verano. Si eres como yo o como la mayoría de la gente, lo que harás será poner una botella en algún sitio accesible que se va llenando poco a poco y que, periódicamente, tendrás que cambiar para que no se desborde. Pero, en tiempos calurosos y ajetreados es muy común que la botella (sea del tamaño que sea) termine llenándose y mojemos a quien sea que tengamos debajo del aire acondicionado.

Esta dinámica, que a mi se me antoja muy molesta, la llevo repitiendo en casa y en la oficina (si, en mi oficina tengo un aire acondicionado con botellita) durante varios años y siempre me planteo, ¿y si hago algo para que me avise justo cuando está apunto de llenarse y así puedo cambiarla a tiempo? Hasta el momento no había tenido tiempo, pero ahora he encontrado un ratillo y me he puesto manos a la obra.

Los requisitos eran sencillos, necesito saber si el nivel de agua de una botella ha llegado a cierta altura en la botella, no necesito saber el % exacto, pero si saber si se esta llenando antes de que se llene. Hay algunos sensores de ultrasonidos para depósitos, pero me parecía matar moscas a cañonazos, además, el tamaño de la botella podía ser variable, por lo que el sensor no tendría que ser muy aparatoso. Buscando encontré este:

Puede funcionar a 3.3v o 5v y básicamente devuelve un valor analógico distinto según por donde le llegue el agua en la parte inferior. Este es el enlace de compra en amazon: https://www.amazon.es/dp/B07DJ5FZ31

Por otro lado quería que la solución fuese portable, es decir, que no tuviese que andar echando cables por ahí, por lo que una solución que admitiese baterías también era recomendable. Como plataforma yo estoy muy acostumbrado a usar ESP32 y me hacía mucho más sencillo la parte de conexión a la wifi, así que buscando encontré esto:

Un ESP32-C3 que tiene un puerto para batería que se puede recargar mediante usb, me fue casi más complicado hacerme con las pilas LS16340

Con todos los componentes solo me quedaba conectar el ESP32-C3 al sensor, teniendo cuidado de no usar el pin que se usa para medir el voltaje de la pila (spoiler, es el A0 y lo descubrí después de haberlo soldado). El esquema sería tal que así:

Todo soldado para probar quedaría así:

Y ahora lo interesante, que sería programarla para que podamos controlar el nivel de agua. Como las especificaciones no son siempre exactas lo que hice fue unas pruebas leyendo el nivel del GPIO04 (que tiene conversor A/D) y meter y sacar el sensor del agua imprimiendo por el puerto serie los valores obtenidos:

#define A1 4
void setup() {
  // (Optional)Press reset button
  // on the dev board to see these print statements
  Serial.begin(115200);
  while (!Serial) { }
  // Configuramos el pin A1 como de entrada analógica
  pinMode(A1, INPUT);
  delay (1000);
  Serial.println("Starting...");
}

void loop() {
  int sensorValue = analogRead(A1);
  Serial.println(sensorValue);
  delay(1000);
}

Nota: Esto lo he compilado usando platformio.io (os lo recomiendo mucho) y os dejaré el proyecto completo en un repositorio para que podáis compilarlo vosotros también.

Una vez compilado e instalado el programa veíamos por el puerto serie cada segundo el valor que leíamos del sensor, obteniendo valores entre cero y 3000 (supongo que el rango máximo será Vin que en nuestro caso es 3.3v), así que nuestro valor «umbral» será de 2000 (no se ha llegado todavía arriba del todo pero ya hay agua mojando el sensor).

El siguente paso es conectar a internet para que nos pueda mandar la alerta. Esto en ESP32 es relativamente sencillo:

#include <WiFi.h>

void printWifiStatus() {
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
}
boolean connectWifi (char *ssid, char *pass){
  WiFi.useStaticBuffers(true);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, pass);
  int max = MAXATTEMPS;

  Serial.println("Connecting to WiFi");
  while ((WiFi.status() != WL_CONNECTED) && --max>0) {
    delay(500);
    Serial.print(".");
  }
  if (max == 0)
    return false;
  Serial.println("Connected to WiFi");
  return true;
}

Con esto solo hay que añadir una llamada a connectWifi parándole el SSID y la contraseña de nuestra red wifi para tener conexión. Una vez que tenemos conexión podemos hacer varias cosas para hacernos llegar una alerta, pero la más sencilla para mi era hacer que me enviase un mensaje por telegram. Para ello habría que crear un bot (hablando con botfather) y obtener el token necesario para poder usarlo. Luego he creado un canal y he puesto a este bot como administrador, con lo que para utilizar este bot solo tendría que hacer estos defines:

#define BOTTOKEN "7413559559:AAHmlkmiubcvtxZO0j0nqITZjByjrS1Ah3U"
#define CHAT_ID "-1002245988095"

y usar la librería Universal-Arduino-Telegram-bot, que podéis encontrar en https://github.com/witnessmenow/Universal-Arduino-Telegram-Bot de esta manera:

#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>

WiFiClientSecure client;
UniversalTelegramBot bot(BOTTOKEN, client);
client.setCACert(TELEGRAM_CERTIFICATE_ROOT);

bot.sendMessage(CHAT_ID, "esto es un mensaje");

Así que la lógica del programa cambia ahora a que cuando el valor detectado sea mayor que 2000 enviemos un mensaje con el bot para alertarnos.

El programa lo he mejorado para leer el valor de voltaje de la batería y para que entre en suspensión y se despierte cada 10 segundos para ahorrar energía, pero eso podéis sacarlo de los ejemplos que hay en el repositorio https://github.com/Xinyuan-LilyGO/LilyGo-T-OI-PLUS. El caso es que ahora tenía que hacer el montaje final en mi botella de agua del aire acondicionado. Elegí un bote de estos gigantes de proteinas (si, alguien en casa va al gimnasio) y tras impermeabilizar adecuadamente los cables y las conexiones decidí meter el sensor por la parte superior del tapón y así antes de llegar arriba del todo haría el contacto adecuado y quedó tal que así:

Y ya con el tubo del aire metido y todo:

Si queréis ver el resultado final (probando con una taza con agua, que no voy a esperar que se llene el bidón este) os dejo aquí un video:

El repositorio con el código inicial podéis encontrarlo en: https://github.com/yoprogramo/waterlevel

Grabar y emular un ratón en linux

Hoy me tocó un trabajo muy ingrato, tenía que reenviar de una cuenta de correo a otra todos los correos recibidos en dos meses. El correo estaba gestionado en una cuenta de outlook y ya tenía como 2000 correos acumulados. En principio suponía que sería sencillo, selecciono todos y reenvio… Pues no, outlook no te deja hacerlo así. El origen de la tarea era porque una regla de reenvío, que lo hace automáticamente, había estado deshabilitada un tiempo y, desafortunadamente, tampoco hay forma de hacerla correr solo para un conjunto determinado de correos, o todos, o los que entran, nada de reglas «personalizadas».

Conseguí crear una acción rápida para enviar el correo y mover el mensaje a la carpeta de procesados (para tenerlos ordenaditos), pero las acciones de outlook no te mandan los mensajes automáticamente sino que te generan un borrador con el destinatario y tu tienes que dar al botón enviar. Así que ahora tenía que dar dos clicks por cada mensaje… Solo son 2000!! ESto podía llevarme muuuucho tiempo.

¿Probamos a bajarlos por imap o algo? Pueeee no, microsoft se ha puesto muy borde con la autenticación del correo y no iba a ser sencillo, así que, para no tirarme toda una mañana (o todo un día) haciendo clicks y creandome un tunel carpiano inmenso tiré de google y encontré un repositorio de alguien que te permite grabar lo que haces con el ratón y repetirlo (aunque era en el navegador el uso de selenium me daba mucha pereza). En concreto el repositorio es este:

https://github.com/RMPR/atbswp

Y las instrucciones «rápidas» para instalarlo en un ubuntu serían estas:

sudo apt install git python3-dev python3-tk python3-setuptools python3-wheel python3-pip python3-wxgtk4.0
git clone https://github.com/RMPR/atbswp.git && cd atbswp
python3 -m pip install pyautogui pynput --user
python3 atbswp/atbswp.py

Una vez instalado y lanzado solo hay que grabar la secuencia que queramos (empezamos dando al botón de grabar o al f9 y terminamos igualmente). Esta secuencia se puede guardar en un archivo para ejecutarla posteriormente (o editarla a mano como hice yo) y luego se puede cargar y ejecutar un número determinado de veces.

Dicho y hecho, registro mis dos clicks y digo que se ejecuten 2000 veces… Ahora solo queda esperar a que termine!

Eso si, el ordenador no puedo usarlo mientras está haciendo mi trabajo… Bueno, será por ordenadores… Me ha dado tiempo a escribir esta entrada y todo mientras redirijo correos.