Overview
- Etheras Microscript is an embedded automation language for ESP32-based controllers, meant to run inside the device firmware as lightweight logic.
- Keywords and built-ins are lowercase and case-sensitive (example:
temp()). - Integer and float runtime; non-zero is true, zero is false. String literals are only allowed as direct arguments to built-ins like
log,publish, ornotify. - Top-level statements run once at script load. Scheduled blocks (
every,after,onmqtt,ontrigger) register tasks that execute later.macrodefines reusable macros expanded inline, whilefuncdefines reusable runtime functions. - Comments:
// lineor/* block */(no nested block comments). - Avoid naming variables after built-ins (e.g., do not use
nowas a variable when callingnow()). - Inside
macromacros, use_-prefixed locals (e.g.,_bit) to avoid variable collisions across expansions. - Error reporting note: macro expansion changes line/col positions, so when macros are present the UI shows a
near: ...snippet instead of strict line/col to help you locate the issue. - Avoid shared temporaries inside
delayblocks; ensure guard/value expressions remain stable until the delay fires. - Scripts rely on underlying firmware services, including multithreaded execution, automatic updates, webserver hosting, MQTT and LoRaWAN connectivity, RS485 and Modbus protocols, and NVS flash access for persistence.
- ThingsBoard is the main platform for provisioning, data ingestion, dashboards, and remote control of scripts and devices.
- Keep scripts small (target < 5000 bytes). macro expansion repeats the body at each call, so large macros used many times can exceed memory limits during save/validation.
if (cond) { ... }, for (i = a; i <= b; i = i + s) { ... } or for (i = a; i < b; i = i + s) { ... }, and switch (expr) { case N: { ... } }. Parentheses and braces are required.
Syntax & Operators
Syntax note: commands use parentheses consistently (e.g., every(1000), load(name, default), publish(...), logn(...)). The only exception is output assignment, which remains out(index) = value.
Expressions
- Arithmetic:
+-*/%(division and modulo return 0 on divide-by-zero). - Comparison:
==!=<<=>>=(yield 1 or 0). - Logical:
and/&&,xor/^^,or/||,not/!. - Bitwise:
~<<>>&|^(C++ precedence). - Conditional:
cond ? expr1 : expr2(C-style; lowest precedence). - Parentheses force evaluation order and are valid anywhere an expression is expected.
Built-in Calls in Expressions
in(n),ain(n),out(n),actuator(n),sensor(n).- Edge detection:
p_edge(in(n)),n_edge(in(n)),edge(in(n)). - Environmental:
temp(),hum(). - Scaling:
scale(x, inMin, inMax, outMin, outMax). - Control:
pid("id", sp, pv, kp, ki, kd, dt_ms, out_min, out_max). - Numeric helpers:
abs(x),tofloat(x),toint(x),round(x),floor(x),ceil(x). - Time:
now(),nowutc(),hour(),min(),dow(). log,logn, andpublishare statements but also usable inside expressions for argument evaluation.
Variables & Persistence
- Assignment creates variables implicitly:
counter = counter + 1. All vars start at 0 until assigned. - Variables can be integers or floats. When an integer is required (indices, timers, persistence), floats are rounded on load.
- Arrays are declared at the top of the script with
int_arrayandfloat_array(max 400 elements, max 10 arrays, total elements across arrays limited to 400). - For tight timing loops, pre-declare variables near the top (e.g.,
counter = 0) to avoid a one-time runtime allocation on first use. load(name, default_exprpulls a stored value or usesdefault_exprwhen missing (does not save automatically). )
save(name)writes the current value to non-volatile storage; call it whenever you intentionally update a persisted variable.- The parser expects a write (
=,load, orfor) before the first read of a variable.
// Persistent counter template
load(counter, 0);
every(1000) {
counter = counter + 1;
save(counter);
}
I/O & Side Effects
Digital & Analog
in(idx),ain(idx): read inputs.out(idx): read output state.out(idx) = expr: set output.toggle(idx): flip an output.- I/O indices are 1-based in scripts (e.g.,
in(1)is the first input).
Actuators & Sensors
actuator(idx),sensor(idx)read host-provided channels.actuator(idx) = exprwrites an actuator.rgb_led(value)sets the status RGB LED using a 3-bit mask (0..7).lcd(color, line [, arg1 ...])writes LCD text;line=0clears the whole LCD after setting color.- Edge helpers:
p_edge,n_edge,edgedetect transitions.
Environment
EMS Web Widgets
wpage("label")creates/resets the EMS page and sets its top title.wswitch(target, "label")renders a toggle that writes 0/1.wbutton(target, "label")renders a button that writes 1 when pressed.wlamp(expr, "label")shows green/red from expression value.winput(target, "label")accepts numeric text input.woutput(expr, "label")displays a numeric expression value.wline()moves subsequent widgets to the next row.
Control Flow
if (condition) { ... } [else { ... }]; parentheses and braces are required.else ifis supported; nesting insideelsealso works if you prefer.for (var = start; var <= end; var = var + step) { ... }; counted loop with<=/<for ascending or>=/>for descending bounds. Step 0 is coerced to 1.switch (expr) { case N: { ... } default: { ... } };caseuses integer literals, fall-through is supported, anddefaultis optional.breakexits the innermost for loop or switch.continueskips to the next for iteration.returnreturns fromfunc.nextexits the current scheduled/event block run early.delay(ms [, guard]) { ... }schedules a one-shot execution aftermswithout blocking the current block; if guard is provided, it must still be true when the delay fires.
if (dry) {
out(1)=0;
}
else{
if (p<target) { out(1)=1; }
else{ out(1)=0; }
}
for (i = 1; i <= 4; i = i + 1) {
out(i) = in(i);
}
Scheduling
every(interval_ms) { ... }: repeating task with drift correction. Negative interval registers a trigger-only task (no automatic runs).- Name tasks:
every(60000, "telemetry") { ... }and trigger them manually withtrigger("telemetry"), which also resets the next due time. ontrigger("label") { ... }: trigger-only task that runs only when you calltrigger("label").after(delay_ms) { ... }: one-shot delayed execution.delay(ms[,guard]){...}: inline one-shot scheduled from inside another block (does not block the parent); optional guard re-checks at fire time.next;inside scheduled/event blocks (every/after/onmqtt/onlora/ontrigger/delay) aborts the rest of that block for the current run only.macro Name(...): compile-time macro expanded inline at call sites.func Name(...): runtime function with optional return value and output params.
Timing note: with a 100 ms cycle the observed jitter is about 1%. For more deterministic timing, avoid every intervals below 100 ms.
every(5000, "status") {
publish("plc/status", "temp", temp(), "pressure", ain(1));
}
onmqtt("force_status") {
trigger("status");
}
Timers & Edges
ton("id", cond, preset_ms [, retentive]): on-delay. Returns 1 aftercondstays true forpreset_ms. With retentive non-zero, state persists untilt_reset.tof("id", cond, preset_ms [, retentive]): off-delay. Remains 1 forpreset_msaftercondfalls.t_reset("id"): clears timer state.- Edges:
p_edge(in(n))(rising),n_edge(in(n))(falling),edge(in(n))(any change).
// Dry-run alarm with retentive on-delay
if (ton("dry_run", w<DRY && pump, 10000, 1)) {
publish("alarm","DRY_RUN");
}
if (p_edge(in(8)) && w>=DRY) { t_reset("dry_run"); }
MQTT Handlers
onmqtt("key") { ... }runs when firmware delivers a matching key. Use"*"to receive all messages.- Attribute payloads containing multiple
_scriptentries are fanned out per entry before delivery. - If no key matches, no
onmqttblock is executed. - Helper variables available inside handlers:
msg_is_onis 1 for true/on payloads, else 0.msg_is_offis 1 for false/off payloads, else 0.msg_is_numberis 1 for numeric payloads, else 0.msg_valueis numeric: true/on -> 1, false/off -> 0, numeric payload -> parsed value. On parse failure, previous value is kept.
load(setpoint, 220);
onmqtt("plc_sp") {
if (msg_is_number) {
setpoint = msg_value;
save(setpoint);
}
}
onmqtt("plc_output") {
if (msg_is_number) { out(1)= (msg_value != 0); }
}
Logging & Publishing
log(arg1, ...)concatenates arguments without a newline;logn(...)appends a newline.publish("topic", arg1, ...)sends a variable/value payload to the firmware:- Single argument: sends the literal string or numeric value.
- Alternating
"key", valuepairs form a JSON object. - Unlabeled values become a JSON array.
- Quoted markers
"$NAME","$MACADDR","$IPADDR"are replaced at publish send-time.
notify(message)sends a notification message (max 32 chars) using the default MQTT path or the configured Telegram bot credentials, depending on the Messaging tab setting.ioreport()emits an IO report event with the current device report payload.
log("Pressure: ", p, " temp: ", temp());
logn("Done.");
publish("status",;
"pressure", p,;
"battery", ain(1),;
"water", ain(2));
Patterns & Examples
One-second status loop
every(1000) {
t = temp();
if (t >= desired) {
out(3) = 1;
log("Heater ON");
}
else { out(3) = 0; }
}
Light panel (excerpt from example_light_panel.ems)
DELAY_MS = 3000;
SCAN_MS = 100;
macro ToggleRelay(idx){
delay(DELAY_MS){ toggle(idx); }
}
every(SCAN_MS) {
if (p_edge(in(1))) { ToggleRelay(1); }
if (p_edge(in(2))) { ToggleRelay(2); }
}
Irrigation booster pump (excerpt from example_irrigation_booster_pump_1.ems)
load(JON, 20);
load(JOFF, 40);
load(MON, 10);
load(MOFF, 30);
every(200) {
p=ain(1)/mul + 10;
b=ain(2)*20/4;
w=ain(3)/mul;
if (ton("dry_run", w<DRY && pump, BDELAY)) {
dry=1;
out(1)=0;
out(2)=0;
out(3)=0;
publish("alarm","DRY_RUN");
}
if (dry) {
out(1)=0;
out(2)=0;
out(3)=0;
}
}
Block Library
Ready-to-use macros live in lib/Interpreter/plc_macros.ems and lib/Interpreter/time_macros.ems. Copy the ones you need into your script or download the files below.
PLC Blocks
- SR
- RS
- HYST
- DEADBAND
- R_TRIG
- F_TRIG
- TP
- TON_B
- TOF_B
- DEBOUNCE
- ON_DELAY
- OFF_DELAY
- CTU
- CTD
- CTUD
- ONCE
- FAULT_LATCH
Time Blocks (UTC-based)
- TIME_OK
- SECONDS_OF_DAY_UTC
- MINUTES_OF_DAY_UTC
- HOUR_UTC
- MIN_UTC
- TIME_WINDOW_UTC
- DOW_IN
- WEEKLY_UTC
- AT_TIME_UTC
- SCHEDULED_UTC