devicescript-multitarea-concurrencia

Concurrency and Multitasking in DeviceScript

  • 5 min

Concurrency in DeviceScript allows interleaving multiple tasks on the same runtime when they yield control through asynchronous waits.

You have code that reads a sensor every 5 seconds and sends data via WiFi. Suddenly, you want to add a button to turn on a screen. Problem: if the code is busy sending WiFi data or waiting for the 5 seconds (delay(5000)), you press the button and… nothing happens. The system is deaf.

To fix this in classic C++, we resort to state machines, time counters (millis()), and complex structures. The code becomes a tangled mess that’s hard to maintain.

DeviceScript combines services, subscriptions, and async/await to organize these tasks. It’s not parallelism or a real-time system: a function that doesn’t yield control can block all others.

How Services and the Runtime Collaborate

As we saw in the previous article, a service like Button or LightBulb abstracts a hardware capability.

The driver handles interaction with the hardware and notifies changes through the service. This separates responsibilities, although everything is still conditioned by the runtime and the microcontroller’s resources.

  • When you say button.down.subscribe(...), you are not polling the button. You are telling the Driver: “You watch the pin, and let me know if something happens”.
  • The driver monitors the pin using the appropriate mechanism for that implementation.
  • Your main code remains free to do other things.

This client-server separation allows organizing multiple concurrent operations without mixing all the logic in a single loop.

Example: The Traffic Light and the Siren

Let’s imagine a classic multitasking scenario:

  1. Task A (Slow): A traffic light that changes color every 2 seconds.
  2. Task B (Fast): An emergency light (strobe) that flashes very quickly (100ms) but only if we press a button.

In a single linear loop, coordinating these timings (2000ms vs 100ms) would be a mathematical headache. In DeviceScript, we simply describe the two tasks separately.

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

// --- Hardware Configuration ---
// Traffic light (let's say it's a Red LED)
const semaforo = startLightBulb({ pin: pins.GPIO2, roleName: "semaforo" })
// Emergency light (a Blue LED)
const emergencia = startLightBulb({ pin: pins.GPIO3, roleName: "emergencia" })
// Activation button
const boton = startButton({ pin: pins.GPIO9 })

// --- TASK 1: The Traffic Light (Independent Infinite Loop) ---
async function cicloSemaforo() {
    while (true) {
        // Red
        await semaforo.intensity.write(1)
        await delay(2000) // 2-second pause
        
        // Off (Simulating change)
        await semaforo.intensity.write(0)
        await delay(2000)
    }
}

// --- TASK 2: Emergency System (Reactive) ---
// Global variable to control state
let modoEmergencia = false

// Listen to the button to activate/deactivate
boton.down.subscribe(() => {
    modoEmergencia = !modoEmergencia
    console.log(`Emergency Mode: ${modoEmergencia}`)
})

// Emergency light loop
async function cicloEmergencia() {
    while (true) {
        if (modoEmergencia) {
            // Fast flashing (Strobe)
            await emergencia.intensity.write(1)
            await delay(100)
            await emergencia.intensity.write(0)
            await delay(100)
        } else {
            // If no emergency, wait a bit to avoid saturating CPU
            await emergencia.intensity.write(0)
            await delay(500)
        }
    }
}

// --- STARTUP ---
console.log("Starting multitasking system...")

// Launch both functions. We do NOT put 'await' in front of the call,
// because we want them to run "in the background", not wait for them to finish.
cicloSemaforo()
cicloEmergencia()

// The main program "ends" here, but the tasks keep running.
Copied!

What Happens During Execution

Running the example in the simulator:

  1. The traffic light will blink slowly.
  2. When you press the button, the emergency light will start blinking rapidly.
  3. Neither affects the other. The delay(2000) from the traffic light does not block the rapid blinking of the emergency light. The rapid blinking does not speed up the traffic light.

This happens because await delay() releases the processor. DeviceScript takes advantage of those gaps to jump from one function to another (Cooperative Concurrency).

Advantages of Separating Tasks

Now that we’ve seen it work, we can formalize why this is superior:

Independent Rhythms

Each service manages its own time. One sensor can read every 10 minutes (temperature) and another every 10 milliseconds (accelerometer). You don’t have to find the “least common multiple” for your main delay.

Logic Isolation

An asynchronous operation can let other tasks advance while waiting. However, a blocked driver, a loop without await, or resource exhaustion can still affect the entire device.

Modular Code

Want to add a third LED that fades? Create an async function respirar(), launch it at startup, and you’re done. You don’t have to touch a single comma in the cicloSemaforo or cicloEmergencia functions.