A digital input allows detecting two electrical states and reacting when they change. A button is the simplest example: it can be pressed or released.
In a simple Arduino program, reading a button usually involves constantly polling it in the main loop. This technique is called polling.
It works, yes, but it is inefficient. If your processor is busy performing complex calculations or waiting for a WiFi response, it might miss the press exactly when the user pressed the button.
DeviceScript, being based on TypeScript, brings the asynchronous and event-driven model. Instead of asking all the time, we tell the system: “Let me know when someone presses the button”.
Today we will see how to use the Button service, how to subscribe to events, and how to finally forget about the annoying bounce or debounce.
The Button service
Just like with lights (LightBulb), in DeviceScript we usually don’t read the GPIO “raw” (gpio.read()), but instead start a button driver.
This abstracts us from the electronics. We don’t care if the button is Active Low (connects to GND) or Active High (connects to VCC), nor if it has a Pull-Up or Pull-Down resistor. The driver handles normalizing all of that so we only receive “Pressed” or “Released”.
To use it, we import startButton:
import { pins } from "@dsboard/esp32_c3_devkit_m"
import { startButton } from "@devicescript/servers"
// Configure a button on GPIO 9
const miBoton = startButton({
pin: pins.GPIO9,
activeHigh: false // false = The button connects to GND (internal Pull-Up)
})By default, if we don’t specify anything, DeviceScript usually assumes we are using internal Pull-Up (the button connects the pin to GND), which is the most common configuration in maker electronics.
Subscribing to events
Here comes the mindset shift. Instead of an if (button.pressed) inside a setInterval, we are going to subscribe to an event.
The Button service exposes several key events:
down: Fires once when the button is pressed.up: Fires once when the button is released.hold: Fires if the button is held down for a certain amount of time.
The syntax is wonderful:
// Executes this function EVERY TIME the button is pressed
miBoton.down.subscribe(async () => {
console.log("Button pressed!")
// Your logic here: turn on an LED, send an MQTT message, etc.
})
// Executes this function when the button is released
miBoton.up.subscribe(async () => {
console.log("Button released")
})Advantages over polling
- Energy Efficiency: If there are no presses, the processor can “sleep” or dedicate itself to other tasks.
- Non-blocking: Your main code can be doing an HTTP request or a screen animation. The press will be processed as soon as it occurs (or queued in the event loop).
- Cleanliness: The code is much more readable. You define the action associated with the event, and you’re done.
Filtering bounce
Anyone who has connected a physical pushbutton to an Arduino knows that mechanical switches are “noisy.” When pressed, the metal contacts bounce microscopically, generating a burst of very fast on-off signals before stabilizing.
In Arduino, we used to fix this with delay(50) or complex libraries.
In DeviceScript, the startButton driver already implements software debouncing.
When you subscribe to down, the system filters those spurious signals. You receive a clean event. If you need to adjust the sensitivity, the driver accepts configuration parameters, but the default values work perfectly for standard pushbuttons.
Example: Light switch
Let’s combine what we learned in the previous lesson (LEDs) with this one. We’ll create a classic “hallway switch” system: you press the button and the light toggles.
import { pins } from "@dsboard/esp32_c3_devkit_m"
import { startButton, startLightBulb } from "@devicescript/servers"
// 1. Configure Hardware (Services)
const btn = startButton({ pin: pins.GPIO9 }) // Button on GPIO 9
const luz = startLightBulb({ pin: pins.GPIO2 }) // LED on GPIO 2
console.log("System ready. Press the button.")
// 2. Reactive Logic
btn.down.subscribe(async () => {
// Read the current light state (0 or 1)
const intensidadActual = await luz.intensity.read()
// Calculate the new state (the opposite)
const nuevaIntensidad = intensidadActual > 0 ? 0 : 1
// Apply it
await luz.intensity.write(nuevaIntensidad)
console.log(`Light changed to: ${nuevaIntensidad}`)
})Copy the code, start the simulator, and press the virtual button on the dashboard. The virtual LED will toggle state with each press.
There is no explicit main loop like setInterval or while (true). The subscription remains active, and the runtime invokes the handler when it receives the event.
Testing the button in the simulator
When we use startButton, the dashboard shows a button control.
- A quick click simulates a normal press.
- Holding down the control simulates a long press, useful for testing the
holdevent.
It’s incredibly useful for debugging “long press” logic without having to physically press a button on the breadboard all the time.