devicescript-multitarea-concurrencia

Concurrencia y multitarea en DeviceScript

  • 5 min

La concurrencia en DeviceScript permite intercalar varias tareas sobre un mismo runtime cuando estas ceden el control mediante una espera asíncrona.

Tenéis un código que lee un sensor cada 5 segundos y manda datos por WiFi. De repente, queréis añadir un botón para encender una pantalla. Problema, si el código está ocupado mandando el WiFi o esperando los 5 segundos (delay(5000)), pulsáis el botón y… nada. El sistema está sordo.

Para arreglar esto en C++ clásico, recurrimos a máquinas de estados, contadores de tiempo (millis()) y estructuras complejas. El código se vuelve un espagueti difícil de mantener.

DeviceScript combina servicios, suscripciones y async/await para organizar estas tareas. No es paralelismo ni un sistema de tiempo real: una función que no cede el control puede bloquear a las demás.

Cómo colaboran los servicios y el runtime

Como vimos en el artículo anterior, un servicio como Button o LightBulb abstrae una capacidad del hardware.

El driver se ocupa de la interacción con el hardware y notifica cambios a través del servicio. Esto separa responsabilidades, aunque todo sigue condicionado por el runtime y los recursos del microcontrolador.

  • Cuando tú dices button.down.subscribe(...), no estás comprobando el botón. Estás diciéndole al Driver: “Tú vigila el pin, y avísame si pasa algo”.
  • El driver vigila el pin mediante el mecanismo adecuado para esa implementación.
  • Tu código principal queda libre para hacer otras cosas.

Esta separación cliente-servidor permite organizar varias operaciones concurrentes sin mezclar toda la lógica en un único bucle.

Ejemplo: el semáforo y la sirena

Vamos a imaginar un escenario clásico de multitarea:

  1. Tarea A (Lenta): Un semáforo que cambia de color cada 2 segundos.
  2. Tarea B (Rápida): Una luz de emergencia (strobo) que parpadea muy rápido (100ms) pero solo si apretamos un botón.

En un bucle único lineal, coordinar estos tiempos (2000ms vs 100ms) sería un dolor de cabeza matemático. En DeviceScript, simplemente describimos las dos tareas por separado.

import { delay } from "@devicescript/core"
import { startLightBulb, startButton } from "@devicescript/servers"
import { pins } from "@dsboard/esp32_c3_devkit_m"

// --- Configuración del Hardware ---
// Semáforo (digamos que es un LED Rojo)
const semaforo = startLightBulb({ pin: pins.GPIO2, roleName: "semaforo" })
// Luz de emergencia (un LED Azul)
const emergencia = startLightBulb({ pin: pins.GPIO3, roleName: "emergencia" })
// Botón de activación
const boton = startButton({ pin: pins.GPIO9 })

// --- TAREA 1: El Semáforo (Bucle Infinito Independiente) ---
async function cicloSemaforo() {
    while (true) {
        // Rojo
        await semaforo.intensity.write(1)
        await delay(2000) // Pausa de 2 segundos
        
        // Apagado (Simulando cambio)
        await semaforo.intensity.write(0)
        await delay(2000)
    }
}

// --- TAREA 2: Sistema de Emergencia (Reactivo) ---
// Variable global para controlar el estado
let modoEmergencia = false

// Escuchamos el botón para activar/desactivar
boton.down.subscribe(() => {
    modoEmergencia = !modoEmergencia
    console.log(`Modo Emergencia: ${modoEmergencia}`)
})

// Bucle de la luz de emergencia
async function cicloEmergencia() {
    while (true) {
        if (modoEmergencia) {
            // Parpadeo rápido (Strobo)
            await emergencia.intensity.write(1)
            await delay(100)
            await emergencia.intensity.write(0)
            await delay(100)
        } else {
            // Si no hay emergencia, esperamos un poco para no saturar CPU
            await emergencia.intensity.write(0)
            await delay(500)
        }
    }
}

// --- ARRANQUE ---
console.log("Iniciando sistema multitarea...")

// Lanzamos las dos funciones. NO ponemos 'await' delante de la llamada,
// porque queremos que se ejecuten "en el fondo", no esperar a que terminen.
cicloSemaforo()
cicloEmergencia()

// El programa principal "termina" aquí, pero las tareas siguen rodando.
Copied!

Qué ocurre durante la ejecución

Al ejecutar el ejemplo en el simulador:

  1. El semáforo parpadeará lentamente.
  2. Al pulsar el botón, la luz de emergencia comenzará a parpadear rápidamente.
  3. Ninguna afecta a la otra. El delay(2000) del semáforo no bloquea el parpadeo rápido de la emergencia. El parpadeo rápido no acelera el semáforo.

Esto ocurre porque await delay() libera el procesador. DeviceScript aprovecha esos huecos para saltar de una función a otra (Concurrencia Cooperativa).

Ventajas de separar las tareas

Ahora que lo hemos visto funcionar, podemos formalizar por qué esto es superior:

Ritmos independientes

Cada servicio maneja su propio tiempo. Un sensor puede leer cada 10 minutos (temperatura) y otro cada 10 milisegundos (acelerómetro). No tienes que buscar el “mínimo común múltiplo” para tu delay principal.

Aislamiento de la lógica

Una operación asíncrona puede dejar avanzar otras tareas mientras espera. Sin embargo, un driver bloqueado, un bucle sin await o el agotamiento de recursos todavía pueden afectar a todo el dispositivo.

Código modular

¿Quieres añadir un tercer LED que respire (fading)? Creas una función async function respirar(), la lanzas al inicio y listo. No tienes que tocar ni una coma de las funciones cicloSemaforo o cicloEmergencia.