Receive command from App
When the user interacts with a Controller on the dashboard — taps a button, drags a slider, picks a segment — the library calls a block of code at global scope. You declare those blocks with the I<Widget>("widget_id") { ... } macro and the WHEN_* events you care about. No registration in setup(), no callback wiring — the macro handles it.
ISimpleButton("btn1") {
WHEN_PRESSED { Serial.println("pressed"); }
};
Five lines. Drop it at file scope. The library calls it whenever the user presses the Simple Button with Widget ID btn1. That’s the whole pattern — the rest of this page is the catalog.
Event reference
| Widget category | Macro(s) | Available events |
|---|---|---|
| Buttons | ISimpleButton, IAdvancedButton, IEmergencyButton | WHEN_PRESSED, WHEN_RELEASED, WHEN_LONG_PRESSED, WHEN_TOGGLED(isOn) |
| Direction Pad | IDirectionPad | WHEN_PAD_PRESSED(btn), WHEN_PAD_RELEASED(btn), WHEN_PAD_LONG_PRESSED(btn) |
| Sliders | IHorizontalSlider, IVerticalSlider | WHEN_CHANGING(v), WHEN_CHANGED(v) |
| Joystick | IJoystick | WHEN_MOVED(x, y), WHEN_RELEASED |
| Switch | ISwitch | WHEN_TURNED_ON, WHEN_TURNED_OFF, WHEN_SWITCH_SET(isOn) |
| Segmented Switch | ISegmentedSwitch | WHEN_SELECTION_CHANGED(idx), WHEN_SEGMENT_SELECTED(idx), WHEN_SEGMENT_DESELECTED(idx) |
Buttons
Simple Button, Advanced Button, and Emergency Button share the same event model. They all support press, release, long-press, and toggle. The visual differences (icons on Advanced, safety filters on Emergency) live in the app — the macros in your sketch are identical.
ISimpleButton("btn1") {
WHEN_TOGGLED(isOn) {
digitalWrite(LED_PIN, isOn ? HIGH : LOW);
}
WHEN_LONG_PRESSED {
Serial.println("held");
}
};
| Event | When it fires | Captures |
|---|---|---|
WHEN_PRESSED | Touch starts | — |
WHEN_RELEASED | Touch ends | — |
WHEN_LONG_PRESSED | Held past the Long press duration (Momentary mode only) | — |
WHEN_TOGGLED(isOn) | Each tap (Toggle mode only) | bool isOn |
For visual configuration (mode, labels, colors, icons, safety filters) see Widget in app / Simple Button, Advanced Button, Emergency Button.
Direction Pad
Direction Pad reports per-button events. The same macro fires for any button on the pad — your code switches on the captured enum to decide what to do for each direction.
IDirectionPad("pad1") {
WHEN_PAD_PRESSED(btn) {
switch (btn) {
case DPadButton::Up: moveForward(); break;
case DPadButton::Down: moveBackward(); break;
case DPadButton::Left: turnLeft(); break;
case DPadButton::Right: turnRight(); break;
case DPadButton::Center: stop(); break;
default: break;
}
}
};
| Event | When it fires | Captures |
|---|---|---|
WHEN_PAD_PRESSED(btn) | Any pad button is pressed | DPadButton btn |
WHEN_PAD_RELEASED(btn) | Any pad button is released | DPadButton btn |
WHEN_PAD_LONG_PRESSED(btn) | A pad button is held past the long-press threshold | DPadButton btn |
The enum values are DPadButton::Up, Down, Left, Right, Center. Pad styles that don’t have a center button (Diamond, Dual, PS) simply never fire Center.
For visual configuration see Widget in app / Direction Pad.
Sliders
Horizontal Slider and Vertical Slider share the same event model. You choose between live updates (every position change while dragging) and final updates (one event when the user releases).
IHorizontalSlider("slider1") {
WHEN_CHANGING(v) {
analogWrite(LED_PIN, map((int)v, 0, 100, 0, 255));
}
WHEN_CHANGED(v) {
Serial.print("final ");
Serial.println(v);
}
};
| Event | When it fires | Captures |
|---|---|---|
WHEN_CHANGING(v) | Continuously while the user drags | float v |
WHEN_CHANGED(v) | Once when the user releases | float v |
Use WHEN_CHANGING for live feedback (a dimmer that follows the finger), WHEN_CHANGED for set-and-forget (a setpoint that applies on release). For high-rate hardware (PWM, audio), WHEN_CHANGED avoids saturating the link with intermediate positions.
For visual configuration see Widget in app / Horizontal Slider, Vertical Slider.
Joystick
Joystick reports the X and Y position together, both normalized to -100..+100. Center is (0, 0).
IJoystick("joy1") {
WHEN_MOVED(x, y) {
int left = constrain((int)(y + x), -100, 100);
int right = constrain((int)(y - x), -100, 100);
setMotors(left, right);
}
WHEN_RELEASED {
setMotors(0, 0);
}
};
| Event | When it fires | Captures |
|---|---|---|
WHEN_MOVED(x, y) | The handle moves | float x, float y, both in -100..+100 |
WHEN_RELEASED | The user lifts the handle | — |
In Sticky mode (the default in the app), the handle springs back to center on release and your sketch sees a final WHEN_RELEASED. In Free mode the handle stays where the user left it; WHEN_RELEASED still fires when the touch ends — your sketch decides whether to stop motion or hold the last command.
For visual configuration see Widget in app / Joystick.
Switch
Switch holds a boolean state. Listen on either edge with WHEN_TURNED_ON and WHEN_TURNED_OFF, or react to every change with WHEN_SWITCH_SET(isOn).
ISwitch("light") {
WHEN_TURNED_ON { digitalWrite(RELAY, HIGH); }
WHEN_TURNED_OFF { digitalWrite(RELAY, LOW); }
WHEN_SWITCH_SET(isOn) {
Serial.print("light → ");
Serial.println(isOn ? "ON" : "OFF");
}
};
| Event | When it fires | Captures |
|---|---|---|
WHEN_TURNED_ON | OFF → ON edge | — |
WHEN_TURNED_OFF | ON → OFF edge | — |
WHEN_SWITCH_SET(isOn) | Any change | bool isOn |
You can mix all three in the same block — they don’t conflict. The edge events run before WHEN_SWITCH_SET for the same change.
For visual configuration see Widget in app / Switch.
Segmented Switch
Segmented Switch behaves differently in Single mode and Multiple mode. The events you write match the mode set in the app.
In Single mode (one segment selected at a time), use WHEN_SELECTION_CHANGED(idx):
ISegmentedSwitch("mode1") {
WHEN_SELECTION_CHANGED(idx) {
switch (idx) {
case 0: setOff(); break;
case 1: setAuto(); break;
case 2: setManual(); break;
}
}
};
In Multiple mode (any combination of segments selected), use WHEN_SEGMENT_SELECTED(idx) and WHEN_SEGMENT_DESELECTED(idx):
ISegmentedSwitch("zones") {
WHEN_SEGMENT_SELECTED(idx) { enableZone(idx); }
WHEN_SEGMENT_DESELECTED(idx) { disableZone(idx); }
};
| Event | When it fires | Captures |
|---|---|---|
WHEN_SELECTION_CHANGED(idx) | The selected segment changes (Single mode) | uint8_t idx |
WHEN_SEGMENT_SELECTED(idx) | A segment becomes selected (Multiple mode) | uint8_t idx |
WHEN_SEGMENT_DESELECTED(idx) | A segment becomes unselected (Multiple mode) | uint8_t idx |
Indices are 0-based and match the order of options in the app’s Inspector.
For visual configuration see Widget in app / Segmented Switch.
Notes
- All
WHEN_*blocks run on the main thread, between calls toinstant.loop(). Don’t block — heavy work freezes the connection. If you need to run something long, set a flag and handle it inloop()outside the block. - You can write events for a mode you don’t use (for example
WHEN_LONG_PRESSEDin Toggle mode) — they simply never fire. No compile-time check ties an event to a specific mode. - Multiple
I<Widget>("id") { ... }blocks for the same widget ID are not supported — declare each widget once.
Next
→ Send data to App — The reverse direction: pushing values from your sketch to a Display.
→ Setup — How to declare your instant object and call begin() / loop().
→ Examples — Complete sketches using these macros end to end.