# Introduction

The Uniot Platform is a comprehensive and versatile ecosystem designed to simplify the creation, deployment, and management of Internet of Things (IoT) solutions. It enables both developers and DIY enthusiasts to build customized smart environments with ease, leveraging its intuitive tools and robust infrastructure.

## **Key Features**

1. [**Visual Scripting**](/platform/sandbox/visual-editor): A user-friendly, block-based editor that allows anyone, even non-programmers, to create automation scenarios.
2. [**Edge Computing**](/general-concepts/edge-logic-deployment): On-device automation reduces dependency on centralized servers, ensuring higher reliability.
3. [**Device Interconnectivity**](/guides/two-devices-one-event): Seamless communication between devices using an event-driven approach.
4. [**Custom Dashboards**](/platform/dashboard): Flexible tools to design personalized dashboards for real-time monitoring and control.
5. [**Unified Firmware**](/advanced/uniot-core): Open-source Uniot Core firmware compatible with Arduino, simplifying the integration and management of devices. The Uniot Badge firmware installs straight from the browser with the [Firmware Installer](/platform/installer).

## **Who is it for?**

The Uniot Platform is ideal for:

* **Developers**: Looking to rapidly prototype and deploy IoT applications.
* **DIY Enthusiasts**: Seeking accessible tools to bring their smart environment ideas to life.
* **Small Businesses**: Wanting to implement cost-effective, scalable IoT solutions.

## **Why Choose Uniot?**

Uniot stands out with its combination of ease of use, flexibility, and advanced features. Unlike traditional IoT platforms, Uniot focuses on:

* Empowering users to develop automation scripts both in a no-code style and in an advanced [UniotLisp](/advanced/uniot-lisp/language-description) language.
* Offering a decentralized approach to automation with edge computing.
* Eliminating vendor lock-in by supporting a wide range of devices. It can be used as an alternative firmware for existing devices or in initial developments.

This documentation will guide you through the various aspects of the platform, from setting up your first device to leveraging advanced features for complex applications. Let’s get started!

## **Support**

For additional help or to report issues:

* Visit our [Community Forum](https://community.uniot.io)
* Check our [GitHub Repository](https://github.com/uniot-io)
* Contact support at <support@uniot.io>


# Guides

The guides are hands-on. Each one takes 20 to 40 minutes, starts from the board you set up in [Getting Started](/guides/getting-started), and adds the smallest possible change to prove one thing the platform does well: changing a device's behavior without reflashing, letting the device decide on its own, linking devices through events, or teaching scripts a new primitive written in C++.

Follow them in order. Every guide builds its script in the [Sandbox](/platform/sandbox), runs it in the [Emulator](/platform/sandbox/emulator) first, and deploys it only once it behaves. Because the Emulator talks to real dashboards and devices, guides 1, 4, 5 and 6 can be followed almost entirely without extra hardware.

## Start Here

{% content-ref url="/pages/GB3cVYWFMg777cgsOu3c" %}
[Getting Started](/guides/getting-started)
{% endcontent-ref %}

## Learning Path

| # | Guide                                                                        | What you build                                         | What it proves                                      | Extra hardware                |
| - | ---------------------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------- | ----------------------------- |
| 0 | [Getting Started](/guides/getting-started)                                   | Button, LED and dashboard linked by one event          | Flash once, deploy logic over the air               | None                          |
| 1 | [Scripts Without Reflashing](/guides/scripts-without-reflashing)             | A blink and a press counter, swapped over the air      | Sandbox workflow, task model, persistent scripts    | None                          |
| 2 | [Dim an LED from a Slider](/guides/dim-an-led-from-a-slider)                 | A dashboard slider driving PWM brightness              | Register indices, control widgets, retained events  | LED and resistor              |
| 3 | [A Night Light That Thinks for Itself](/guides/night-light)                  | Light sensor, threshold and LED, decided on the device | Edge logic, publishing only changes, works offline  | Potentiometer or LDR          |
| 4 | [Two Devices, One Event](/guides/two-devices-one-event)                      | Two peers toggling each other, then a doorbell         | Device-to-device events with no cloud code          | Second board, or the Emulator |
| 5 | [Add a Real Sensor with a Custom Primitive](/guides/custom-primitive-sensor) | A thermostat over a DHT22 exposed as `get_temp`        | Extending scripts from C++, mocking in the Emulator | DHT22                         |
| 6 | Schedule on the Device                                                       | A lamp that follows a daily schedule on its own        | Uniot Core time, autonomy, everything combined      | LED or relay                  |

Guides 1 to 6 are coming soon. They will appear in the sidebar as each one is published.

## Hardware-Specific

{% content-ref url="/pages/JIifa7nkIHMYDMTA9ek2" %}
[Uniot Badge](/guides/uniot-badge)
{% endcontent-ref %}


# Getting Started

This guide takes you from a bare ESP board to a working IoT device in about 20 minutes. You will flash the Uniot Core firmware once, connect the device to your account, deploy a script to it over the air, and control it from a browser dashboard.

## What You'll Build

A device whose physical button, onboard LED, and browser dashboard all talk through a single `led` event:

* Press the physical button — the onboard LED toggles, and the dashboard reflects it.
* Toggle a switch on the dashboard — the onboard LED follows.

None of this logic is compiled into the firmware. You flash generic firmware once; the behavior comes from a small script deployed to the device over the air, and devices, scripts, and dashboards all communicate through events. This is Uniot's core idea — read more in [Edge Logic Deployment](/general-concepts/edge-logic-deployment).

## Prerequisites

* A supported board. This guide provides ready-to-use configurations for **NodeMCU-class ESP8266** and **ESP32 DevKit** boards; any other ESP8266/ESP32 board works with minor pin tweaks.
* A USB **data** cable (some charging cables carry no data).
* [PlatformIO](https://platformio.org/) installed — either the VSCode extension or the CLI.
* A Uniot account — [Get Early Access](https://forms.fillout.com/t/k1LDnvkgvPus). Once you are in, copy your **account ID** from your profile page (it is also shown on the Add-new-device screen). You will need it in Step 1.

{% hint style="info" %}
**Have a Uniot Badge?** It has firmware of its own, so you don't need to build any. Install or update it from your browser with the [Firmware Installer](/platform/installer), and see the [Uniot Badge](/guides/uniot-badge) guide.
{% endhint %}

## Step 1: Flash the Firmware

### Create a Project

Create a new PlatformIO project (via the IDE or `pio project init`) — or open an existing one — and replace its `platformio.ini` and `src/main.cpp` with the files below.

### Configure platformio.ini

{% tabs %}
{% tab title="ESP8266 (NodeMCU)" %}
{% code title="platformio.ini" lineNumbers="true" %}

```ini
[env:nodemcuv2]
platform = espressif8266
framework = arduino
board = nodemcuv2
monitor_speed = 115200
board_build.filesystem = littlefs

lib_deps =
    uniot-io/uniot-core@^0.9.0

build_unflags =
    -std=gnu++11

build_flags =
    -std=gnu++17
    -D UNIOT_CREATOR_ID=\"UNIOT\"
    -D UNIOT_USE_LITTLEFS=1
    -D UNIOT_LOG_ENABLED=1
    -D UNIOT_LOG_LEVEL=4
    -D MQTT_MAX_PACKET_SIZE=2048
```

{% endcode %}

Using a different ESP8266 board? Change `board` accordingly (e.g. `d1_mini`, `esp12e`).
{% endtab %}

{% tab title="ESP32 (DevKit)" %}
{% code title="platformio.ini" lineNumbers="true" %}

```ini
[env:esp32doit-devkit-v1]
platform = espressif32
framework = arduino
board = esp32doit-devkit-v1
monitor_speed = 115200
board_build.filesystem = littlefs

lib_deps =
    uniot-io/uniot-core@^0.9.0

build_unflags =
    -std=gnu++11

build_flags =
    -std=gnu++17
    -D UNIOT_CREATOR_ID=\"UNIOT\"
    -D UNIOT_USE_LITTLEFS=1
    -D UNIOT_LOG_ENABLED=1
    -D UNIOT_LOG_LEVEL=4
    -D MQTT_MAX_PACKET_SIZE=2048
```

{% endcode %}

Using a different ESP32 board? Change `board` accordingly (e.g. `esp32dev`).
{% endtab %}
{% endtabs %}

`UNIOT_CREATOR_ID` is **required** — the build fails without it. The full list of build flags is described in [Build Flags](/advanced/uniot-core#build-flags). For ESP32-C3 boards (extra USB flags) and projects targeting several boards at once, see the ready-made environments in Uniot Core's [platformio.ini](https://github.com/uniot-io/uniot-core/blob/master/platformio.ini).

### Write main.cpp

Both files below are complete — pick your board's tab, copy it, and fill in your WiFi credentials and account ID at the top of `setup()`.

{% tabs %}
{% tab title="ESP8266 (NodeMCU)" %}
{% code title="main.cpp" lineNumbers="true" %}

```c++
#include <Uniot.h>

// NodeMCU and most ESP8266 boards:
// GPIO0 = FLASH button (pressed = LOW)
// GPIO2 = onboard LED (lit when LOW - inverted)
#define PIN_BUTTON 0
#define BTN_ACTIVE_LEVEL LOW
#define PIN_LED 2
#define LED_ACTIVE_LEVEL LOW

void setup() {
  // Fill in your WiFi network and Uniot account ID.
  // To provision from your phone instead (captive portal),
  // comment out both lines - see Step 2.
  Uniot.configWiFiCredentials("YourSSID", "YourPassword");
  Uniot.configUser("your_account_id");

  // Blink connection status on the onboard LED.
  Uniot.configWiFiStatusLed(PIN_LED, LED_ACTIVE_LEVEL);

  // WiFi reset button; also exposed to scripts as (bclicked 0).
  Uniot.configWiFiResetButton(PIN_BUTTON, BTN_ACTIVE_LEVEL, true);

  // Let scripts drive the LED as digital output 0: (dwrite 0 ...).
  Uniot.registerLispDigitalOutput(PIN_LED);

  // Let scripts read the LED pin level back as digital input 0: (dread 0).
  Uniot.registerLispDigitalInput(PIN_LED);

  Uniot.begin();
}

void loop() {
  Uniot.loop();
}
```

{% endcode %}

{% hint style="info" %}
The onboard LED on GPIO2 is inverted: when a script or the dashboard turns it "on", the LED goes dark. Prefer non-inverted behavior? Wire an external LED with a resistor from GPIO2 to GND and set `LED_ACTIVE_LEVEL` to `HIGH`.
{% endhint %}
{% endtab %}

{% tab title="ESP32 (DevKit)" %}
{% code title="main.cpp" lineNumbers="true" %}

```c++
#include <Uniot.h>

// ESP32 DevKit (DoIt v1):
// GPIO0 = BOOT button (pressed = LOW)
// GPIO2 = onboard LED (lit when HIGH)
#define PIN_BUTTON 0
#define BTN_ACTIVE_LEVEL LOW
#define PIN_LED 2
#define LED_ACTIVE_LEVEL HIGH

void setup() {
  // Fill in your WiFi network and Uniot account ID.
  // To provision from your phone instead (captive portal),
  // comment out both lines - see Step 2.
  Uniot.configWiFiCredentials("YourSSID", "YourPassword");
  Uniot.configUser("your_account_id");

  // Blink connection status on the onboard LED.
  Uniot.configWiFiStatusLed(PIN_LED, LED_ACTIVE_LEVEL);

  // WiFi reset button; also exposed to scripts as (bclicked 0).
  Uniot.configWiFiResetButton(PIN_BUTTON, BTN_ACTIVE_LEVEL, true);

  // Let scripts drive the LED as digital output 0: (dwrite 0 ...).
  Uniot.registerLispDigitalOutput(PIN_LED);

  // Let scripts read the LED pin level back as digital input 0: (dread 0).
  Uniot.registerLispDigitalInput(PIN_LED);

  Uniot.begin();
}

void loop() {
  Uniot.loop();
}
```

{% endcode %}

{% hint style="info" %}
Some ESP32 boards (e.g. the official ESP32-DevKitC) have no onboard user LED. If yours doesn't, wire an LED with a resistor from GPIO2 to GND.
{% endhint %}
{% endtab %}
{% endtabs %}

{% hint style="warning" %}
**Before uploading**, replace `YourSSID`, `YourPassword`, and `your_account_id` in the two config lines at the top of `setup()` with your WiFi credentials and Uniot account ID.

Prefer not to put credentials in code? Comment out both lines. The device will then open a captive portal on boot, and you will provision it from your phone in Step 2.
{% endhint %}

While connecting, Uniot Core blinks the connection status on the LED; once connected, the LED goes dark and your scripts own it (if WiFi drops, the core temporarily takes it back). Note that scripts address hardware by logical index, not GPIO number: the button is exposed as `(bclicked 0)`, and the LED as digital output `0` for `(dwrite 0 ...)` and as digital input `0` for `(dread 0)`, which returns the current level of the LED pin — see [the register system](/general-concepts/primitives#the-register-system) for how indices work and [WiFi Management](https://github.com/uniot-io/uniot-core/blob/master/docs/reference.md#3-wifi-management) for details on the WiFi subsystem.

### Upload

Connect the board via USB and run:

```bash
pio run --target upload
```

Or press the upload arrow in the PlatformIO toolbar of VSCode.

{% hint style="success" %}
**Checkpoint** — run `pio device monitor`: you should see Uniot Core log lines. The onboard LED shows the connection state:

* **Brief flash, then dark** — connected (this is what you should see)
* **Medium blink** — connecting to your WiFi
* **Slow blink** — setup (Access Point) mode, waiting to be provisioned (only if you commented out the credentials; continue in Step 2)
* **Fast blink** — error
  {% endhint %}

## Step 2: Connect the Device to Your Account

You already provided everything the device needs before flashing. On boot it joins your WiFi — the status LED goes dark — and registers under your account ID. Skip ahead to [Authorize the Device](#authorize-the-device).

### Using the Captive Portal Instead

If you commented out `configWiFiCredentials()` and `configUser()` in `setup()`, the device has no credentials yet and opens its own WiFi network with a setup page. Provision it from your phone or laptop:

**Start the wizard.** Open the **Devices** page on the platform and click **Add new device** — or switch back to that screen if you already have it open. It shows these same steps along with your account ID.

{% hint style="info" %}
On an iPhone or Mac, copy your account ID **before** joining the device's WiFi — the captive portal opens in a separate window, and this page won't be reachable until you disconnect. (Your account ID is also available on your profile page.)
{% endhint %}

**Join the device's network.** Power the device and connect your phone or laptop to the WiFi network named `UNIOT-xxxxxx`.

**Provision it.** In the portal that opens, enter your account ID, pick your home WiFi, type its password, and press **Connect**.

{% hint style="info" %}
Your WiFi credentials are used only on the device itself, only to connect it to the Internet.
{% endhint %}

Keep both config lines commented out from now on. `configWiFiCredentials()` stores its values on every boot, so if it comes back into `setup()`, it overwrites whatever you entered through the portal.

### Authorize the Device

New devices are not trusted automatically. Open the **Devices** page, switch to the **Unauthorized** tab, select your device, and click **Authorize** to bind it to your account.

{% hint style="success" %}
**Checkpoint** — your device appears on the Devices page with the status **Online**. If it doesn't, see [Troubleshooting](#troubleshooting).
{% endhint %}

## Step 3: Deploy Your First Script

Open the **Sandbox** page. Every new account comes with a welcome script called **"My First Script"** — open it. The script is built from visual blocks, which compile to this UniotLisp code, executed directly on the device:

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-3ccf18c7c4a952a452f040a89f2415aba08d625f%2Fwelcome_script.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="My First Script" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs bclicked (button_id)) ;-> Bool
; (defjs dwrite (pin state)) ;-> Bool
;
;;; end-user-library

(define state ())

(setq state ())

; Runs the task every '50' ms. Since 'times' is '0',
; it runs indefinitely. The context is released after
; each run, allowing other processes to run smoothly.
(task 0 50 '
 (progn
; If the button '0' is clicked, emit an event 'led' to toggle state.
  (if
   (bclicked 0)
   (progn
    (push_event 'led
     (not
      (bool state)))))
; When the ‘led’ event is triggered, set ‘state’ to the received
; value and write to pin ‘0’, driving the LED accordingly.
  (if
   (is_event 'led)
   (progn
    (setq state
     (pop_event 'led))
    (dwrite 0 state)))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

What the script does:

* **State variable** — `state` starts as `false` and tracks whether the LED is on.
* **Task** — `(task 0 50 '...)` runs the body every 50 ms, forever (`times` is `0`). Every script is built around this [task model](/general-concepts/scripting#task-based-execution-model).
* **Button check** — when `(bclicked 0)` reports a click, the script publishes the `led` event with the toggled value. Events travel through MQTT, so the dashboard (and other devices) hear them too.
* **Event handler** — when a `led` event arrives, the script saves its value into `state` and writes it to digital output `0` via `dwrite`. Indices like `0` refer to [registered pins](/general-concepts/primitives#the-register-system), not raw GPIO numbers.

The firmware from Step 1 already provides these indices: `bclicked 0` is your BOOT/FLASH button, and the onboard LED is both digital output `0` (`dwrite`) and digital input `0` (`dread`, which reads the pin level back).

{% hint style="info" %}
No hardware at hand? You can run this script in the [Emulator](/platform/sandbox/emulator) and interact with virtual components instead.
{% endhint %}

To deploy: with the script open, press **Deploy**, select the device you authorized in Step 2, and confirm. The [Sandbox](/platform/sandbox) page describes the full development workflow.

{% hint style="success" %}
**Checkpoint** — press the **BOOT/FLASH** button on the board: the onboard LED toggles with each press (remember, inverted on ESP8266). Nothing happens? Open your device's page, check the **Logs** tab, and see [Debugging Scripts](/general-concepts/scripting#debugging-scripts).
{% endhint %}

## Step 4: Control It from the Dashboard

Open the **Dashboard** page. A pre-made dashboard called **"My First Dashboard"** was also created for your account. It contains two widgets, both bound to the `led` event:

* **Switch** — publishes the `led` event each time you toggle it.
* **LED** — subscribes to the `led` event and reflects its current value.

With the script deployed, the device, the script, and the dashboard form one loop around the `led` event:

* **Press the physical button** — the script publishes `led` with the toggled value to the MQTT broker. The broker delivers it back to the script (which updates `state` and drives the LED via `dwrite`) and to the dashboard (whose widgets update).
* **Toggle the Switch widget** — the dashboard publishes the same `led` event, and the script receives and handles it in exactly the same way.

The device doesn't treat the dashboard as anything special — everything simply speaks events. See [Dashboard](/platform/dashboard) for widgets and layout editing.

{% hint style="success" %}
**Checkpoint** — it works in both directions: pressing the physical button flips the Switch and LED widgets, and toggling the Switch flips the onboard LED (ESP8266: "on" = unlit).
{% endhint %}

## Troubleshooting

**Upload fails or no serial port appears.** Hold the BOOT/FLASH button while PlatformIO prints "Connecting…"; make sure the CH340/CP2102 USB driver is installed; try adding `upload_speed = 115200` to `platformio.ini`.

**I don't see the `UNIOT-xxxxxx` network.** The device has WiFi credentials, so it skips Access Point mode. If `configWiFiCredentials()` is still active in `setup()`, comment it out along with `configUser()` and upload again. Otherwise, clear the stored credentials: quick-press the button 4+ times, then hold it for \~3 seconds (all within \~5 seconds). A sketch that also calls `Uniot.configWiFiResetOnReboot()` can be reset by power-cycling it 5 times in quick succession instead; the one in this guide doesn't.

**I can't join the device's network.** Forget the `UNIOT-xxxxxx` network on your phone or laptop and connect to it again.

**The device never appears under Unauthorized.** Usually a wrong or missing account ID (the `configUser()` argument, or the account ID field in the captive portal) or wrong WiFi credentials. Watch the serial monitor for connection errors.

**The LED keeps blinking.** A medium or fast blink that never stops means wrong credentials or a router out of range. Hold the button for 3–5 seconds to force a reconnect attempt. If that doesn't help, fix the values in `setup()` and upload again — or, if you used the captive portal, clear the WiFi config (see above) and redo Step 2.

**The script is deployed but nothing happens.** Open your device's page and check the **Logs** tab for script errors, the **Script** tab to confirm the deployment, and the **Registers** tab to confirm pin indices — then see [Debugging Scripts](/general-concepts/scripting#debugging-scripts).

## What's Next

You now have the full loop — firmware, over-the-air scripts, and a dashboard. Keep going:

{% content-ref url="/pages/CYVNEiYSA5KilM0Nf8Kw" %}
[Scripting](/general-concepts/scripting)
{% endcontent-ref %}

{% content-ref url="/pages/DHslxsJgBqyheEr05Iyh" %}
[Primitives](/general-concepts/primitives)
{% endcontent-ref %}

{% content-ref url="/pages/qRiqeMCWFjiPcmrG3ztl" %}
[Dashboard](/platform/dashboard)
{% endcontent-ref %}

{% content-ref url="/pages/n4gcVyjWLQRRehzL5EY1" %}
[Edge Logic Deployment](/general-concepts/edge-logic-deployment)
{% endcontent-ref %}

{% content-ref url="/pages/JIifa7nkIHMYDMTA9ek2" %}
[Uniot Badge](/guides/uniot-badge)
{% endcontent-ref %}


# Scripts Without Reflashing

You flashed the firmware once in [Getting Started](/guides/getting-started). In this guide you never touch the USB cable again: you write two different scripts, test each one in the browser, and push each one to the board over the air. Then you power-cycle the board and watch the last script come back on its own. About 10 minutes.

## What You'll Build

* A blinking LED, built from blocks and tested in the Emulator before it reaches the board.
* A press counter that replaces the blink with one click and reports through `print`, introducing the logs you will debug with from now on.
* Proof that a persistent script survives a power cycle.

This is the everyday loop of the platform: the board is generic, the behavior is a script, and changing it costs seconds instead of a compile-upload cycle. Read more in [Edge Logic Deployment](/general-concepts/edge-logic-deployment).

## Prerequisites

* The board from [Getting Started](/guides/getting-started), online and authorized under your account.
* Nothing else. The firmware from Getting Started already exposes the onboard LED as digital output `0` and the BOOT/FLASH button as button `0`, and this guide does not change it.

{% hint style="info" %}
On ESP8266 boards the onboard LED is inverted: `dwrite` writes raw pin levels, and GPIO2 lights when LOW, so "on" in a script means the LED goes dark. The Emulator does not know this; its Digital Write card lights whenever the script writes true. The blink looks the same either way, but the counter's LED is lit after odd presses instead of even. Every checkpoint below still holds with light and dark swapped. To make the board match the Emulator, wire an external LED with a resistor from GPIO2 to GND, as described in Getting Started's [Write main.cpp](/guides/getting-started#write-maincpp) section.
{% endhint %}

## Step 1: Blink, from the Emulator to the Board

### Build the Script

Open the **Sandbox** page and create a new script; call it `Blink`.

The script keeps one variable, `state`, flips it every half second, and writes it to the LED. In the **Visual Editor**, assemble the blocks shown below. Press **Compile** to turn the blocks into the UniotLisp code on the second tab; that code is what actually runs on the device.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-7516fe0c47c9c1f7ad4f83169281062c5ddd7426%2Fblink_script.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Blink" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs dwrite (pin state)) ;-> Bool
;
;;; end-user-library

(define state ())

(setq state ())

(task 0 500 '
 (progn
  (setq state
   (not
    (bool state)))
  (dwrite 0 state)))
```

{% endcode %}
{% endtab %}
{% endtabs %}

What the script does:

* **Initial value** — the **set state to false** block outside the task runs once, when the script starts. The compiler turns every variable into a `define` that creates it as `()`, followed by a `setq` with the block's value; `()` is how UniotLisp writes `false`, so here both lines set the same thing.
* **Main loop** — **run task times 0 ms 500** is the program's entry point: everything in its **do** slot runs every 500 ms, and `0` times means forever. In code it is `(task 0 500 '...)`, and the `progn` inside it just means "run these in order". See the [task-based execution model](/general-concepts/scripting#task-based-execution-model).
* **Toggle** — **set state to not state** reads the variable, flips it with the **not** block, and stores it back. The **not** block compiles to `(not (bool state))`: `bool` first turns whatever is in the variable into true or false, so **not** works even when the variable holds a number.
* **Output** — **digital write state to pin 0** sends the value to digital output `0`, which the firmware mapped to the onboard LED: `(dwrite 0 state)`. The `0` is a register index, not a GPIO number; see [The Register System](/general-concepts/primitives#the-register-system).
* **User library block** — the comment block at the top of the code declares which primitives the script uses. The Visual Editor writes it from the blocks you placed; there is nothing to add for it.

{% hint style="info" %}
Prefer typing? Switch the Sandbox to **Advanced** and paste the listing into the code editor instead of building blocks. Keep a script you write by hand in Advanced mode: switching back to Blockly replaces the code with what the blocks generate. See [Visual Editor vs. Code Editor](/platform/sandbox#visual-editor-vs-code-editor).
{% endhint %}

### Run It in the Emulator

The [Emulator](/platform/sandbox/emulator) runs your script in the browser on the same interpreter a device uses, with an interactive card standing in for each primitive.

1. Select your device in the sidebar. The Emulator then uses the device's registers and warns you if the script asks for something the device does not have.
2. Press the play button, or Ctrl+Enter (Cmd+Enter on Mac).

A floating window opens with cards, included **Digital Write**. Its LED blinks every half second.

### Deploy

Press **Deploy** in the Emulator's header. The script is sent to the device and the window closes.

{% hint style="success" %}
**Checkpoint** — the onboard LED blinks once a second. No cable, no compiler, no upload.
{% endhint %}

{% hint style="info" %}
Two Emulator details worth knowing: task intervals are waited in real time, and a task set to run forever stops after 9,999 iterations in the Emulator. On the device, `0` iterations really means forever. See [Limits](/platform/sandbox/emulator#limits).
{% endhint %}

## Step 2: Count Button Presses

The second script reacts to input and talks back. Create a script called `Counter`. Every press of the button adds one to a counter; the LED is lit while the count is even, and the count is printed after each press.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-ad316471e8eb4e7bb6106ac126a2a25275446c69%2Fcounter_script.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Counter" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs bclicked (button_id)) ;-> Bool
; (defjs dwrite (pin state)) ;-> Bool
;
;;; end-user-library

(define count ())

(setq count 0)

(task 0 50 '
 (progn
  (if
   (bclicked 0)
   (progn
    (setq count
     (+ count 1))
    (dwrite 0
     (eql
      (% count 2) 0))
    (print count)))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

What the script does:

* **Initial value** — **set count to 0** outside the task runs once at start. As before it becomes a `define` and a `setq`: `(define count ())` creates the variable, `(setq count 0)` gives it its number.
* **Main loop** — **run task times 0 ms 50** polls twenty times a second, forever: `(task 0 50 '...)`. Fast enough that a click is never missed, slow enough to leave the device idle most of the time.
* **Button check** — the **if** block's condition is **is button 0 clicked**, which is true exactly once per press-and-release and then clears itself, so the **do** slot runs once per click no matter how long the button is held. Compiled: `(if (bclicked 0) (progn ...))`, with `progn` again grouping the blocks inside the **do** slot.
* **Counter** — **change count by 1** adds one to the variable: `(setq count (+ count 1))`. The **change** block is a shortcut for "set to itself plus a number".
* **Output** — **digital write** takes a **comparison** as its value: **remainder of count ÷ 2 = 0**, true while the count is even. The `=` in the comparison block compiles to `eql`, so the code reads `(dwrite 0 (eql (% count 2) 0))`. The LED turns off on the first press and on again on the second.
* **Report** — the **print count** block sends the value out of the script as a log line: `(print count)`. Where it lands depends on where the script runs, which is the point of the next section.

### Read the Logs

Compile and run with the device selected. The Emulator now shows a **Button Clicked** card next to the **Digital Write** card. Click the button a few times: the LED card toggles, and the **Logs** panel on the right prints a timestamped line with the count after each click. If the panel is hidden, press **Logs** in the header. To follow the order of calls, turn on **Debug** (the bug icon in the header): each primitive call lights up its card and the run pauses 300 ms after every call.

There are two different logs, and it helps to know which is which:

* The Emulator's **Logs** panel shows what the script *says*: every `print`, plus any error with its full message. See [Logs](/platform/sandbox/emulator#logs).
* The **Logger** pane in the Sandbox shows what the script *asks for*: the compiler's output and a trace of primitive calls and their answers. See [Logger](/platform/sandbox/logger).

Press **Deploy**, then press BOOT/FLASH on the board. Open your device's page and switch to the **Logs** tab: the same count lines appear there, delivered over MQTT. Errors from a deployed script arrive in the same place, which makes `print` your main tool for [debugging scripts](/general-concepts/scripting#debugging-scripts) on hardware.

{% hint style="success" %}
**Checkpoint** — each press of BOOT/FLASH adds a line to the device's **Logs** tab, and the LED is on after even presses.
{% endhint %}

## Step 3: Survive a Reboot

A deployed script can be volatile or persistent (by default). A volatile script runs until the device restarts. A persistent script is saved to the device's flash, verified by checksum, and started again on every boot; if it ever fails to load, the device still boots normally. See [Script Persistence](/general-concepts/scripting#script-persistence).

Unplug the board and plug it back in. The status LED blinks while the device reconnects and goes dark once it is online. Press BOOT/FLASH: the LED toggles and a new line appears in the **Logs** tab. Nobody deployed anything; the device restored the script itself. The count starts from zero, because variables live in memory, not in flash.

{% hint style="success" %}
**Checkpoint** — the counter works after a power cycle without a new deploy.
{% endhint %}

## Troubleshooting

**The play button is disabled.** The Sandbox shows **Recompile required** after any edit. Press **Compile** first; if the Emulator is still running from before, stop it, since Compile waits for the run to end.

**Compile fails with "Undefined symbol".** This happens with hand-written code when a primitive is missing from the user library block at the top of the script. Add a `defjs` line for it; the Visual Editor adds these automatically. See [User Library Block](/general-concepts/primitives#user-library-block).

**A card shows a warning triangle.** Hover it. "Registers are out of bounds" means the script uses an index the selected device never registered; compare with the device's **Registers** tab. "Unavailable in the selected device" means the device's firmware does not provide that primitive at all.

**The Emulator run stops on its own.** A task set to run forever stops after 9,999 iterations in the Emulator only; the window title changes to **Finished**. On the device it keeps running.

**Deployed, but the board does nothing.** Check the device is **Online** on the **Devices** page, then the **Script** tab on the device page to confirm the deployment, then the **Logs** tab for an error message. See [Debugging Scripts](/general-concepts/scripting#debugging-scripts).

**The LED seems inverted.** On ESP8266 boards the onboard LED is active LOW, so "on" is dark; see the hint under Prerequisites. If the LED instead blinks steadily on its own and ignores the script, the device lost WiFi and Uniot Core has taken the LED back to show the connection status until it reconnects.

**The script did not come back after a reboot.** It was deployed as volatile. Deploy again and pick the persistent option.

## What's Next

{% content-ref url="/pages/0HTj7dPqr9RorMLi56Ku" %}
[Dim an LED from a Slider](/guides/dim-an-led-from-a-slider)
{% endcontent-ref %}

{% content-ref url="/pages/CYVNEiYSA5KilM0Nf8Kw" %}
[Scripting](/general-concepts/scripting)
{% endcontent-ref %}

{% content-ref url="/pages/VgA087SOFbR4tbJjOUDM" %}
[Emulator](/platform/sandbox/emulator)
{% endcontent-ref %}

{% content-ref url="/pages/u8swI6c2kZsOPEu2M2pR" %}
[Visual Editor](/platform/sandbox/visual-editor)
{% endcontent-ref %}


# Dim an LED from a Slider

In [Scripts Without Reflashing](/guides/scripts-without-reflashing) the LED was either on or off. In this guide a dashboard slider sets its brightness through PWM. Along the way you make your first firmware change since Getting Started, a single line, see how firmware exposes hardware to scripts, and watch the broker remember the slider position for a device that reboots. About 15 minutes.

## What You'll Build

* One new line in `main.cpp` that registers an analog output.
* A script that receives a `brightness` event and writes its value with `awrite`.
* A **Slider** widget with **Retain** on, tested against the Emulator before the LED ever lights.
* A power cycle after which the LED comes back at the last slider position, with nobody touching anything.

The firmware line is the important part. Scripts address hardware by logical index, not by GPIO number, so the same script runs on any board whose firmware registers the same indices. The firmware declares what the hardware *can* do; the script decides what it *does*. See [The Register System](/general-concepts/primitives#the-register-system) and [Edge Logic Deployment](/general-concepts/edge-logic-deployment).

## Prerequisites

* The board from [Getting Started](/guides/getting-started), online and authorized under your account, with the PlatformIO project from that guide still at hand. You will upload once more.
* An LED, a 220 Ω resistor, and two jumper wires. A breadboard makes it tidy but is not required.

## Step 1: Register an Analog Output

### Wire the LED

Connect the LED's long leg (anode) to **GPIO13** through the resistor, and its short leg (cathode) to **GND**. Any GPIO-capable pin would do; GPIO13 is used here because it is free on both boards and has no role at boot. It is labelled **D7** on NodeMCU boards and **13** (sometimes **G13**) on ESP32 DevKit boards.

Why not the onboard LED? It already has two jobs: Uniot Core blinks the WiFi status on it, and the earlier guides drive it as digital output `0`. On ESP8266 it is also inverted. A separate LED on its own pin keeps this guide's dimming visible alongside all of that.

### Add One Line to main.cpp

Open `src/main.cpp` from Getting Started and add the two highlighted lines: a pin definition at the top and one registration call before `Uniot.begin()`. Everything else stays as it was.

{% code title="main.cpp (excerpt)" lineNumbers="true" %}

```c++
#define PIN_LED_PWM 13  // D7 on NodeMCU, GPIO13 on ESP32

void setup() {
  // ... everything from Getting Started ...

  // Let scripts dim the LED as analog output 0: (awrite 0 value).
  Uniot.registerLispAnalogOutput(PIN_LED_PWM);

  Uniot.begin();
}
```

{% endcode %}

This line is a capability, not logic. It tells Uniot Core that GPIO13 may be driven with PWM and hands it to scripts as analog output `0`. Nothing about brightness, sliders, or events lives in the firmware.

Each primitive has its own index namespace: analog output `0` is GPIO13, while digital output `0` is still the onboard LED from Getting Started, and the two never collide. See [Registering GPIO Pins](/general-concepts/primitives#registering-gpio-pins).

The value `awrite` accepts is `0` to `1023`. Uniot Core configures 10-bit PWM on both ESP8266 and ESP32, so the range is the same on either board.

### Upload

Connect the board via USB and upload as before:

```bash
pio run --target upload
```

The device keeps its WiFi credentials and account binding, so it reconnects on its own. It also keeps the persistent script from the previous guide and starts it again, which is fine; you replace it in Step 3.

{% hint style="success" %}
**Checkpoint** — open your device's page and switch to the **Registers** tab: analog output index `0` is listed on GPIO 13.
{% endhint %}

## Step 2: Write the Script

Open the **Sandbox** page and create a script called `Dimmer`. It has two jobs. Whenever a `brightness` event arrives, it writes the event's value to the LED. Whenever the button is clicked, it publishes `brightness` with the value `0`, so the LED goes dark and the slider follows.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-9981c06d08dad2f70ec1007a4252cb08c0defc5f%2Fdimmer_script.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Dimmer" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs awrite (pin value)) ;-> Int
; (defjs bclicked (button_id)) ;-> Bool
;
;;; end-user-library

(define brightness ())

(setq brightness 0)

(task 0 100 '
 (progn
  (if
   (is_event 'brightness)
   (progn
    (setq brightness
     (pop_event 'brightness))
    (awrite 0 brightness)))
  (if
   (bclicked 0)
   (progn
    (push_event 'brightness 0)))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

What the script does:

* **Initial value** — **set brightness to 0** outside the task runs once at start: `(define brightness ())` creates the variable, `(setq brightness 0)` gives it its number.
* **Main loop** — **run task times 0 ms 100** polls ten times a second, forever: `(task 0 100 '...)`. The [task model](/general-concepts/scripting#task-based-execution-model) is the same as in the earlier guides.
* **Event check** — **is event brightness** is true while a `brightness` event is waiting in the script's queue. It stays true until something takes the event out, which is what **pop event** does in the next block. See [is event](/platform/sandbox/visual-editor/special#is-event) and [pop event](/platform/sandbox/visual-editor/special#pop-event).
* **Store and write** — **set brightness to pop event brightness** takes the oldest waiting value and keeps it: `(setq brightness (pop_event 'brightness))`. Then **analog write brightness to pin 0** sends it to analog output `0`: `(awrite 0 brightness)`. Event values are numbers, which is exactly what `awrite` wants. The `0` is the register index from Step 1, not a GPIO number.
* **Button** — when **is button 0 clicked** fires, **push event brightness 0** publishes a `brightness` event with the value `0`: `(push_event 'brightness 0)`. The device does not write the LED directly here. The event goes to the broker and comes back through the same **is event** check as a slider move would, so the dashboard slider follows the button too. See [push event](/platform/sandbox/visual-editor/special#push-event).
* **User library block** — the comment block at the top declares the primitives the script uses. The Visual Editor writes it from the blocks you placed.

{% hint style="info" %}
Prefer typing? Switch the Sandbox to **Advanced** and paste the listing into the code editor instead of building blocks. Keep a script you write by hand in Advanced mode: switching back to Blockly replaces the code with what the blocks generate. See [Visual Editor vs. Code Editor](/platform/sandbox#visual-editor-vs-code-editor).
{% endhint %}

## Step 3: Add the Slider and Test Without Hardware

### Add the Slider

Open the **Dashboard** page and enter **Edit Mode**. Press **Add Widget** and pick **Slider**, then fill in its settings:

* **Name**: `Brightness`
* **Event**: `brightness`, spelled exactly as in the script. Event names are case-sensitive and may contain letters, digits, and underscores only.
* **Min**: `0` and **Max**: `1023`, the range `awrite` accepts.
* **Retain**: on. Step 4 is about what this does.

Press **Save**. The widget is now bound to the `brightness` event, on the same event bus as every device in your account. See [Widget Configuration](/platform/dashboard#widget-configuration).

### Run It in the Emulator

Back in the **Sandbox**, select your device in the sidebar, press **Compile**, then play. The [Emulator](/platform/sandbox/emulator) checks the script against the device's registers first: if the device was not reflashed with the line from Step 1, the **Analog Write** card shows a warning triangle with "Registers are out of bounds", and the LED would not work on the device either.

The Emulator window floats, so open the **Dashboard** page next to it. Drag the slider: a moment after you let go, the **Analog Write** gauge in the Emulator moves to the same value. Now click the **Button Clicked** card: the slider on the dashboard snaps back to zero.

Nothing here is simulated. The slider publishes a real `brightness` event to the broker, the Emulator receives it the way a device would, and the Emulator's own push goes back out to the dashboard. See [Events](/platform/sandbox/emulator#events).

{% hint style="warning" %}
Pushed events are real: any deployed device of yours listening for `brightness` would react to them too. That is the point in this guide, but keep it in mind when you reuse event names later.
{% endhint %}

### Deploy

Press **Deploy** in the Emulator's header. The script replaces the one from the previous guide.

{% hint style="success" %}
**Checkpoint** — dragging the slider dims the LED on the breadboard, and pressing BOOT/FLASH turns it off and pulls the slider to zero.
{% endhint %}

## Step 4: Reboot and Watch Retain Work

Set the slider to about half. Unplug the board and plug it back in. The onboard LED blinks while the device reconnects, and once it goes dark the breadboard LED comes back at half brightness. Nobody moved the slider.

Two things made that happen. The persistent script was restored from flash, as in the previous guide. And because the widget has **Retain** on, the broker kept the last `brightness` value and replayed it to the device as soon as it subscribed, so the script's first **is event** check already found it. See [Retain](/platform/dashboard#retain).

The Emulator behaves the same way. Stop it, start a fresh run, and the **Analog Write** gauge jumps to the slider's level immediately: the retained value seeds the event queue on the first check.

### Try It Without Retain

To see the difference, open the slider's settings with the gear icon, untick **Retain**, and press **Save**. This also clears the value the broker was holding. Move the slider to about half again and power-cycle the board. This time the LED stays dark after the reconnect, until you touch the slider.

Make the slider the last thing you touch before unplugging. Events pushed by a device are always retained, so a button press just before the power cycle would leave a retained `0` on the broker.

Turn **Retain** back on when you are done; it is what a slider should normally do, and the sliders in the later guides all use it.

{% hint style="success" %}
**Checkpoint** — with Retain on, the LED comes back at the last slider level after a power cycle; with it off, it stays dark until the slider moves.
{% endhint %}

## Troubleshooting

**The LED never lights.** Check the polarity: the long leg goes to the resistor and GPIO13, the short leg to GND. Compare the pin with the device's **Registers** tab, and confirm on the **Script** tab that the Dimmer script is the one deployed.

**The LED is only ever fully on or off.** The value reaching `awrite` is outside `0` to `1023`, or it is a boolean. Check the slider's **Min** and **Max**, and look at the device's **Logs** tab for a script error.

**The slider moves but nothing happens.** The widget's **Event** and the script's event name differ; names are case-sensitive. In the Emulator, hover the warning triangle on the **Analog Write** card: "Registers are out of bounds" means the selected device does not have analog output `0`, so either it was not reflashed with the line from Step 1 or you selected a different device.

**The slider jumps to zero on its own.** Someone pressed BOOT/FLASH, or a retained `brightness 0` from an earlier button press was replayed. Both are expected; move the slider again.

**An old value comes back after a reboot.** A retained event from an earlier project with the same name is being replayed. Use a fresh event name, or clear it as described under [Retain](/platform/dashboard#retain).

**The Emulator run stops on its own.** A task set to run forever stops after 9,999 iterations in the Emulator only; see [Limits](/platform/sandbox/emulator#limits). On the device it keeps running.

## What's Next

{% content-ref url="/pages/Q7jSDmCm3GzwDMLQ1YqI" %}
[A Night Light That Thinks for Itself](/guides/night-light)
{% endcontent-ref %}

{% content-ref url="/pages/DHslxsJgBqyheEr05Iyh" %}
[Primitives](/general-concepts/primitives)
{% endcontent-ref %}

{% content-ref url="/pages/qRiqeMCWFjiPcmrG3ztl" %}
[Dashboard](/platform/dashboard)
{% endcontent-ref %}


# A Night Light That Thinks for Itself

A light sensor, a threshold, and an LED. The device reads, decides and acts on its own; the dashboard only watches the value and tunes the threshold. At the end you switch the WiFi off and the night light keeps working. About 30 minutes.

## What You'll Build

* An analog input registered in firmware and read with `aread`.
* A script that turns the LED on when it gets dark, without sending anything anywhere.
* A `light` event published only when the reading moves by more than a step, and a **Value** widget showing it.
* A `threshold` slider on the dashboard, retained, that the script reads back.
* A demonstration that none of this needs the network.

This is [Edge Logic Deployment](/general-concepts/edge-logic-deployment) in one page: the logic lives on the device, the cloud is an observer.

## Prerequisites

* The board and the LED on GPIO13 from [Dim an LED from a Slider](/guides/dim-an-led-from-a-slider), with the PlatformIO project at hand. You will upload once more.
* A light-dependent resistor (LDR) and a 10 kΩ resistor, or a 10 kΩ potentiometer instead of both.

## Step 1: Register the Analog Input

### Wire the Sensor

The LDR and the resistor form a voltage divider: LDR from **3V3** to a free breadboard row, resistor from that row to **GND**, and the row to the analog pin, **A0** on NodeMCU or **GPIO34** on ESP32. With the LDR on the 3V3 side, light gives a high reading and darkness a low one, which is what the script assumes.

With a potentiometer, connect the outer legs to **3V3** and **GND** and the wiper to the analog pin. Turning towards GND is nightfall.

{% hint style="info" %}
**ESP8266**: A0 on NodeMCU takes 0 to 3.3 V. On a bare ESP-12 module it takes 0 to 1 V; use a 2.2 kΩ resistor there instead of 10 kΩ.

**ESP32**: use GPIO32 to GPIO39. The other ADC pins share hardware with WiFi and stop reading while the radio is on.
{% endhint %}

### Edit main.cpp

Open `src/main.cpp` and make two changes: register the sensor, and add GPIO13 to the digital output line from Getting Started so the LED can be switched on and off as well as dimmed. The analog output line from the previous guide stays.

{% tabs %}
{% tab title="ESP8266 (NodeMCU)" %}
{% code title="main.cpp (excerpt)" lineNumbers="true" %}

```c++
#define PIN_LED_PWM 13  // D7, already there since Dim an LED from a Slider
#define PIN_LDR A0      // the divider's middle point

void setup() {
  // ... everything from Getting Started, except the line below ...

  // Was registerLispDigitalOutput(PIN_LED). Digital output 0 stays
  // the onboard LED; GPIO13 becomes digital output 1.
  Uniot.registerLispDigitalOutput(PIN_LED, PIN_LED_PWM);

  // Kept from Dim an LED from a Slider: GPIO13 is also analog output 0.
  Uniot.registerLispAnalogOutput(PIN_LED_PWM);

  // Let scripts read the light sensor as analog input 0: (aread 0).
  Uniot.registerLispAnalogInput(PIN_LDR);

  Uniot.begin();
}
```

{% endcode %}
{% endtab %}

{% tab title="ESP32 (DevKit)" %}
{% code title="main.cpp (excerpt)" lineNumbers="true" %}

```c++
#define PIN_LED_PWM 13  // GPIO13, already there since Dim an LED from a Slider
#define PIN_LDR 34      // GPIO34, the divider's middle point

void setup() {
  // ... everything from Getting Started, except the line below ...

  // Was registerLispDigitalOutput(PIN_LED). Digital output 0 stays
  // the onboard LED; GPIO13 becomes digital output 1.
  Uniot.registerLispDigitalOutput(PIN_LED, PIN_LED_PWM);

  // Kept from Dim an LED from a Slider: GPIO13 is also analog output 0.
  Uniot.registerLispAnalogOutput(PIN_LED_PWM);

  // Let scripts read the light sensor as analog input 0: (aread 0).
  Uniot.registerLispAnalogInput(PIN_LDR);

  Uniot.begin();
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

GPIO13 is now analog output `0` and digital output `1` at the same time; each primitive has its own index namespace. See [Registering GPIO Pins](/general-concepts/primitives#registering-gpio-pins). `aread` returns `0` to `1023` on both boards.

Upload with `pio run --target upload`. The device reconnects on its own and restarts the Dimmer script, which you replace in Step 2.

{% hint style="success" %}
**Checkpoint** — the device's **Registers** tab lists analog input `0` on A0 or GPIO 34, and digital output `1` on GPIO 13.
{% endhint %}

## Step 2: Read and Log the Sensor

Open the **Sandbox** page and create a script called `Night Light`. The first version only prints the sensor: a task every 200 ms with one **print** block holding **analog read** register `0`.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-38b1f735bb5d5d8049a66afa6663bfb6ddede880%2Fnight_light_1.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Night Light (first version)" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs aread (pin)) ;-> Int
;
;;; end-user-library

(task 0 200 '
 (progn
  (print
   (aread 0))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

Select your device, press **Compile**, then play. Turn the **Analog Read** knob in the Emulator and watch the **Logs** panel: the printed value follows about half a second after you stop turning ([Analog Read](/platform/sandbox/emulator#analog-read)).

Press **Deploy** and open the device's **Logs** tab. Note the value in room light, then cover the sensor and note it again. Pick a threshold halfway between uncovered and covered; this guide uses `100`.

{% hint style="success" %}
**Checkpoint** — covering the sensor changes the printed value.
{% endhint %}

## Step 3: Decide on the Device

The script grows in three edits, and the LED works from the first one on.

### Turn the LED On When It Gets Dark

The LED should be on whenever the reading is below a threshold. Outside the task, add **set light to 0** and **set threshold to** your number. Inside it, replace the print block with **set light to analog read 0**, then a **digital write** to register `1` whose value is the **comparison** **light < threshold**.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-1dcf80a91b987edcf164483498dd0647f3ba1768%2Fnight_light_2.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Night Light" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs aread (pin)) ;-> Int
; (defjs dwrite (pin state)) ;-> Bool
;
;;; end-user-library

(define light ())
(define threshold ())

(setq light 0)
(setq threshold 100)

(task 0 200 '
 (progn
  (setq light
   (aread 0))
  (dwrite 1
   (< light threshold))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

* **Read** — **set light to analog read 0** stores the current reading: `(setq light (aread 0))`. The `0` is the analog input index from Step 1.
* **Decide and act** — **digital write light < threshold to pin 1** compiles to `(dwrite 1 (< light threshold))` and lights digital output `1` while the reading is below the threshold. The comparison happens in the device's own memory; no event, no broker.

Compile and run. The **Digital Write** card now shows two registers; turn the knob below the threshold and register `1` lights. Deploy.

{% hint style="success" %}
**Checkpoint** — the breadboard LED comes on when you cover the sensor and goes off when you uncover it.
{% endhint %}

### Publish Only Changes

The device should report the reading to the dashboard, but only when it has moved by more than `20` since the last report. Outside the task, add **set last\_sent to -100**. At the end of the task, add an **if** whose condition is **absolute of light - last\_sent > 20**: a **comparison** holding a **math operation** set to **absolute** and an **arithmetic** block. In its **do** slot, **push event** `light` with the value **light**, then **set last\_sent to light**.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-4d72882ce4a1a58d0d7fa7f6c80671b47615244a%2Fnight_light_3.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Night Light" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs aread (pin)) ;-> Int
; (defjs dwrite (pin state)) ;-> Bool
;
;;; end-user-library

(define last_sent ())
(define light ())
(define threshold ())

(setq last_sent -100)
(setq light 0)
(setq threshold 100)

(task 0 200 '
 (progn
  (setq light
   (aread 0))
  (dwrite 1
   (< light threshold))
  (if
   (>
    (abs
     (- light last_sent)) 20)
   (progn
    (push_event 'light light)
    (setq last_sent light)))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

* **Distance** — `(abs (- light last_sent))` is how far the reading has drifted since the last publish. The **if** fires only when that exceeds `20`, about two percent of the range.
* **Publish** — `(push_event 'light light)` sends the reading and `(setq last_sent light)` remembers what was sent. See [push event](/platform/sandbox/visual-editor/special#push-event).
* **Start** — `last_sent` begins at `-100`, further than `20` from any reading, so the first pass always publishes once.

Why not publish every reading? Five messages a second from every device, forever, mostly saying that nothing changed: broker load and dashboard noise for no information. The device has the previous value in memory and can tell when a change matters. That filtering is the device thinking, the same as the LED decision; the network carries information, not samples. The `20` is a policy, and a different project may want `5` or `100`.

{% hint style="success" %}
**Checkpoint** — the script compiles and the LED still follows the knob. The events show up in Step 4.
{% endhint %}

### Accept a Threshold from the Dashboard

The dashboard should be able to move the threshold while the script runs. At the end of the task add a second **if** with **is event** `threshold` as its condition and **set threshold to pop event threshold** in its **do** slot.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-313eda223909c5b164aef9314ae246492f998ea4%2Fnight_light_4.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Night Light" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs aread (pin)) ;-> Int
; (defjs dwrite (pin state)) ;-> Bool
;
;;; end-user-library

(define last_sent ())
(define light ())
(define threshold ())

(setq last_sent -100)
(setq light 0)
(setq threshold 100)

(task 0 200 '
 (progn
  (setq light
   (aread 0))
  (dwrite 1
   (< light threshold))
  (if
   (>
    (abs
     (- light last_sent)) 20)
   (progn
    (push_event 'light light)
    (setq last_sent light)))
  (if
   (is_event 'threshold)
   (progn
    (setq threshold
     (pop_event 'threshold))))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

* **Listen and take** — the same pattern as `brightness` in the previous guide: [is event](/platform/sandbox/visual-editor/special#is-event) is true while a `threshold` event waits, and [pop event](/platform/sandbox/visual-editor/special#pop-event) takes it into the variable. From the next pass on, the comparison uses it.
* **Default stays** — with no event, the `100` at the top remains in force. The dashboard is optional.

This is the complete script, including the user-library block the editor adds. Save it; the checkpoint for this edit is in Step 4.

{% hint style="info" %}
Prefer typing? Switch the Sandbox to **Advanced** and paste the listing into the code editor instead of building blocks. Keep a script you write by hand in Advanced mode: switching back to Blockly replaces the code with what the blocks generate. See [Visual Editor vs. Code Editor](/platform/sandbox#visual-editor-vs-code-editor).
{% endhint %}

## Step 4: Watch and Tune from the Dashboard

Open the **Dashboard** page, enter **Edit Mode**, and add two widgets with **Add Widget**:

* A **Value** named `Light`, event `light`, **Decimal digits** `0`.
* A **Slider** named `Threshold`, event `threshold`, **Min** `0`, **Max** `1023`, **Retain** on.

Save both and drag the slider to the threshold from Step 2. See [Widget Configuration](/platform/dashboard#widget-configuration).

Back in the **Sandbox**, compile and run with the Dashboard page next to the Emulator window. Turn the **Analog Read** knob slowly: the **Light** widget follows in jumps of at least `20` and stays still while the knob is still. Leave the knob at some value and drag the **Threshold** slider past it: register `1` on the **Digital Write** card flips as the slider crosses. Everything goes through the real broker, so the widgets see the Emulator exactly as they will see the device ([Events](/platform/sandbox/emulator#events)).

Press **Deploy** in the Emulator's header. The device picks up the retained slider position on its first pass.

{% hint style="success" %}
**Checkpoint** — the **Light** widget follows the sensor in steps, and moving the **Threshold** slider changes where the breadboard LED switches.
{% endhint %}

## Step 5: Pull the Plug on WiFi

Switch the router off, or carry the board out of range. The onboard LED starts blinking as Uniot Core tries to reconnect. The breadboard LED does not care: cover the sensor and it lights, uncover it and it goes dark, at the threshold the slider last set. The script never stopped; the `light` events it tried to publish were simply dropped.

Bring the network back. The onboard LED goes dark, the broker replays the retained `threshold`, and the **Light** widget catches up on the next change of `20` or more. Nothing needed resetting or redeploying.

In the [imperative model](/general-concepts/edge-logic-deployment#imperative-model-direct-commands) the device would stream readings and a cloud rule would send "turn on" back, so a dead link means a dark room. Here the cloud only ever saw a number and offered a threshold. The decision was the device's from the start, and losing the cloud loses nothing but the view.

{% hint style="success" %}
**Checkpoint** — the LED keeps reacting to the sensor with the network down, and after reconnecting the widget and the slider are as you left them.
{% endhint %}

## Troubleshooting

**The reading is stuck at 0 or 1023.** The analog pin is not on the point between the LDR and the resistor, or the pin is wrong for the board: ESP32 needs GPIO32 to 39, and a bare ESP8266 module tops out at 1 V.

**Dark gives a high number.** The LDR and the resistor are swapped. Swap them back, or change the comparison to **light > threshold**.

**The Light widget updates constantly.** The reading jitters more than the step. Raise the `20` to `50`, or move the sensor away from a flickering light.

**The slider has no effect.** The widget's **Event** and the script's event name differ (names are case-sensitive), or the slider is outside the range you saw in Step 2. If you toggled **Retain**, the broker forgot the value; drag the slider again.

**The LED never lights, or the Emulator says "Registers are out of bounds".** The device does not have the firmware from Step 1, or another device is selected. The **Registers** tab must list digital output `1` and analog input `0`.

**Nothing works after the network comes back.** Check the device's **Logs** tab; a script error stops the interpreter. See [Debugging Scripts](/general-concepts/scripting#debugging-scripts).

**The Emulator run stops on its own.** A forever task stops after 9,999 iterations in the Emulator only, about half an hour at 200 ms; see [Limits](/platform/sandbox/emulator#limits).

## What's Next

{% content-ref url="/pages/bginrcI4v3vqc4nopR1v" %}
[Two Devices, One Event](/guides/two-devices-one-event)
{% endcontent-ref %}

{% content-ref url="/pages/n4gcVyjWLQRRehzL5EY1" %}
[Edge Logic Deployment](/general-concepts/edge-logic-deployment)
{% endcontent-ref %}

{% content-ref url="/pages/DHslxsJgBqyheEr05Iyh" %}
[Primitives](/general-concepts/primitives)
{% endcontent-ref %}


# Two Devices, One Event

Two boards run the script from [Getting Started](/guides/getting-started). Press the button on one and the LED on both toggles, and the dashboard follows. Then the two get different jobs, a doorbell button and a bell, by deploying different scripts: no firmware change, no rule written in the cloud, no configuration linking them. About 15 minutes, and no extra hardware if the Emulator stands in for the second board.

## What You'll Build

* A second peer: another ESP board with the Getting Started firmware, or the Emulator.
* The same script on both peers, proving a script is portable across boards.
* A doorbell: one peer publishes `ring`, the other blinks three times when it hears it.
* The dashboard as a third peer, with a **Push Button** that rings the bell and an **LED** that flashes with it.

Every device, dashboard and Emulator in your account shares one event bus. A device knows nothing about the other devices; it only knows event names. Publish `ring`, and whoever listens for `ring` reacts, whether that is a board, a widget, or a browser tab.

## Prerequisites

* The board from the previous guides, online and authorized. This guide calls it **A**. Its firmware already has everything needed: the button as `bclicked 0` and the onboard LED as digital output `0`.
* A second peer, called **B**. Either a second ESP8266 or ESP32 board, or nothing at all: the [Emulator](/platform/sandbox/emulator#events) publishes and receives real events and can play B from a browser tab.
* No PlatformIO work. If you have a spare board of the other family, use it: an ESP8266 and an ESP32 side by side make the portability point better than two identical boards.

{% hint style="warning" %}
**Pushed events are real.** Every device in your account that handles `led` or `ring` will react to this guide. If something else already uses those names, pick different ones and use them consistently in the scripts and widgets below.
{% endhint %}

On ESP8266 boards the onboard LED is inverted: "on" in a script means the LED goes dark. Every checkpoint below still holds with light and dark swapped, and a mixed pair will simply blink out of phase with each other. See the note in Getting Started's [Write main.cpp](/guides/getting-started#write-maincpp) section.

## Step 1: Bring Up the Second Peer

{% tabs %}
{% tab title="Second board" %}
Repeat Step 1 and Step 2 of [Getting Started](/guides/getting-started) on the second board: flash the firmware for its family, connect it to your WiFi, and authorize it under the same account. Use the `platformio.ini` and `main.cpp` from that guide as they are; the only thing the two boards must share is the account.

Give it a name you can tell apart from A. Open the device's page from the **Devices** page and rename it, for example `Bell`.

{% hint style="success" %}
**Checkpoint** — the **Devices** page shows both boards with the status **Online**.
{% endhint %}
{% endtab %}

{% tab title="Emulator as the second peer" %}
Nothing to set up yet. The Emulator becomes peer B the moment you run a script in it without selecting a device: it then publishes on your account's event topic with sender type `emulator`, and queues the events that devices and widgets publish, exactly as a board would ([Events](/platform/sandbox/emulator#events)).

Two things to keep in mind on this path. Only one emulation runs at a time, so you deploy to A first and start B afterwards. And the floating Emulator window is B: leave it open while you browse to the dashboard, and close it only when the guide is done.

{% hint style="success" %}
**Checkpoint** — board A is **Online** on the **Devices** page. The Emulator is ready whenever you are.
{% endhint %}
{% endtab %}
{% endtabs %}

## Step 2: One Script, Two Peers

The welcome script from Getting Started already does everything this step needs. On a click it publishes `led` with the toggled state; when `led` arrives it stores the value and writes it to digital output `0`. Nothing in it names a device, so it will run identically on both peers.

Open the **Sandbox** page and open **My First Script**.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-3ccf18c7c4a952a452f040a89f2415aba08d625f%2Fwelcome_script.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="My First Script" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs bclicked (button_id)) ;-> Bool
; (defjs dwrite (pin state)) ;-> Bool
;
;;; end-user-library

(define state ())

(setq state ())

; Runs the task every '50' ms. Since 'times' is '0',
; it runs indefinitely. The context is released after
; each run, allowing other processes to run smoothly.
(task 0 50 '
 (progn
; If the button '0' is clicked, emit an event 'led' to toggle state.
  (if
   (bclicked 0)
   (progn
    (push_event 'led
     (not
      (bool state)))))
; When the ‘led’ event is triggered, set ‘state’ to the received
; value and write to pin ‘0’, driving the LED accordingly.
  (if
   (is_event 'led)
   (progn
    (setq state
     (pop_event 'led))
    (dwrite 0 state)))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

### Deploy to A

Select board A in the sidebar, press **Compile**, then the play button. Press **Deploy** in the Emulator's header. Board A now runs the welcome script again instead of the night light from the previous guide.

### Run It on B

{% tabs %}
{% tab title="Second board" %}
Select board B in the sidebar and deploy the same script the same way. Two boards, one script, and the second deployment took ten seconds.

Now press the **BOOT/FLASH** button on A. The LED on A toggles, and so does the LED on B. Press the button on B: both toggle again. Open the **Dashboard** page and watch **My First Dashboard**: the **Switch** and **LED** widgets follow every press on either board, and toggling the **Switch** flips both boards.
{% endtab %}

{% tab title="Emulator as the second peer" %}
Go back to the scripts list and choose **Emulate** from the script's menu; that always runs without a device. The **Deploy** button is absent from the header and the title reads **Running...** without a device name. That is peer B.

Now press the **BOOT/FLASH** button on A. The LED on A toggles, and the register `0` LED on the Emulator's **Digital Write** card toggles with it. Click the Emulator's **Button Clicked** card: the card's LED flips, and so does the LED on A. Open the **Dashboard** page in another tab and watch **My First Dashboard**: the **Switch** and **LED** widgets follow every press on either peer, and toggling the **Switch** flips both.
{% endtab %}
{% endtabs %}

What happened:

* **One topic, many listeners** — `push_event` publishes `led` on your account's event topic. Every device of the account, every dashboard widget bound to `led`, and a running Emulator all receive it. A's script and B's script each pop the same event and each write the same value to their own digital output `0`.
* **The value travels, not a command** — the event carries the new state, `1` or `0`, rather than "toggle". Both peers end up in the same state no matter which one started, and they cannot drift apart.
* **Scripts are per device, events are shared** — you deployed the script twice, once per peer. Nothing was configured to connect them. Two peers with the same script agree on `led` simply because they both use the name.

{% hint style="success" %}
**Checkpoint** — pressing the button on either peer toggles both LEDs and the dashboard **Switch**, and the **Switch** toggles both peers.
{% endhint %}

## Step 3: Different Roles, Same Firmware

The doorbell needs two scripts. A becomes the button at the door: it announces a press and does nothing else. B becomes the bell: it blinks when it hears one.

### The Bell Button

Create a script called `Bell Button`. On a click it publishes `ring` with the value `1`, and about half a second later it publishes `ring` with the value `0`: a press and a release, like a real button. The half second is a countdown in a variable called `hold`. The blocks are **set hold to 0** outside a **run task** with interval `100`, and two **if** blocks inside it.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-4df8d6cd23fd9394b88b879626709c0398d21469%2Fbell_button_script.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Bell Button" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs bclicked (button_id)) ;-> Bool
;
;;; end-user-library

(define hold ())

(setq hold 0)

(task 0 100 '
 (progn
  (if
   (bclicked 0)
   (progn
    (push_event 'ring 1)
    (setq hold 5)))
  (if
   (> hold 0)
   (progn
    (setq hold
     (+ hold -1))
    (if
     (eql hold 0)
     (progn
      (push_event 'ring 0)))))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

What the script does:

* **Press** — the first **if** checks **is button 0 clicked**. On a click, **push event ring 1** publishes the press, `(push_event 'ring 1)`, and **set hold to 5** starts the countdown.
* **Countdown** — the second **if** runs while **hold > 0**. **Change hold by -1** takes one off on every pass, so five passes at 100 ms are about half a second: `(setq hold (+ hold -1))`.
* **Release** — an inner **if** with the comparison **hold = 0** publishes the release the moment the countdown ends: `(push_event 'ring 0)`. A press followed by a release is exactly what a dashboard **Push Button** widget sends, and Step 4 uses that.
* **Nothing else** — the script never touches an LED. The button peer has no idea what a bell is.

### The Bell

Create a second script called `Bell`. When a `ring` event with the value `1` arrives, it blinks the LED three times; a `ring` with any other value is taken off the queue and ignored. The blinking is a countdown too, in a variable called `blinks`. The blocks are **set blinks to 0** outside a **run task** with interval `200`, and two **if** blocks inside it.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-244575e05daadc272aa56d0539ba1f3141bf5124%2Fbell_script.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Bell" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs dwrite (pin state)) ;-> Bool
;
;;; end-user-library

(define blinks ())

(setq blinks 0)

(task 0 200 '
 (progn
  (if
   (is_event 'ring)
   (progn
    (if
     (eql
      (pop_event 'ring) 1)
     (progn
      (setq blinks 6)))))
  (if
   (> blinks 0)
   (progn
    (dwrite 0
     (eql
      (% blinks 2) 0))
    (setq blinks
     (+ blinks -1))))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

What the script does:

* **Listen** — the first **if** checks **is event ring**, true while a `ring` event waits in the queue. Inside it, an inner **if** compares **pop event ring = 1**: the pop takes the event off the queue whatever its value, so each `ring` is handled once. Without the pop it would stay in the queue and ring forever.
* **Press only** — only a value of `1` reaches **set blinks to 6**: `(if (eql (pop_event 'ring) 1) ...)`. The `0` that follows every press, from the Bell Button script or from a dashboard widget, is popped and ignored.
* **Blink** — the second **if** runs while **blinks > 0**. **Digital write** to register `0` with **remainder of blinks ÷ 2 = 0** as its value lights the LED while the count is even, and **change blinks by -1** counts down: `(dwrite 0 (eql (% blinks 2) 0))`. Six half-periods of 200 ms play as three blinks over about a second; then the count reaches `0` and the task idles until the next `ring`.
* **No sender check** — the bell does not care who rang. A board, the Emulator, or a widget: any `ring` with value `1` will do.

{% hint style="info" %}
Prefer typing? Switch the Sandbox to **Advanced** and paste the listings into the code editor instead of building blocks. Keep a script you write by hand in Advanced mode: switching back to Blockly replaces the code with what the blocks generate. See [Visual Editor vs. Code Editor](/platform/sandbox#visual-editor-vs-code-editor).
{% endhint %}

### Try It in the Emulator

Before anything reaches a board, run the bell against yourself. Start **Bell** with **Emulate** from the scripts list so it runs without a device. Then open the **Dashboard** page in another tab, enter **Edit Mode**, and add a **Push Button** widget named `Doorbell` with event `ring` and **Retain** off; Step 4 adds the rest. Save and press it: the **Digital Write** card in the Emulator blinks three times. Every press is a `1` and a `0` through the real broker, and the script rang once per press.

Stop the run before moving on.

### Deploy the Roles

{% tabs %}
{% tab title="Second board" %}
Open **Bell Button**, select board A, compile, play, and **Deploy**. Open **Bell**, select board B, compile, play, and **Deploy**.

Press the **BOOT/FLASH** button on A. Its own LED does nothing. About a second later B has finished blinking three times.
{% endtab %}

{% tab title="Emulator as the second peer" %}
Open **Bell Button**, select board A, compile, play, and **Deploy**. Then start **Bell** with **Emulate** from the scripts list. The Emulator is the bell; leave it running.

Press the **BOOT/FLASH** button on A. Its own LED does nothing. The **Digital Write** card in the Emulator blinks three times.
{% endtab %}
{% endtabs %}

Neither board was reflashed for this. B runs the plain Getting Started firmware and A still carries the extra registers from the earlier guides, but both scripts use only `bclicked 0` and `dwrite 0`, which every board in this series has. So the two are interchangeable: swap the scripts and B becomes the button and A the bell, again with no upload. The roles came entirely from the scripts, and the link between them is one word, `ring`.

{% hint style="success" %}
**Checkpoint** — a press on A makes B blink three times. A's own LED stays as it was.
{% endhint %}

## Step 4: The Dashboard as a Third Peer

Open the **Dashboard** page, enter **Edit Mode**, and add the widget still missing with **Add Widget**:

* The **Push Button** named `Doorbell`, event `ring`, **Retain** off, from Step 3.
* An **LED** named `Ringing`, event `ring`, any colour.

Save and leave edit mode. See [Widget Configuration](/platform/dashboard#widget-configuration).

Press **Doorbell**. The widget sends `1` on press and `0` on release, the same pair A sends, and B rings. Now press the physical button on A: the **Ringing** widget lights for about half a second and goes dark, because the LED widget lights while the last `ring` value is not `0` and A's script publishes the `0` after its countdown.

Three peers now share `ring`: a board that publishes it, a board or Emulator that acts on it, and a browser tab that does both. None of them was told about the other two. The dashboard is not a control plane sitting above the devices; it is one more publisher and subscriber on the same bus, which is why a widget and a script can be swapped for one another without either side noticing ([Events](/platform/dashboard#events)).

{% hint style="success" %}
**Checkpoint** — the **Doorbell** widget rings B, and a press on A flashes the **Ringing** widget. Three peers, one event name, no configuration linking them.
{% endhint %}

### Going Further

A third board with the Getting Started firmware joins the doorbell by having **Bell** deployed to it, and nothing else. It will ring together with B, and the button peer and the dashboard need no change because they never knew how many bells there were. The same goes the other way: a second button peer running **Bell Button**, or a second dashboard with its own **Doorbell**, rings every bell in the account. Event names, not device lists, are how things in Uniot find each other, and the number of peers behind a name is nobody's concern but the broker's.

## Troubleshooting

**Only one peer reacts.** Both peers must be authorized under the same account and both must run a script that handles the event. Check the **Devices** page for **Online**, and each device's **Script** tab for the script you expect. On the Emulator path, make sure the floating window still says **Running...**; a closed or **Finished** window is not a peer.

**The Emulator does not react to the board.** Stop the run and start it again with no device selected; the Emulator only queues events that arrive while it is running. If it has been running a while, note that a forever task stops after 9,999 iterations in the Emulator only, about 17 minutes at 100 ms or 33 minutes at 200 ms; see [Limits](/platform/sandbox/emulator#limits).

**The Emulator's LED flips by itself when the run starts.** If the **Switch** widget on My First Dashboard has **Retain** on, the broker replays the last `led` value to every new subscriber, and the Emulator seeds its queue from it on the first check. This is the same replay a rebooting board gets; see [Retain](/platform/dashboard#retain).

**B rings twice per press, or keeps ringing.** Twice: the bell script reacts to any value instead of only `1`, so the release `0` rings it again. Keeps ringing: the event is checked with **is event** but never popped, so it stays in the queue. Compare your blocks with the listing.

**Pressing the button on A resets its WiFi.** A long hold on the BOOT/FLASH button is the WiFi reset gesture from Getting Started. Press briefly; a click is a press and release well under a second.

**A's LED is lit all the time on ESP8266.** Nothing is wrong. The Bell Button script never writes the LED, so it stays at the level Uniot Core left it after connecting; and the bell script's "off" is a LOW level, which lights an inverted onboard LED. Wire an external LED as described in Getting Started if it bothers you.

**Old values come back.** A retained `ring` or `led` from an earlier project replays to every new peer. Keep **Retain** off on the **Doorbell** widget, or clear a stale value as described in [Retain](/platform/dashboard#retain), or use fresh event names.

**The script is deployed but a board does nothing.** Open the device's page and check the **Logs** tab; a script error stops the interpreter. See [Debugging Scripts](/general-concepts/scripting#debugging-scripts).

## What's Next

{% content-ref url="/pages/27Rbsy4W7qPBLrgqTTfF" %}
[Add a Real Sensor with a Custom Primitive](/guides/custom-primitive-sensor)
{% endcontent-ref %}

{% content-ref url="/pages/qRiqeMCWFjiPcmrG3ztl" %}
[Dashboard](/platform/dashboard)
{% endcontent-ref %}


# Add a Real Sensor with a Custom Primitive

The built-in primitives cover pins. For anything else you teach the script vocabulary a new word, written in C++. Here that word is `get_temp`, backed by a DHT22, and the script is the thermostat sketched on the [Edge Logic Deployment](/general-concepts/edge-logic-deployment) page, made real: the device reads the temperature, decides whether the "AC" runs, and reports to the dashboard. You mock the sensor in the Emulator, so the script is finished before the wiring is. About 40 minutes.

## What You'll Build

* A `get_temp` primitive in `main.cpp` that returns the temperature in tenths of a degree, as an integer.
* An Emulator mock for it, written as a small JavaScript function.
* A thermostat script that drives the breadboard LED as the "AC" with a little hysteresis, publishes `temperature` when the reading changes and `ac` when the AC switches, and takes a `setpoint` from a slider.
* A dashboard with the temperature, the setpoint and the AC state.

Primitives are the bridge between scripts and the hardware you actually have. The firmware declares the vocabulary and the script uses it; neither knows how the other is written. See [Creating Custom Primitives](/general-concepts/primitives#creating-custom-primitives).

## Prerequisites

* The board from the previous guides, with the PlatformIO project at hand. You will upload once more. The breadboard LED on GPIO13 from [Dim an LED from a Slider](/guides/dim-an-led-from-a-slider), digital output `1` since [A Night Light That Thinks for Itself](/guides/night-light), plays the AC. If it is gone, the onboard LED works as digital output `0`, inverted on ESP8266.
* A DHT22 temperature sensor, or a DHT11 with coarser readings. The three-pin modules have the pull-up resistor on board; a bare four-pin sensor needs a 10 kΩ resistor between its data pin and 3V3.
* Three jumper wires.

## Step 1: Write the Primitive

### Wire the Sensor

Connect the sensor's **VCC** (or **+**) to **3V3**, **GND** (or **-**) to **GND**, and **DATA** (or **OUT**) to **GPIO4**, labelled **D2** on NodeMCU boards and **4** on ESP32 DevKit boards. GPIO4 is free on both and has no role at boot. On a bare four-pin sensor, with the grille facing you, the pins are VCC, DATA, unused, GND from left to right; the 10 kΩ resistor goes between the first two.

### Add the Library

The Adafruit DHT library talks to the sensor, and it depends on Adafruit's Unified Sensor library. Add both to `lib_deps` in `platformio.ini`, under the Uniot Core line:

{% code title="platformio.ini (excerpt)" lineNumbers="true" %}

```ini
lib_deps =
    uniot-io/uniot-core@^0.9.0
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit Unified Sensor@^1.1.14
```

{% endcode %}

The names with spaces are the registry names; PlatformIO downloads both on the next build.

### Implement get\_temp

Open `src/main.cpp` and add four things: a second include and a `using namespace` line at the top, the sensor object and the primitive above `setup()`, and two lines in `setup()` before `Uniot.begin()`. Everything from the previous guides stays as it was.

{% code title="main.cpp (excerpt)" lineNumbers="true" %}

```c++
#include <Uniot.h>
#include <DHT.h>

using namespace uniot;

#define PIN_DHT 4  // D2 on NodeMCU, GPIO4 on ESP32

DHT dht(PIN_DHT, DHT22);  // use DHT11 if that is what you have

// (get_temp) -> temperature in tenths of a degree, or -1000 when the read failed
Object get_temp(Root root, VarObject env, VarObject list) {
  auto expeditor = PrimitiveExpeditor::describe("get_temp", Lisp::Int, 0)
                       .init(root, env, list);
  expeditor.assertDescribedArgs();

  float t = dht.readTemperature();
  if (isnan(t)) {
    return expeditor.makeInt(-1000);
  }
  return expeditor.makeInt((int)roundf(t * 10));
}

void setup() {
  // ... everything from the previous guides ...

  dht.begin();
  Uniot.addLispPrimitive(get_temp);  // before begin()

  Uniot.begin();
}
```

{% endcode %}

* **`describe` comes first** — `PrimitiveExpeditor::describe("get_temp", Lisp::Int, 0)` declares the Lisp name, the return type and the argument count, and `.init(root, env, list)` binds that to the current call. It must be the first statement: when you register a primitive, Uniot Core calls it once with empty arguments just to read this description, and leaves before the body runs. The name in `describe` is what scripts call; the C++ name is irrelevant. See [Using PrimitiveExpeditor](/general-concepts/primitives#using-primitiveexpeditor).
* **`assertDescribedArgs`** — checks the call against the description. With no arguments there is little to check, but keep the habit: a primitive with arguments gets its type errors reported for free.
* **Integers only** — UniotLisp has no floating point, so 23.4 °C travels as `234`. Scale before returning, and say so in the primitive's comment.
* **Every primitive returns something** — a failed read cannot return nothing, so pick a sentinel the real world never produces. `-1000` is minus 100 °C. The script checks for it and skips the pass. A failed read is a condition the script can handle, so it is a value rather than a `terminate()`; see [Reporting Errors](/general-concepts/primitives#reporting-errors).
* **Register before `begin()`** — `addLispPrimitive` queues the primitive; the persisted script starts inside `begin()`, and the device announces its primitives to the platform when it connects. `dht.begin()` only sets up the pin.

{% hint style="info" %}
**A primitive should read a value, not wait for one.** Everything else on the device pauses while a primitive runs. A DHT read takes about 5 ms, and the library refuses to talk to the sensor more than once every 2 s; a second call inside that window returns the previous reading at once. That is short enough to read inline. A sensor that needs a long conversation, or one you want sampled on its own schedule, is better read by a scheduler task that keeps the latest value for the primitive to return. See [Keep Primitives Short and Non-blocking](/general-concepts/primitives#keep-primitives-short-and-non-blocking).
{% endhint %}

### Upload

Connect the board via USB and upload as before:

```bash
pio run --target upload
```

The first build downloads the two libraries. The device reconnects on its own and restarts the script it had, which you replace in Step 2.

{% hint style="success" %}
**Checkpoint** — open your device's page and switch to the **Primitives** tab: `get_temp` is listed with an empty **Param Types** column and **Return Type** `Int`. The firmware described it once, in `describe()`; the platform learned it from the device.
{% endhint %}

## Step 2: Mock It in the Emulator

Open the **Sandbox** page, select your device in the sidebar, and create a script called `Thermostat`. The **Primitives** category now has a **get\_temp** block below the built-ins. It is generated from what the device reported, and it is there only while that device is selected. The first version only prints the reading: a task every 2000 ms with one **print** block holding **get\_temp**.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-c0140058734dc03070c8e1929a1d17923e8730c0%2Fthermostat_1.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Thermostat (first version)" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs get_temp ()) ;-> Int
;
;;; end-user-library

(task 0 2000 '
 (progn
  (print
   (get_temp))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

The `defjs` line in the user library block is generated with the block. If you type scripts by hand, write it yourself, or the Emulator stops with "Undefined symbol: get\_temp"; see [User Library Block](/general-concepts/primitives#user-library-block).

Press **Compile**, then play. The Emulator has no sensor, so the **get\_temp** card shows `---` as its return value: a [User Primitive](/platform/sandbox/emulator#user-primitive) card, waiting to be told what to answer. Click its gear:

* Switch **Enable Return Value** on. **Select Return Type** is locked to `Int`, because the device declared it.
* Under **Return Value Source** choose **Use Function** and paste this into **JavaScript Function**:

{% code title="Emulator mock for get\_temp" %}

```javascript
() => {
  const t = 240 + 20 * Math.sin(Date.now() / 30000);
  return Math.round(t + Math.random() * 2 - 1);
}
```

{% endcode %}

* Press **Apply**.

The function runs on every call. It draws a slow wave, about three minutes long, between 22.0 and 26.0 °C with a little noise on top, so later on the temperature crosses the setpoint by itself, in both directions. Inside the function you have `Math`, `Date.now()`, the call's arguments, and a `state` object that survives between calls; nothing else. It must be synchronous and return a finite number within 500 ms, or the card turns red with `ERR` and the run stops. See [Configuring Return Values](/platform/sandbox/emulator#configuring-return-values).

Watch the **Logs** panel: a value near `240` every two seconds, drifting. The card shows the latest one.

{% hint style="warning" %}
**Emulate** in the scripts list menu always runs without a device, and it clears the selection. Start every run in this guide with the play button in the script's header, with the device selected, so the card takes its type from the device and the Emulator can check the script against it.
{% endhint %}

### Compare With the Real Sensor

Press **Deploy** in the Emulator's header and open the device's **Logs** tab. The same script now prints the real reading: `234` for 23.4 °C. Pinch the sensor between two fingers and watch the value climb over the next few readings; let go and it falls back. A `-1000` means the read failed; see [Troubleshooting](#troubleshooting).

{% hint style="success" %}
**Checkpoint** — the Emulator's logs show a drifting value near `240`, and the device's **Logs** tab shows the room temperature in tenths, rising when you warm the sensor.
{% endhint %}

## Step 3: Decide on the Device

Back in the Sandbox, the script grows in two edits. Stop the Emulator first if it is still running.

### Turn the AC On and Off

The AC should switch on when the temperature rises above a setpoint, switch off once it has fallen half a degree below it, and stay as it is in between. A failed read must change nothing. Outside the task, add three variables: **set ac to false**, **set temp to 0** and **set setpoint to 240**. Inside the task, replace the print block with **set temp to get\_temp**, then an **if** whose condition is the **comparison** **temp > -1000**; type the minus sign into the number block. Everything else goes into that **if**'s **do** slot, as the walkthrough below describes.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-0634525e8a3112ba242c620f960d49f60d5b954d%2Fthermostat_2.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Thermostat" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs get_temp ()) ;-> Int
; (defjs dwrite (pin state)) ;-> Bool
;
;;; end-user-library

(define ac ())
(define temp ())
(define setpoint ())

(setq ac ())
(setq temp 0)
(setq setpoint 240)

(task 0 2000 '
 (progn
  (setq temp
   (get_temp))
  (if
   (> temp -1000)
   (progn
    (if
     (and
      (> temp setpoint)
      (not
       (bool ac)))
     (progn
      (setq ac #t)
      (push_event 'ac #t)))
    (if
     (and
      (< temp
       (- setpoint 5))
      (bool ac))
     (progn
      (setq ac ())
      (push_event 'ac ())))
    (dwrite 1 ac)))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

* **Read, then check** — **set temp to get\_temp** stores the reading: `(setq temp (get_temp))`. The outer **if** skips everything when it is the `-1000` sentinel, so a failed read changes nothing: the AC stays as it was and nothing is published.
* **On above the setpoint** — an **if** whose condition is an **and** joining the comparison **temp > setpoint** and **not ac**; in its **do** slot, **set ac to true** and **push event** `ac` with the value **true**. In code, `(and (> temp setpoint) (not (bool ac)))`: the AC switches on only when it is off and the reading has risen above the setpoint.
* **Off below the margin** — a second **if** with **and** joining the comparison **temp < setpoint - 5**, with an **arithmetic** block on the right, and **ac**; in its **do** slot, **set ac to false** and **push event** `ac` with the value **false**. The AC switches off only once the reading is under `setpoint - 5`, half a degree lower than where it switched on. Between the two lines the previous decision stands. That half degree is hysteresis, and it is the difference between a thermostat and a relay that chatters every time a noisy reading crosses one line. `ac` is the device's memory of its own decision.
* **Publish transitions only** — `(push_event 'ac #t)` and `(push_event 'ac ())` sit inside the two inner `if`s, so the event goes out once per switch, not once per pass. Event values are numbers: `#t` arrives as `1` and `()` as `0`. See [push event](/platform/sandbox/visual-editor/special#push-event).
* **Drive the LED every pass** — **digital write ac to pin 1** is the last block inside the outer **if**: `(dwrite 1 ac)` writes the current decision to digital output `1` on every valid reading. That is cheap, and it puts the LED right after a reboot as soon as the first reading is in.

Compile and run. Register `1` on the **Digital Write** card lights when the mocked temperature climbs past `240` and goes dark once it has dropped under `235`; in between it keeps its state. If you do not want to wait for the wave, the setpoint moves in Step 4.

### Publish and Listen

Two more jobs: publish the temperature whenever it changes, and accept a new setpoint from the dashboard. Outside the task add **set last\_sent to -1000**. Inside the outer **if**, after the digital write, add an **if** with the **comparison** **temp ≠ last\_sent**; in its **do** slot, **set last\_sent to temp** and **push event** `temperature` with the value **temp**. At the very end of the task, outside the outer **if**, add an **if** with **is event** `setpoint` as its condition and **set setpoint to pop event setpoint** in its **do** slot.

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-3a8d1f743550eb68f58956ff77f22163868a373d%2Fthermostat_3.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Thermostat" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs get_temp ()) ;-> Int
; (defjs dwrite (pin state)) ;-> Bool
;
;;; end-user-library

(define last_sent ())
(define ac ())
(define temp ())
(define setpoint ())

(setq last_sent -1000)
(setq ac ())
(setq temp 0)
(setq setpoint 240)

(task 0 2000 '
 (progn
  (setq temp
   (get_temp))
  (if
   (> temp -1000)
   (progn
    (if
     (and
      (> temp setpoint)
      (not
       (bool ac)))
     (progn
      (setq ac #t)
      (push_event 'ac #t)))
    (if
     (and
      (< temp
       (- setpoint 5))
      (bool ac))
     (progn
      (setq ac ())
      (push_event 'ac ())))
    (dwrite 1 ac)
    (if
     (not
      (eql temp last_sent))
     (progn
      (setq last_sent temp)
      (push_event 'temperature temp)))))
  (if
   (is_event 'setpoint)
   (progn
    (setq setpoint
     (pop_event 'setpoint))))))
```

{% endcode %}
{% endtab %}
{% endtabs %}

* **Only changes** — `(not (eql temp last_sent))` is true when the reading differs from the one last sent. A room moves by a tenth of a degree every few minutes, so the device is mostly silent; the Emulator's noisy mock publishes more often. The `-1000` start makes the first valid reading publish. This is the `light` event from [A Night Light That Thinks for Itself](/guides/night-light#publish-only-changes) with a step of one.
* **Listen and take** — `(is_event 'setpoint)` is true while a `setpoint` event waits, and `(pop_event 'setpoint)` takes it into the variable. From the next pass on, the comparisons use it. Without a dashboard, the `240` at the top stays in force. See [is event](/platform/sandbox/visual-editor/special#is-event) and [pop event](/platform/sandbox/visual-editor/special#pop-event).
* **Vocabulary** — the user library block declares two words, `get_temp` and `dwrite`. Everything else in the script is plain UniotLisp; nothing in it says DHT22, GPIO4 or GPIO13.

{% hint style="info" %}
Prefer typing? Switch the Sandbox to **Advanced** and paste the listing into the code editor instead of building blocks. Keep a script you write by hand in Advanced mode: switching back to Blockly replaces the code with what the blocks generate. See [Visual Editor vs. Code Editor](/platform/sandbox#visual-editor-vs-code-editor).
{% endhint %}

{% hint style="success" %}
**Checkpoint** — the script compiles and register `1` still follows the mocked temperature. The events show up in Step 4.
{% endhint %}

## Step 4: Dashboard, Then Deploy

Open the **Dashboard** page, enter **Edit Mode**, and add three widgets with **Add Widget**:

* A **Value** named `Temperature`, event `temperature`. Under **Number format** set **Factor** to `10` with **Divider**, and **Decimal digits** to `1`. The device sends `234` and the widget shows `23.4`. The device only ever deals in integers; turning them into a number a person wants to read is the dashboard's job.
* A **Slider** named `Setpoint`, event `setpoint`, **Min** `150`, **Max** `300`, **Retain** on. It speaks tenths too: `240` is 24.0 °C.
* An **LED** named `AC`, event `ac`, any colour. It lights while the last `ac` value is not `0`.

Save and leave edit mode. See [Widget Configuration](/platform/dashboard#widget-configuration).

Back in the **Sandbox**, compile and run with the Dashboard page next to the Emulator window. The **Temperature** widget follows the mock, changing every two seconds. Drag the **Setpoint** slider below the current reading: register `1` on the **Digital Write** card lights and the **AC** widget with it. Now drag it just above the reading, by less than half a degree: the AC stays on. The reading is below the setpoint but not below the margin, so the device keeps its decision. Drag the slider further up and the AC goes off. Everything goes through the real broker, so the widgets see the Emulator exactly as they will see the device ([Events](/platform/sandbox/emulator#events)).

Press **Deploy** in the Emulator's header. On its first pass the device finds the retained `setpoint` waiting, reads the real sensor, and decides. Set the slider a few tenths above the room temperature and pinch the sensor: within a couple of readings the breadboard LED comes on and the **AC** widget lights. Let go, and both go off once the reading has fallen half a degree under the setpoint.

{% hint style="success" %}
**Checkpoint** — the **Temperature** widget shows the room in tenths, the slider moves the point where the breadboard LED switches, and the **AC** widget follows the LED.
{% endhint %}

### Going Further

The script's only hardware words are `get_temp` and `dwrite`. A board with a different sensor, a DS18B20 on a one-wire bus, a thermistor on an analog pin or an I2C chip, runs this exact script unchanged as long as its firmware answers `get_temp` in tenths of a degree. The same goes the other way: a `get_humidity` primitive on this board is another twenty lines of C++, and every script in your account can use it from the moment the device reconnects, because the device tells the platform what it speaks. Firmware declares vocabulary, scripts use it, and the platform draws the blocks.

## Troubleshooting

**Always `-1000`.** Wiring first: DATA on GPIO4, VCC on 3V3, GND on GND, and the pull-up on a bare sensor. Then the type: a DHT11 declared as `DHT22` reads garbage or nothing; change the constructor argument. A single `-1000` right after boot is normal, the sensor needs about a second to settle.

**Build error mentioning `Object`, `Root` or `PrimitiveExpeditor`.** The `using namespace uniot;` line is missing or sits below the primitive. Both `#include` lines and the `using` line go at the top of `main.cpp`. An error about `DHT.h` not being found means the `lib_deps` lines did not make it into `platformio.ini`.

**The get\_temp card says "Unavailable in the selected device".** The selected device has not reported `get_temp`: it was not reflashed with Step 1, it has not reconnected yet, or another device is selected. Check the device's **Primitives** tab. The **Deploy** confirmation warns about the same thing.

**The card turns red with `ERR` and the run stops.** The mock threw, returned something other than a finite number, or took longer than 500 ms. The **Logs** panel has the reason on a line starting with `get_temp: User function error`. Open the gear, fix the function, **Apply**, and run again.

**"Undefined symbol: get\_temp" when compiling.** The script was typed by hand without the `; (defjs get_temp ()) ;-> Int` line in the user library block, or the block was removed. The Visual Editor writes it; in the Code Editor you do.

**The get\_temp block is missing from the toolbox.** No device is selected, or the selected device has not reported the primitive. Select the device in the sidebar; the **Primitives** category is rebuilt from what it reports.

**The Temperature widget shows `234`.** **Factor** is `0`, which means off, or **Decimal digits** is `0`. Set **Factor** `10` with **Divider** and **Decimal digits** `1`.

**The AC flickers near the setpoint.** The second comparison lost its `- 5`, so on and off share one line and a noisy reading crosses it every pass. Compare your blocks with the listing; the margin is the point.

**The slider does nothing.** The widget's **Event** and the script's event name differ; names are case-sensitive. Or the slider sits outside the room's range: `150` to `300` is 15 to 30 °C, and a setpoint the room never reaches never switches anything.

**The reading on the device repeats or never changes.** The DHT22 answers at most once every 2 s; a faster task gets the previous reading back. Keep the 2000 ms interval. A value stuck for minutes while you warm the sensor is a wiring or pull-up problem; see the first entry.

**The script is deployed but nothing happens.** Open the device's page and check the **Logs** tab; a script error stops the interpreter. See [Debugging Scripts](/general-concepts/scripting#debugging-scripts).

## What's Next

{% content-ref url="/pages/JIifa7nkIHMYDMTA9ek2" %}
[Uniot Badge](/guides/uniot-badge)
{% endcontent-ref %}

{% content-ref url="/pages/DHslxsJgBqyheEr05Iyh" %}
[Primitives](/general-concepts/primitives)
{% endcontent-ref %}

{% content-ref url="/pages/VgA087SOFbR4tbJjOUDM" %}
[Emulator](/platform/sandbox/emulator)
{% endcontent-ref %}


# Uniot Badge

The Uniot Badge is a mini IoT development board. Made in the size of a credit card, this badge is designed to be both a fun gadget and a showcase of what IoT can achieve. The board has several interactive components, including colorful LEDs, a button, a vibration motor, and a sensor, all packed into a sleek form factor.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-6ea8328643e7bd430fd3524bb01f412a844116b8%2Funiot_badge.png?alt=media" alt=""><figcaption><p>Uniot Badge</p></figcaption></figure>

## Hardware Overview

The Uniot Badge is equipped with the following hardware components:

* **ESP32-C3 Dev Board**: The main microcontroller board that runs on the [Uniot Core](/advanced/uniot-core).
* **VL53L0X ToF Sensor**: A time-of-flight sensor for distance measurement.
* **Button**: A button for user interaction.
* **Vibration Motor**: A small motor that can vibrate to provide haptic feedback.
* **10x addressable RGB LEDs**: A circle of 10 RGB LEDs for displaying colors and patterns (NeoPixel-like).

## Getting Started

The badge comes with its firmware installed. To connect it to your account:

1. Power it over USB-C. With no WiFi stored, it opens a WiFi network named `UNIOT-…`. Join it from your phone or computer.
2. On the page that opens, enter your WiFi details and your Uniot account ID.
3. When the badge is online, authorize it on the **Devices** page — see [Authorize the Device](/guides/getting-started#authorize-the-device).

To update the firmware, or to install it on a badge that doesn't have it, use the [Firmware Installer](/platform/installer) at [install.uniot.io](https://install.uniot.io). An update keeps the badge's WiFi settings, identity and script.

## Promo

You can redeem the promo code by scanning the QR code on your badge. This promo code will extend your plan by:

* **Device: +1**
* **Dashboard: +1**
* **Script: +1**

A script and a dashboard, both named 'Uniot Promo Badge,' will also be created on the platform for demonstration purposes. You can deploy the script to the badge and try it out in interaction with the dashboard.

Learn more about this script below in the [Script Examples](#promo-badge) section. If you wish, you can delete these dashboard and script and create your own (the plan extension will remain).

## Functionality Overview

The Uniot Badge can light up, vibrate, measure distances, and even react to your actions. With just the press of a button, you can activate various features like making lights flash in patterns or making the badge respond to movement.

### Primitives

To interact with peripherals, the firmware has corresponding primitives. Read more about primitives [here](/general-concepts/primitives).

#### vibro

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-c9409ae59539451652702e9f23887ee96ae7f928%2Funiot_badge_primitive_vibro.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Vibrates the specified number of times (the period of one vibration is set in the [firmware](https://github.com/uniot-io/uniot-promo-badge-firmware/blob/1.0.0/lib/Vibro/Vibro.h#L8)).

**Parameters:**

* **Times** (Number): Number of vibrations

#### tof\_distance

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-127b85330d5400e8dbca03b82bd01c7b8a716345%2Funiot_badge_primitive_tof.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Reads the value of the distance sensor.

**Returns:**

* **Distance** (Number): Distance to obstacle in **millimeters**

#### pixel\_clear

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-2220748287ea60602ca65dffeeeb0f5692ade285%2Funiot_badge_primitive_pixel_clear.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Turns all LEDs off.

#### pixel\_set

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-765451cd9537aa0b1307480851f691460ab0c0d6%2Funiot_badge_primitive_pixel_set.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Sets an LED's color.

**Parameters:**

* **Index** (Number): LED index (**0-9**)
* **Red** (Number): Red color brightness (**0-255**)
* **Green** (Number): Green color brightness (**0-255**)
* **Blue** (Number): Blue color brightness (**0-255**)

#### pixel\_show

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-ab98759ac92855e7ce2cd8b8342ff272bd527134%2Funiot_badge_primitive_pixel_show.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Turns on the set LEDs.

## Firmware

The Uniot Badge is powered by firmware based on [Uniot Core](/advanced/uniot-core). Its source, and every release, are in the [uniot-promo-badge-firmware](https://github.com/uniot-io/uniot-promo-badge-firmware) repository.

Each release comes in two builds, which differ only in WiFi transmit power:

* **Compatible** works on every badge, at a shorter range. Badges ship with it.
* **Full range** transmits at the chip's full power, for a badge whose radio copes with it.

The [Firmware Installer](/platform/installer) shows which version and build a badge is running, and installs either one — see [Which radio build](/platform/installer#which-radio-build). It can also install a hardware test that checks the LEDs, the sensor, the button and the motor without a network or an account.

Power-cycling the badge five times in quick succession clears its WiFi settings and opens the `UNIOT-…` network again. It helps when you've moved the badge to another network.

## Script Examples

### Promo Badge

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-aabf3acbc7aebc78a1ca0e78f27902f7e12224f3%2Funiot_badge_script_promo.svg?alt=media" alt=""><figcaption><p>The visual script</p></figcaption></figure>

The visual script generates the following code:

{% code lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs bclicked (button_id)) ;-> Bool
; (defjs vibro (_0)) ;-> Bool
; (defjs tof_distance ()) ;-> Int
; (defjs pixel_clear ()) ;-> Bool
; (defjs pixel_set (_0 _1 _2 _3)) ;-> Bool
; (defjs pixel_show ()) ;-> Bool
;
;;; end-user-library

(define led ())
(define red ())
(define run ())
(define led_fill ())
(define green ())
(define rise ())
(define blue ())
(define delta ())
(define distance ())
; Re-maps a number from one range to another.
(defun map
 (x in_min in_max out_min out_max)
 (setq run
  (- in_max in_min))
 (setq rise
  (- out_max out_min))
 (setq delta
  (- x in_min))
 (/
  (* delta rise)
  (+ run out_min)))

(setq led 0)
(setq led_fill ())

(setq red 10)
(setq green 10)
(setq blue 10)

(task 0 80 '
 (progn
  (if
   (is_event 'red)
   (progn
    (setq red
     (pop_event 'red))))
  (if
   (is_event 'green)
   (progn
    (setq green
     (pop_event 'green))))
  (if
   (is_event 'blue)
   (progn
    (setq blue
     (pop_event 'blue))))
  (if
   (bclicked 0)
   (progn
    (setq led_fill
     (not
      (bool led_fill)))
    (vibro 2)))
  (setq distance
   (tof_distance))
  (setq led
   (map distance 40 360 0 10))
  (pixel_clear)
  (if
   (bool led_fill)
   (progn
    (while
     (< #itr
      (+ led 1))
     (pixel_set #itr red green blue)))
   (progn
    (pixel_set led red green blue)))
  (pixel_show)
  (if
   (is_event 'capture)
   (progn
    (if
     (eql
      (pop_event 'capture) 1)
     (progn
      (push_event 'distance distance)))))))
```

{% endcode %}

To demonstrate the functionality of the badge, this script uses several blocks:

* **Variables Initialization**: The blocks initializes several variables.
  * **`led`**: The number of active (glowing) LEDs.
  * **`led_fill`**: The indicator of the pattern according to which the LEDs will turn on.
  * **`red`**: The brightness of the red color of the LEDs.
  * **`green`**: The brightness of the green color of the LEDs.
  * **`blue`**: The brightness of the blue color of the LEDs.
* **Function Block**: Function `map` accepts 5 arguments and performs a linear interpolation to calculate the number of active LEDs relative to the distance sensor value.
  * **`x`**: The distance sensor value
  * **`in_min`**: The minimum value of the distance sensor (in **millimeters**).
  * **`in_max`**: The maximum value of the distance sensor (in **millimeters**).
  * **`out_min`**: The minimum number of active LEDs.
  * **`out_max`**: The maximum number of active LEDs.
* **Run Task Block**: Configured to run every 80 milliseconds indefinitely (as the `times` parameter is set to `0`).
* **Color Events Trigger Block**: Responds to the `red`/`green`/`blue` events by updating corresponding variables to the event's value.
* **Button Check Block**: Monitors if the button (that occupies the register with index `0`, learn more about registers [here](/general-concepts/primitives#the-register-system)) is clicked. When clicked, inverts the state of the `led_fill` variable and calls `vibro` primitive to provide a haptic feedback.
* **Distance Sensor Reading**: Calls the `tof_distance` primitive and sets the value of the sensor to the `distance` variable.
* **Determining Active LEDs**: Calls the `map` function to calculate the number of active LEDs relative to `distance`.
* **Update LEDs Block**: Updates LEDs according to current data.
  * **Turn Off Block**: Calls the `pixel_clear` primitive to turn off all LEDs.
  * **Set Color Block**: Set the color for the active LED(s) according to the pattern (`led_fill`) using the `pixel_set` primitive.
  * **Turn On Block**: Calls the `pixel_show` primitive to turn on the set LEDs.
* **Event Trigger Block**: Responds to the `capture` event by emmiting a global event `distance` with the `distance` variable value. Checks whether the value of the `capture` event is equal to `1` to generate the `distance` event only once when the corresponding Push Button widget is pressed on the dashboard (this widget generates an event with a value of `1` when pressed and an event with a value of `0` when the button is released)

### Health and Wellness Reminder

With this script, you can turn the badge into a personal reminder tool. It can vibrate at set frequencies to remind you to take a break, drink water, or stretch. The LEDs are also used to show different colors based on the type of reminder – blue for hydration, green for stretching, red for break.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-fcbe899349f0baa64ae493c7efa5018d4ff96a44%2Funiot_badge_script_reminder.svg?alt=media" alt=""><figcaption><p>The visual script</p></figcaption></figure>

The visual script generates the following code:

{% code lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs vibro (_0)) ;-> Bool
; (defjs pixel_clear ()) ;-> Bool
; (defjs pixel_set (_0 _1 _2 _3)) ;-> Bool
; (defjs pixel_show ()) ;-> Bool
; (defjs bclicked (button_id)) ;-> Bool
;
;;; end-user-library

(define WATER_INTERVAL ())
(define water_timer ())
(define current_reminder ())
(define red ())
(define BREAK_INTERVAL ())
(define break_timer ())
(define TASK_INTERVAL ())
(define green ())
(define STRETCH_INTERVAL ())
(define stretch_timer ())
(define blue ())
(define active ())
; Describe this function...
(defun process_reminder
 (type timer)
 (if
  (<= timer 0)
  (progn
   (setq active #t)
   (setq current_reminder type)
   (if
    (eql current_reminder 1)
    (progn
     (setq blue 255))
    (if
     (eql current_reminder 2)
     (progn
      (setq red 255))
     (if
      (eql current_reminder 3)
      (progn
       (setq green 255)))))
   (vibro 3)
   (pixel_clear)
   (while
    (< #itr 10)
    (pixel_set #itr red green blue))
   (pixel_show))))
; Reset active reminder
(defun reset_reminder ()
 (if
  (eql current_reminder 1)
  (progn
   (setq water_timer WATER_INTERVAL)
   (setq blue 0)))
 (if
  (eql current_reminder 2)
  (progn
   (setq break_timer BREAK_INTERVAL)
   (setq red 0)))
 (if
  (eql current_reminder 3)
  (progn
   (setq stretch_timer STRETCH_INTERVAL)
   (setq green 0)))
 (setq active ())
 (setq current_reminder 0)
 (pixel_clear)
 (pixel_show))

(setq WATER_INTERVAL
 (* 60
  (* 60 1000)))
(setq BREAK_INTERVAL
 (* 180
  (* 60 1000)))
(setq STRETCH_INTERVAL
 (* 90
  (* 60 1000)))

(setq water_timer WATER_INTERVAL)
(setq break_timer BREAK_INTERVAL)
(setq stretch_timer STRETCH_INTERVAL)

(setq current_reminder 0)
(setq TASK_INTERVAL 200)

(setq red 0)
(setq green 0)
(setq blue 0)

(task 0 TASK_INTERVAL '
 (progn
  (setq water_timer
   (+ water_timer
    (- TASK_INTERVAL)))
  (setq break_timer
   (+ break_timer
    (- TASK_INTERVAL)))
  (setq stretch_timer
   (+ stretch_timer
    (- TASK_INTERVAL)))
  (if
   (not
    (bool active))
   (progn
    (process_reminder 1 water_timer)))
  (if
   (not
    (bool active))
   (progn
    (process_reminder 2 break_timer)))
  (if
   (not
    (bool active))
   (progn
    (process_reminder 3 stretch_timer)))
  (if
   (bclicked 0)
   (progn
    (reset_reminder)))))
```

{% endcode %}

Let's take a closer look at the individual parts of the script:

* **Variables Initialization**
  * **`TASK_INTERVAL`**: The task execution interval (in **ms**).
  * **`WATER_INTERVAL`**: How often you want to drink water (every 60 minutes, in **ms**).
  * **`BREAK_INTERVAL`**: How often you want to take a break (every 180 minutes, in **ms**).
  * **`STRETCH_INTERVAL`**: How often you want to stretch (every 90 minutes, in **ms**).
  * **`water_timer`**: The water timer, set to the corresponding frequency value.
  * **`break_timer`**: The break timer, set to the corresponding frequency value.
  * **`stretch_timer`**: The stretch timer, set to the corresponding frequency value.
  * **`red`**: The brightness of the red color of the LEDs.
  * **`green`**: The brightness of the green color of the LEDs.
  * **`blue`**: The brightness of the blue color of the LEDs.
  * **`current_reminder`**: The numeric value of the reminder (`1` for water, `2` for break, `3` for stretch).
* **Run Task Block**: Configured to run every `TASK_INTERVAL` milliseconds indefinitely (as the `times` parameter is set to `0`).
* **Process Reminder Block**: The `process_reminder` function processes provided reminder type (numeric value of the reminder) and it's corresponding timer (`timer`). If the timer is expired:
  * **Set Info**: Sets variable `active` as `true` (an indicator that there is an active reminder). Sets variable `current_reminder` to the numeric value of the reminder (the `type` argument passed to the function).
  * **Set Color**: Sets color according to the `current_reminder`.
  * **Notification Block**: Runs vibration motor and turns LEDs on by calling the corresponding primitives.
* **Reset Reminder Block**: The `reset_reminder` function, as the name implies, resets the current reminder.
* **Update Timers Block**: Reduces the value of the timers by `TASK_INTERVAL` every task execution.
* **Check Reminders Block**: Checks each type of reminder by calling the `process_reminder` function if no reminder is active (do not processes new reminders until the current one is acknowledged).
* **Button Check Block**: Monitors if the button is clicked. When clicked, resets current reminder by calling the `reset_reminder` function.

### Gesture-Based Appliance Control

With the following script the badge can act as as a control device for household appliances. There is a tap gesture (quick down-up motion) detection. The appliance state switched when tap is detected.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-89ad9bb86bc7b4c5d2ee86e835e8f9cd1661c411%2Funiot_badge_script_appliance_control.svg?alt=media" alt=""><figcaption><p>The visual script</p></figcaption></figure>

The visual script generates the following code:

{% code lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs tof_distance ()) ;-> Int
; (defjs pixel_clear ()) ;-> Bool
; (defjs pixel_set (_0 _1 _2 _3)) ;-> Bool
; (defjs pixel_show ()) ;-> Bool
;
;;; end-user-library

(define TAP_DURATION ())
(define TASK_INTERVAL ())
(define TAP_THRESH_MIN ())
(define distance ())
(define TAP_THRESH_MAX ())
(define state ())
(define tap_timer ())
(define tap_state ())
; Describe this function...
(defun trackGesture ()
 (if
  (eql tap_state 0)
  (progn
   (if
    (and
     (>= distance TAP_THRESH_MIN)
     (<= distance TAP_THRESH_MAX))
    (progn
     (setq tap_state 1)
     (setq tap_timer TAP_DURATION))))
  (progn
   (setq tap_timer
    (+ tap_timer
     (- TASK_INTERVAL)))
   (if
    (<= tap_timer 0)
    (progn
     (setq tap_state 0)
     (setq tap_timer 0))
    (if
     (and
      (eql tap_state 1)
      (< distance TAP_THRESH_MIN))
     (progn
      (setq tap_state 2))
     (if
      (and
       (eql tap_state 2)
       (and
        (>= distance TAP_THRESH_MIN)
        (< distance TAP_THRESH_MAX)))
      (progn
       (push_event 'appliance-state
        (not
         (bool state)))
       (setq tap_state 0)
       (setq tap_timer 0))))))))
; Describe this function...
(defun updateLEDs ()
 (pixel_clear)
 (if
  (is_event 'appliance-state)
  (progn
   (setq state
    (eql
     (pop_event 'appliance-state) 1))))
 (if
  (bool state)
  (progn
   (while
    (< #itr 10)
    (pixel_set #itr 0 5 0))))
 (pixel_show))

(setq TAP_DURATION 400)
(setq TAP_THRESH_MIN 150)
(setq TAP_THRESH_MAX 500)
(setq tap_state 0)
(setq tap_timer 0)

(setq TASK_INTERVAL 30)
(setq distance 0)
(setq state ())

(task 0 TASK_INTERVAL '
 (progn
  (setq distance
   (tof_distance))
  (trackGesture)
  (updateLEDs)))
```

{% endcode %}

The script consist of the following parts:

* **Variables Initialization**
  * **`TASK_INTERVAL`**: The frequency of task execution (in **ms**).
  * **`distance`**: The current value of the distance sensor.
  * **`state`**: The appliance state (switched by a tap).
  * **`tap_state`**: The current gesture state:
    * **`0`**: No tap in progress.
    * **`1`**: Hand started moving within the thresholds.
    * **`2`**: Hand detected below minimum threshold.
    * **`3`**: Hand is back within the thresholds.
  * **`TAP_DURATION`**: The maximum allowed time (in **ms**) for a valid tap.
  * **`TAP_THRESH_MAX`**: The sensor distance below which gesture detection starts.
  * **`TAP_THRESH_MIN`**: The sensor distance below which a tap state is considered as **`2`**.
  * **`tap_timer`**: The timer to track a tap validity.
* **Run Task Block**: Configured to run every `TASK_INTERVAL` milliseconds indefinitely (as the `times` parameter is set to `0`).
* **Distance Sensor Reading**: Calls the `tof_distance` primitive and sets the value of the sensor to the `distance` variable.
* **Gesture Recognition Block**: The `trackGesture` function determines whether a tap has been performed and emits a global event if it has.
* **Update LEDs Block**: Listens to a global event, changes the local state and updates LEDs according to this state.

### Appliance Control and Security Alarm

With a few additions to the previous script, the device can turn into a motion detector with just one event (this event can be generated by a widget on the dashboard or by another device).

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-5390cae95db90c96ab679e557eac91cf54363690%2Funiot_badge_script_appliance_control_alarm.svg?alt=media" alt=""><figcaption><p>The visual script</p></figcaption></figure>

The visual script generates the following code:

{% code lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs tof_distance ()) ;-> Int
; (defjs pixel_clear ()) ;-> Bool
; (defjs pixel_set (_0 _1 _2 _3)) ;-> Bool
; (defjs pixel_show ()) ;-> Bool
;
;;; end-user-library

(define TASK_INTERVAL ())
(define TAP_DURATION ())
(define distance ())
(define TAP_THRESH_MIN ())
(define state ())
(define TAP_THRESH_MAX ())
(define tap_timer ())
(define is_detector ())
(define tap_state ())
(define fixed_distance ())
; Describe this function...
(defun trackIntrusion ()
 (if
  (not
   (eql distance fixed_distance))
  (progn
   (push_event 'alarm 1))))
; Describe this function...
(defun trackGesture ()
 (if
  (eql tap_state 0)
  (progn
   (if
    (and
     (>= distance TAP_THRESH_MIN)
     (<= distance TAP_THRESH_MAX))
    (progn
     (setq tap_state 1)
     (setq tap_timer TAP_DURATION))))
  (progn
   (setq tap_timer
    (+ tap_timer
     (- TASK_INTERVAL)))
   (if
    (<= tap_timer 0)
    (progn
     (setq tap_state 0)
     (setq tap_timer 0))
    (if
     (and
      (eql tap_state 1)
      (< distance TAP_THRESH_MIN))
     (progn
      (setq tap_state 2))
     (if
      (and
       (eql tap_state 2)
       (and
        (>= distance TAP_THRESH_MIN)
        (< distance TAP_THRESH_MAX)))
      (progn
       (push_event 'appliance-state
        (not
         (bool state)))
       (setq tap_state 0)
       (setq tap_timer 0))))))))
; Describe this function...
(defun updateLEDs ()
 (pixel_clear)
 (if
  (is_event 'appliance-state)
  (progn
   (setq state
    (eql
     (pop_event 'appliance-state) 1))))
 (if
  (bool state)
  (progn
   (while
    (< #itr 10)
    (pixel_set #itr 0 5 0))))
 (pixel_show))
; Describe this function...
(defun guardCheck ()
 (if
  (is_event 'protection)
  (progn
   (setq is_detector
    (eql
     (pop_event 'protection) 1))
   (if
    (bool is_detector)
    (progn
     (setq fixed_distance distance))
    (progn
     (push_event 'alarm 0))))))

(setq TASK_INTERVAL 30)
(setq distance 0)
(setq state ())
(setq is_detector ())
(setq fixed_distance 0)

(setq TAP_DURATION 400)
(setq TAP_THRESH_MIN 150)
(setq TAP_THRESH_MAX 500)
(setq tap_state 0)
(setq tap_timer 0)

(task 0 TASK_INTERVAL '
 (progn
  (setq distance
   (tof_distance))
  (guardCheck)
  (if
   (bool is_detector)
   (progn
    (trackIntrusion))
   (progn
    (trackGesture)
    (updateLEDs)))))
```

{% endcode %}

This script has everything from the previous script and the following additions:

* **Variables Initialization**
  * **`is_detector`**: Indicates that the device is operating as a motion detector.
  * **`fixed_distance`**: Stores the value from the distance sensor at the time of arming
* **Intrusion Recognition Block**: The `trackIntrusion` function checks whether the value from the distance sensor has changed after arming, and if so, generates a global event `alarm` with value `1`.
* **Check Guard Block**: Responds to the `protection` event by updating the `is_detector` variable to the event's value. Fixes the current value of the distance sensor if the device is in armed mode, or generates an `alarm` event with a value of `0` if the device is in the switch mode.


# Edge Logic Deployment

## **Why** Uniot uses Edge Logic Deployment instead of imperative or declarative models

At Uniot, we have chosen **Edge Logic Deployment** as the foundation of our system. This approach allows for the **dynamic deployment of executable logic** to edge devices, where it is processed by the **Edge Logic Engine**, a runtime responsible for **interpreting and executing automation scripts locally**. Unlike traditional IoT models that rely on centralized cloud control, this system enables **autonomous decision-making at the device level**. The **Edge Logic Engine** runs deployed logic using [UniotLisp](/advanced/uniot-lisp), a lightweight scripting language optimized for real-time execution in resource-constrained environments. Together, these components ensure that devices can **react to events, execute scripts efficiently, and adapt to changing conditions - without requiring constant network connectivity or firmware updates**.

Traditional IoT systems rely on **imperative** or **declarative** control models, but both have limitations that make them inefficient for scalable, adaptive automation. Below, we compare these models and explain why **Edge Logic Deployment** is more powerful.

***

## **Traditional IoT control models and their limitations**

### **Imperative model: direct commands**

In the **imperative model**, a controller (cloud server, app, or user interface) sends **explicit commands** to devices. The device simply executes what it is told, with no autonomy.

**Example: Controlling a Smart Light (Imperative)**

```json
{"command": "turn_on", "device": "light1"}
```

Or, in an RPC-style API:

```json
{"method": "setRGB", "params": {"r": 255, "g": 0, "b": 0}}
```

The device does not make any decisions—it just follows instructions.

**Limitations of the Imperative model**

❌ **No autonomy** – The device does not act on its own.\
❌ **High network dependency** – Every action requires a real-time command.\
❌ **Not scalable** – Managing many devices with direct commands is inefficient.

### **Declarative model: desired state control**

The **declarative model** defines a **goal** or desired state, and the system determines how to achieve it.

**Example: Smart Thermostat (Declarative)**

```json
{"desired_state": {"temperature": 22}}
```

The system ensures that the temperature stays at **22°C**, automatically controlling the heater or fan.

**Limitations of the Declarative model**

❌ **Limited expressiveness** – It only specifies **what** should happen, not **how**.\
❌ **Still requires centralized logic** – The controller interprets and enforces the desired state.\
❌ **Difficult to handle edge cases** – Complex automation rules don’t fit well.

***

## **Why Uniot uses Edge Logic Deployment**

Instead of sending direct commands (**imperative**) or defining a target state (**declarative**), Uniot uses **Edge Logic Deployment**:

✅ **Logic is sent to the device dynamically**\
✅ **The device executes the logic autonomously** via the **Edge Logic Engine**\
✅ **No need for constant network communication**

#### **Example: Smart Thermostat using Edge Logic Deployment**

Instead of sending a one-time command, Uniot deploys **a script** that **runs directly on the device**:

```lisp
(task 0 1000 '
 (if
  (>
   (get_temp) 25)
  (turn_on_ac)
  (print '
   (Temperature is fine! No need to turn on AC))))
```

* The **Edge Logic Engine** on the device **evaluates the condition** in real time.
* If the temperature **exceeds 25°C**, it turns on the AC.
* If not, it **logs a message** instead of executing unnecessary commands.

This means:\
✔ **Devices are autonomous** – they don’t wait for cloud instructions.\
✔ **Network load is reduced** – less traffic from frequent commands.\
✔ **Logic can adapt dynamically** – new scripts can be deployed **without firmware updates**.

### **How Edge Logic Deployment compares to traditional models**

| Feature                   | **Imperative**  | **Declarative** | **Edge Logic Deployment (Uniot)** |
| ------------------------- | --------------- | --------------- | --------------------------------- |
| **Execution Model**       | Direct commands | Desired state   | Scripts run at the edge           |
| **Device Autonomy**       | ❌ No            | ⚠️ Limited      | ✅ Full autonomy                   |
| **Network Dependency**    | ❌ High          | ⚠️ Medium       | ✅ Low                             |
| **Complex Logic Support** | ❌ No            | ⚠️ Limited      | ✅ Yes                             |
| **Real-Time Adaptation**  | ❌ No            | ⚠️ Somewhat     | ✅ Yes                             |
| **Scalability**           | ❌ Hard          | ⚠️ Moderate     | ✅ High                            |

#### **Why this is better**

* **More powerful than declarative models** → Instead of just setting a target state, devices can **execute complex logic**.
* **More flexible than imperative models** → Devices aren’t just passive receivers of commands; they can **react dynamically**.
* **Enables true edge computing** → Instead of relying on the cloud, **logic runs locally** on the **Edge Logic Engine**.

***

## **Why this is not the same as low-level firmware scripting**

A common misunderstanding is that **Edge Logic Deployment** is just a form of low-level scripting, like **Lua on ESP8266** or **MicroPython on microcontrollers**. However, **Uniot’s approach is fundamentally different**.

### **Differences between Edge Logic Deployment and low-level firmware scripting**

| Feature             | **Firmware Scripting (Lua/MicroPython)**       | **Edge Logic Deployment (Uniot)**                        |
| ------------------- | ---------------------------------------------- | -------------------------------------------------------- |
| **Execution Model** | Installed at flash level, runs continuously    | Logic is dynamically deployed and updated                |
| **Update Method**   | Requires full firmware update or manual access | Logic updates are sent over the network                  |
| **Scalability**     | Limited to specific firmware versions          | Any device with an Edge Logic Engine can run new scripts |
| **Device Autonomy** | Requires manual scripting per device           | Centralized deployment, but execution is local           |
| **Flexibility**     | Requires device-specific implementation        | Works across multiple devices without modification       |
| **Security**        | Full access to device internals                | Runs in a controlled environment with restrictions       |

***

### **Key reasons this is different**

✔ **Dynamically deployed:** Unlike MicroPython or Lua, logic is **not hardcoded** – it is deployed at runtime.\
✔ **Works across devices:** The same logic can run on **multiple devices**, without modifying firmware.\
✔ **Security & Control:** The **Edge Logic Engine** runs logic in **a controlled environment**, ensuring **safety**.\
✔ **Uses UniotLisp as a Virtual Machine:** The **Edge Logic Engine** runs [UniotLisp](/advanced/uniot-lisp), an optimized Lisp-based scripting language, making execution **lightweight, portable, and efficient**.\
✔ **No firmware flashing required:** New logic can be **sent remotely**, unlike firmware scripts that need manual updates.

Instead of treating IoT devices as **programmable boards** (like Arduino with MicroPython), **Uniot treats them as smart agents that receive and execute logic dynamically**.

***

## **Conclusion: Edge Logic Deployment is the Future**

By **sending scripts instead of commands or states**, Uniot enables **true edge computing** where devices:

* **Execute autonomous logic locally**
* **Adapt dynamically to real-world conditions**
* **Require less cloud dependency**
* **Scale easily to large networks**

This shift from **command-driven control** to **dynamic logic deployment** is a **game-changer** in IoT, edge computing, and automation.


# Scripting

## Overview

Scripting is a core feature of Uniot that enables dynamic device behavior through remote code execution. Instead of hardcoding all logic in firmware, you can send scripts to devices via MQTT, allowing you to:

* **Update logic without reflashing**: Change device behavior instantly
* **Deploy different behaviors**: Same firmware, different scripts per device
* **A/B test features**: Try different algorithms on different devices
* **Rapid prototyping**: Test ideas without compilation cycles
* **User customization**: Let end-users define their own automation rules

Scripts are written in **UniotLisp**, a lightweight Lisp dialect optimized for embedded systems, and executed by the on-device interpreter.

## Workflow

### Script Creation

Uniot provides two ways to create scripts, catering to different user skill levels:

#### Visual Editor (Blockly)

A drag-and-drop interface for users who prefer graphical programming:

* **Block-based Programming**: Drag blocks representing actions, conditions, and logic
* **No Syntax Knowledge Required**: Visual blocks prevent syntax errors
* **Automatic Code Generation**: Blocks are converted to UniotLisp code
* **Instant Preview**: See the generated Lisp code in real-time
* **Perfect for**: Beginners, quick prototyping, common automation tasks

**Example Visual Blocks**:

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-e873369a9826dc487e5daaaf08baf87f7b254493%2Fscripting_example_1.svg?alt=media" alt=""><figcaption></figcaption></figure>

**Generated UniotLisp Code**:

```lisp
(task 0 100 '
 (progn
  (if
   (bclicked 0)
   (progn
    (dwrite 0 #t)))))
```

#### Manual Code Editor

A text-based editor for advanced users who want full control:

* **Full Language Access**: Use all UniotLisp features and primitives
* **Syntax Highlighting**: Visual feedback for code structure
* **Error Detection**: When you compile, the line with an error is marked and the expression that failed is underlined
* **Perfect for**: Advanced users, complex logic, performance-critical code

For example, **"My First Script"**, the script every new account comes with, as UniotLisp code:

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs bclicked (button_id)) ;-> Bool
; (defjs dwrite (pin state)) ;-> Bool
;
;;; end-user-library

(define state ())

(setq state ())

; Runs the task every '50' ms. Since 'times' is '0',
; it runs indefinitely. The context is released after
; each run, allowing other processes to run smoothly.
(task 0 50 '
 (progn
; If the button '0' is clicked, emit an event 'led' to toggle state.
  (if
   (bclicked 0)
   (progn
    (push_event 'led
     (not
      (bool state)))))
; When the ‘led’ event is triggered, set ‘state’ to the received
; value and write to pin ‘0’, driving the LED accordingly.
  (if
   (is_event 'led)
   (progn
    (setq state
     (pop_event 'led))
    (dwrite 0 state)))))
```

### Script Delivery

Scripts are delivered to devices through MQTT:

```
Uniot Platform → MQTT Broker → Device → UniotLisp Interpreter → Execution
```

**Delivery Flow**:

1. **User creates/edits script** in the [Sandbox](/platform/sandbox)
2. **The script is sent to the device** over MQTT. A device that is offline receives it when it next connects.
3. **The device stores the script**, so it runs again after a reboot
4. **Interpreter executes** the script

### Script Execution

Before a script is sent, the Sandbox compiles it in the browser, against the selected device's memory limits, so syntax errors and most mistakes are caught before the script reaches the device. See [Compile, emulate, deploy](/platform/sandbox#compile-emulate-deploy).

On the device:

1. **A fresh interpreter is built for the script**, with memory of its own, sized by the firmware, and the primitives the firmware registered. A new script replaces the one that was running.
2. **The script runs from top to bottom.** Definitions and assignments run once. A `task` schedules its body to run on a timer; a script without one simply finishes, and the interpreter is shut down.
3. **The task body runs on its schedule**, and between runs the device does its other work: the network, buttons, timers.
4. **Printed lines and errors go to the platform**, and appear under the device's **Logs** tab. An error stops the script; the device itself keeps running.

**What a script can reach:**

* **Only what the firmware provides.** A script works through the primitives the firmware registered. Pins and buttons are addressed by their index in the [register](/general-concepts/primitives#the-register-system), so a script can only reach the ones the firmware made available.
* **Only its own memory.** Running out stops the script with `Memory exhausted`, not the device.
* **No endless loops.** A loop, together with the loops inside it, may run for 20,000 passes ([Loops](/platform/sandbox/visual-editor/loops#how-long-a-loop-may-run)), and recursion too deep for the device is stopped with an error as well. Work that should go on for as long as the device runs belongs in the task.

### Task-based Execution Model

A script does its ongoing work in a `task`, which the device runs on a schedule. A script has at most one task; the Sandbox warns about a second. A script without a task runs once, from top to bottom, and stops.

* `(task times period ' body)`
  * `times`: how many times to run (use `0` for infinite)
  * `period`: interval between runs in milliseconds
  * `body`: quoted list of expressions to evaluate each tick

Example (run forever every 100 ms):

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-e873369a9826dc487e5daaaf08baf87f7b254493%2Fscripting_example_1.svg?alt=media" alt=""><figcaption></figcaption></figure>

```lisp
(task 0 100 '
 (progn
  (if
   (bclicked 0)
   (progn
    (dwrite 0 #t)))))
```

This model keeps scripts cooperative and responsive.

### Script Persistence

The platform keeps the last script sent to each device, and delivers it whenever the device connects. On top of that, the device can store the script itself, so that it starts again as soon as the device does, before the network is back.

Deploying from the Sandbox stores the script on the device. When you choose a device in the dialog, or in a **Deploy** widget's settings on a dashboard, the **Store script on the device** option decides it; it is on by default.

| After a restart | Stored on the device                             | Not stored                                      |
| --------------- | ------------------------------------------------ | ----------------------------------------------- |
| **Runs again**  | Straight away, even without a network            | Once the device has reconnected and received it |
| **Use for**     | Anything the device should keep doing on its own | Trying a script out                             |

A stored script that stops with an error doesn't stop the device: the error is reported, and the device carries on without a script until one is delivered again — the next deploy, or the platform's copy the next time the device connects.

## Examples

Below are pairs of visual blocks and the generated UniotLisp code.

### Button → Turn LED On

Run task every 100 ms; if button 0 clicked → digital write true to pin 0.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-e873369a9826dc487e5daaaf08baf87f7b254493%2Fscripting_example_1.svg?alt=media" alt=""><figcaption></figcaption></figure>

Generated code:

```lisp
(task 0 100 '
 (progn
  (if
   (bclicked 0)
   (progn
    (dwrite 0 #t)))))
```

### Blink LED (toggle state)

Run task every 500 ms; toggle LED at pin 0.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-5067ef385338749cab4574317c570e78d9c12d0d%2Fscripting_example_2.svg?alt=media" alt=""><figcaption></figcaption></figure>

Generated code:

```lisp
(define state ())

(setq state ())

(task 0 500 '
 (progn
  (setq state
   (not
    (bool state)))
  (dwrite 0 state)))
```

### Sensor Threshold Control

Run task every 5 s; if sensor (A0) > 512 → LED on else LED off.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-8f3951d36fffa87d00f73d7edea6aee49a812338%2Fscripting_example_3.svg?alt=media" alt=""><figcaption></figcaption></figure>

Generated code:

```lisp
(task 0 5000 '
 (progn
  (if
   (>
    (aread 0) 512)
   (progn
    (dwrite 0 #t))
   (progn
    (dwrite 0 ())))))
```

### Button Toggle Latch

Run task every 100 ms; if button clicked → toggle LED.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-607dc15da241428d23bdb28bb284ff0e6b9a3850%2Fscripting_example_4.svg?alt=media" alt=""><figcaption></figcaption></figure>

Generated code:

```lisp
(define state ())

(setq state ())

(task 0 100 '
 (progn
  (if
   (bclicked 0)
   (progn
    (setq state
     (not
      (bool state)))
    (dwrite 0 state)))))
```

## Best Practices

### Keep Scripts Simple

Break complex logic into functions:

```lisp
; Good - modular and readable
(defun read_sensor ()
 (aread 0))
(defun is_too_hot
 (t)
 (> t 512))
(defun activate_cooling ()
 (dwrite 0 #t))

(task 0 500 '
 (list
  (if
   (is_too_hot
    (read_sensor))
   (list
    (activate_cooling)))))

; Avoid - complex monolithic code
(task 0 500 '
 (list
  (if
   (>
    (aread 0) 512)
   (list
    (dwrite 0 #t)))))
```

### Use Meaningful Names

```lisp
; Good
(define temprature_treshold 20)
(defun turn_on_heater ()
 (dwrite 0 #t))

; Avoid
(define tt 20)
(defun toh ()
 (dwrite 0 #t))
```

### Add Comments

```lisp
; Check if room temperature is below threshold
; and activate heating if necessary
(defun check_heating ()
 (setq temp
  (read_temp_sensor)) ; Read DS18B20 sensor
 (if
  (< temp temrature_treshold)
  (list
   (activate_heating)))) ; Turn on relay
```

## Debugging Scripts

### Logging

Use the `print` statement (the corresponding [visual block](/platform/sandbox/visual-editor/text#print)) for debugging:

```lisp
(print 'script-started)
(print (eval ' (aread 0)))
```

Printed messages appear:

* Under the “Logs” tab on the device page, for a deployed script
* In [the emulator logs](/platform/sandbox/emulator#logs), while emulating

### Errors

If the interpreter encounters an error while executing a script, the device reports it to the platform.

You can see in the device list when an error occurs on one of them. The error message is displayed in the same place as the logs (device page → "Logs" tab).

### Common Issues

#### Script Not Running

**Check**:

1. Device is online and connected
2. Script was successfully deployed (check the "Script" tab on the device page)
3. No syntax errors (check whether device has an error)
4. Device has enough memory

#### Unexpected Behavior

**Debug Steps**:

1. Add `print` statements at key points
2. Verify GPIO registration matches script indices (the "Registers" tab on the device page)
3. Check variable values with `print` and `eval` statements
4. Test primitives individually

#### Memory Errors

**Solutions**:

1. Reduce heap size usage
2. Avoid large lists
3. Clear unused variables
4. Increase `UNIOT_LISP_HEAP` in firmware

## Conclusion

Scripting transforms devices from fixed-function hardware into dynamic, programmable platforms. By combining:

* **Easy Creation** (Visual editor for beginners, code editor for experts)
* **Remote Deployment** (MQTT-based delivery)
* **Sandboxed Execution** (Safe, isolated environment)
* **Hardware Access** (Primitives for GPIO and sensors)
* **Persistence** (Survive reboots)

You can create IoT solutions that are:

* **Flexible**: Change behavior without reflashing
* **User-Friendly**: Non-programmers can create automations
* **Powerful**: Advanced users have full control
* **Safe**: Sandboxed execution protects devices
* **Maintainable**: Version control and rollback

Whether you're building smart home automation, industrial monitoring, or custom IoT solutions, scripting enables rapid iteration and deployment of device logic.

## Further Reading

* [**Primitives**](/general-concepts/primitives): Understanding built-in and custom primitives
* [**UniotLisp Reference**](/advanced/uniot-lisp): Complete language documentation
* [**Sandbox**](/platform/sandbox): Using the visual editor and code editor


# Primitives

## Overview

Primitives are the bridge between C++ application code and scripts in Uniot Core. They are C++ functions callable from script that enable hardware access, complex calculations, and integration with application logic.

Uniot Core provides two types of primitives:

1. **Built-in Primitives**: Pre-implemented functions for common hardware operations (GPIO, buttons)
2. **Custom Primitives**: User-defined functions for application-specific logic

A third group is always present and needs no registration: `task`, `is_event`, `pop_event` and `push_event` are the core primitives every script is built around. They are described on the [Scripting](/general-concepts/scripting) page.

### Use Cases

Common scenarios where primitives are valuable:

* **Hardware Control**: Expose sensors, actuators, and peripherals to scripts
* **Complex Logic**: Implement algorithms in C++ but allow script-based configuration
* **State Management**: Let scripts read and modify device state
* **Custom Protocols**: Implement device-specific communication protocols

## How Primitives Work

### Execution Flow

1. **Script Received**: Device receives a script via MQTT, or restores the persisted one from flash at boot
2. **Interpreter Built**: A fresh UniotLisp interpreter is created and every registered primitive is installed into it
3. **Primitive Call**: Script calls a primitive function
4. **C++ Execution**: Primitive validates its arguments and executes C++ code
5. **Result Return**: Result is converted back to a Lisp value
6. **Script Continues**: Script processes the result

### Type System

Primitives declare the type of each argument. The declaration is checked when the primitive is called, and a mismatch stops the script with an error such as `[dwrite] Invalid type of 1 parameter, expected <Bool/Int>, got <Symbol>`.

| Lisp Type       | Accepts                             | Read with                          | Notes                                                                                   |
| --------------- | ----------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------- |
| `Lisp::Int`     | Integer                             | `getArgInt(i)` → `int`             | UniotLisp has no floats; scale fractional values to integers                            |
| `Lisp::Bool`    | `#t` / `()` (true / nil)            | `getArgBool(i)` → `bool`           | There is no `#f`; `()` is the only false value                                          |
| `Lisp::BoolInt` | A boolean or an integer             | `getArgBool(i)` or `getArgInt(i)`  | `getArgBool` treats non-zero as true; `getArgInt` turns `#t` into `1` and `()` into `0` |
| `Lisp::Symbol`  | A symbol, usually quoted (`'name`)  | `getArgSymbol(i)` → `const char *` | Used for event names and other identifiers                                              |
| `Lisp::Cell`    | A list, usually a quoted expression | `getArg(i)` → raw `Object`         | Received unevaluated; `task` uses this for its body and evaluates it later              |
| `Lisp::Any`     | Anything                            | Any of the above                   | Skips validation for that argument; the accessor you call still checks its own type     |

The return type is declared the same way and produced with `makeInt()`, `makeBool()` or `makeSymbol()`. Every primitive must return something; an action-only primitive returns a boolean.

### How the Platform Learns About Primitives

A primitive's name, return type and argument types are declared once, in its `describe()` call. When the device connects to MQTT, Uniot Core sends that description for every custom primitive in the device status packet, next to the list of registers and the FOURCC labels of the objects behind them. The platform uses this to draw the blocks in the Visual Editor, to generate the user library block, and to show the pin mapping on the device's **Registers** tab. Nothing about a primitive has to be configured on the platform side.

### User Library Block

{% hint style="info" %}
**Using the Visual Editor?** This block is generated for you from the device's primitive list. You can skip this section unless you write scripts by hand.
{% endhint %}

Scripts begin with a metadata block that lists the primitives the script uses. It is written entirely as comments, so the interpreter on the device ignores it. Its readers are the tools around the script: the Visual Editor writes it, and the Code Editor and the [Emulator](/platform/sandbox/emulator) rely on it to know which functions exist. A hand-written script that calls a primitive missing from the block fails to compile in the Emulator with "Undefined symbol".

**Structure**

The user library block is enclosed by special markers, exactly as the Visual Editor emits it:

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs primitive-name (arg1 arg2 ...)) ;-> ReturnType
;
;;; end-user-library
```

**Purpose**

* **Editor and Emulator**: They only know the primitives declared here, built-ins included
* **Documentation**: The block doubles as inline documentation of what the script expects from the device

**When You Need to Care About This**

* **Visual Editor Users**: Nothing to do; the block is generated and kept up to date for you
* **Manual Script Writers**: **Required**; add the block at the top of every script and declare every primitive the script calls, including built-ins such as `dwrite` or `aread`

**Syntax: `defjs` Declarations**

Each primitive is declared on its own line:

```lisp
; (defjs primitive-name (param1 param2 ...)) ;-> ReturnType
```

The parameter names are only labels. The Visual Editor uses `pin`, `state`, `value` and `button_id` for the built-ins and `_0`, `_1`, … for custom primitives.

**Example**

If you have a custom primitive `set_led_brightness` that takes two integers (LED index and brightness) and returns a boolean:

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs set_led_brightness (_0 _1)) ;-> Bool
; (defjs dwrite (pin state)) ;-> Bool
; (defjs aread (pin)) ;-> Int
;
;;; end-user-library

;; Rest of your script starts here
(task 0 100 '
  (list
    (if (> (aread 0) 512)
      (set_led_brightness 0 255))))
```

**Notes**

* The block is delimited by `;;; begin-user-library` and `;;; end-user-library` markers
* Each declaration is on a single line, prefixed with `;` (comment)
* The `;->` arrow indicates the return type
* Multiple primitives can be declared in the same block
* The block appears at the very beginning of the script
* The device itself never reads it; a script without the block runs on the device exactly the same

## Built-in Primitives

Uniot Core includes a set of built-in primitives for common hardware operations. These primitives work in conjunction with the **Register system**, which provides a layer of abstraction between scripts and physical hardware pins.

### The Register System

The Register system maps logical pin indices (used in scripts) to physical GPIO pins. This abstraction provides several benefits:

* **Safety**: Scripts can't accidentally access unregistered pins
* **Flexibility**: Physical pin assignments can change without modifying scripts
* **Portability**: Same scripts work on different hardware configurations
* **Organization**: Group related pins under named registers

#### How It Works

1. **Registration**: C++ code registers physical GPIO pins using `Uniot.registerLisp*()` methods
2. **Mapping**: Each registered pin gets a logical index (0, 1, 2, ...) in registration order
3. **Access**: Scripts use logical indices to access pins through built-in primitives
4. **Validation**: Primitives validate indices against registered pins before hardware access

Three rules follow from this design:

* **A built-in primitive exists only when its register has at least one entry.** A sketch that never calls `registerLispAnalogInput()` has no `aread`; a script calling it fails with an undefined symbol.
* **Each pin type has its own index namespace.** Digital output index `0` and analog output index `0` may be the same physical pin or different ones; they are looked up in different registers.
* **Registering again replaces the previous set.** List every pin of one type in a single `registerLisp*()` call.

### Registering GPIO Pins

Before using built-in primitives, you must register GPIO pins:

```c++
void setup() {
  // Register digital output pins (GPIO 12, 13, 14).
  // Script indices for the digital write (dwrite) primitive:
  // pin 12 = index 0, pin 13 = index 1, pin 14 = index 2
  Uniot.registerLispDigitalOutput(12, 13, 14);

  // Register digital input pins (GPIO 0, 4).
  // Script indices for the digital read (dread) primitive:
  // pin 0 = index 0, pin 4 = index 1
  Uniot.registerLispDigitalInput(0, 4);

  // Register analog output pins for PWM (GPIO 12, 13, 14).
  // Script indices for the analog write (awrite) primitive:
  // pin 12 = index 0, pin 13 = index 1, pin 14 = index 2
  Uniot.registerLispAnalogOutput(12, 13, 14);

  // Register analog input pins (GPIO A0).
  // Script indices for the analog read (aread) primitive:
  // A0 = index 0
  Uniot.registerLispAnalogInput(A0);

  Uniot.begin();
}
```

{% hint style="warning" %}
**Register inputs before `Uniot.begin()`.** Registering a pin for `dread` or `aread` sets it to plain `INPUT`, which clears any pull resistor. `begin()` gives every button on such a pin its pull back, but only for buttons it knows about at that point.
{% endhint %}

### Quick Reference

| Primitive  | Purpose                      | Registration Method           | Signature              | Returns                   |
| ---------- | ---------------------------- | ----------------------------- | ---------------------- | ------------------------- |
| `dwrite`   | Write digital pin (HIGH/LOW) | `registerLispDigitalOutput()` | `(dwrite index state)` | Bool: the state written   |
| `dread`    | Read digital pin (HIGH/LOW)  | `registerLispDigitalInput()`  | `(dread index)`        | Bool: `#t` HIGH, `()` LOW |
| `awrite`   | Write analog pin (PWM)       | `registerLispAnalogOutput()`  | `(awrite index value)` | Int: the value written    |
| `aread`    | Read analog pin (ADC)        | `registerLispAnalogInput()`   | `(aread index)`        | Int: 0-1023               |
| `bclicked` | Check button click           | `addLispButton()`             | `(bclicked index)`     | Bool: `#t` once per click |

An index that is not registered stops the script with `[dwrite] pin is out of range` (or `[bclicked] wrong button id`); the message appears on the device's **Logs** tab.

### Available Built-in Primitives

#### 1. `dwrite` - Digital Write

Writes a digital value (HIGH/LOW) to a registered output pin.

**Parameters**:

* `index` (Int): Logical pin index (0-based)
* `state` (Bool/Int): Pin state — accepts a boolean (`#t` / `()`) or an integer (non-zero = HIGH, `0` = LOW)

**Returns**: The state written, as a boolean

**C++ Registration**:

```c++
Uniot.registerLispDigitalOutput(12, 13, 14);
```

#### 2. `dread` - Digital Read

Reads a digital value (HIGH/LOW) from a registered input pin.

**Parameters**:

* `index` (Int): Logical pin index (0-based)

**Returns**: Boolean state (`#t` = HIGH, `()` = LOW)

**C++ Registration**:

```c++
Uniot.registerLispDigitalInput(0, 4);
```

#### 3. `awrite` - Analog Write (PWM)

Writes an analog value (PWM) to a registered output pin.

**Parameters**:

* `index` (Int): Logical pin index (0-based)
* `value` (Int): PWM value (0-1023)

**Returns**: The value written

The range is 0-1023 on every chip. When at least one analog output pin is registered, Uniot Core sets the PWM resolution to 10 bits during `begin()`, so a script writes the same range on an ESP8266 and on an ESP32.

**C++ Registration**:

```c++
Uniot.registerLispAnalogOutput(12, 13, 14);
```

#### 4. `aread` - Analog Read

Reads an analog value from a registered input pin.

**Parameters**:

* `index` (Int): Logical pin index (0-based)

**Returns**: Integer value in the range 0-1023

The range is the same on every chip. The ESP8266 ADC is 10-bit already; on the ESP32 family Uniot Core sets the read resolution to 10 bits during `begin()` when at least one analog input pin is registered. The voltage behind a reading still depends on the board.

**C++ Registration**:

```c++
Uniot.registerLispAnalogInput(A0);
```

#### 5. `bclicked` - Button Clicked

Checks whether a registered button was clicked since the last check.

**Parameters**:

* `index` (Int): Button index in the `bclicked` register (0-based)

**Returns**: `#t` once per click, then `()` until the next click

A click is a press followed by a release shorter than the long-press threshold. Reading it clears it, so a second `(bclicked 0)` in the same task pass returns `()`. A click that no script reads clears itself after about 10 seconds, which means a script that starts just after a press would react to it. Call `Uniot.resetLispButtons()` from a start hook to drop such presses:

```c++
Uniot.setLispStartHook([](uniot::LispStartReason) {
  Uniot.resetLispButtons();
});
```

**C++ Registration**:

`addLispButton()` does everything a button needs in one call: it creates the `Button`, polls it every 100 ms, links it to `bclicked`, and returns the button's index. A long press is 3 seconds. The pin gets the internal pull its active level needs (pull-up for `LOW`, pull-down for `HIGH`); pins without that pull need an external resistor.

```c++
// In setup(), after Uniot.begin(). The FOURCC id labels the button on the Registers tab.
int door = Uniot.addLispButton(4, LOW, FOURCC(door));  // returns the index scripts pass to bclicked
```

Indices follow registration order in the `bclicked` register. The WiFi reset button from `configWiFiResetButton()` is linked during `Uniot.begin()`, so buttons added before `begin()` come before it and buttons added after `begin()` follow it. Adding your buttons after `begin()`, still in `setup()`, keeps the reset button at index `0` and numbers yours from `1`. Pass `false` as the third argument of `configWiFiResetButton()` to keep the reset button out of the register altogether.

For a button with different timing, create and poll it yourself, then register it. The scheduler task must be named:

```c++
auto button = new uniot::Button(5, LOW, 50);  // pin, active level, long press in poll ticks
auto buttonTask = uniot::TaskScheduler::make(*button);
Uniot.getScheduler().push("user_btn", buttonTask);
buttonTask->attach(100);                       // 50 ticks x 100 ms = 5 s long press
Uniot.registerLispButton(button, FOURCC(_btn));
```

### Register System Internals

The Register system consists of two main components:

#### GpioRegister

Manages GPIO pin mappings:

* Stores physical pin numbers in named registers (`dwrite`, `dread`, `awrite`, `aread`)
* Maps logical indices to physical GPIO numbers
* Sets the pin mode when a pin is registered

#### ObjectRegister

Manages object references (like Button instances):

* Stores pointers to registered objects, appending to the named register (`bclicked`)
* Associates objects with FOURCC identifiers that the platform shows on the Registers tab
* Provides type-safe object retrieval and detects objects that were destroyed

#### PrimitiveExpeditor Access

Built-in primitives access the Register system through `PrimitiveExpeditor`. The proxy returned by `getAssignedRegister()` is bound to the register whose name equals the primitive's name, so `dwrite` reads the `dwrite` register:

```c++
Object dwrite(Root root, VarObject env, VarObject list) {
  auto expeditor = PrimitiveExpeditor::describe(name::dwrite, Lisp::Bool, 2, Lisp::Int, Lisp::BoolInt)
                       .init(root, env, list);
  expeditor.assertDescribedArgs();

  auto pin = expeditor.getArgInt(0);      // Get logical index
  auto state = expeditor.getArgBool(1);   // Get state

  uint8_t gpio = 0;
  // Get physical GPIO from register
  if (pin >= 0 && expeditor.getAssignedRegister().getGpio(pin, gpio)) {
    digitalWrite(gpio, state);            // Write to physical pin
  } else {
    expeditor.terminate("pin is out of range");
  }

  return expeditor.makeBool(state);
}
```

## Creating Custom Primitives

### Basic Structure

A primitive is a C++ function with a specific signature:

```c++
Object primitive_name(Root root, VarObject env, VarObject list)
```

**Parameters**:

* `root`: Garbage collector root (memory management)
* `env`: Current environment (scope/context)
* `list`: Argument list from Lisp

Three rules apply to every primitive:

* **`describe(...).init(...)` is the first statement.** When you register a primitive, Uniot Core calls it once with null arguments to read its description and jumps out of `describe()` before the body runs. Code placed before `describe()` would run with those null arguments.
* **The name in `describe()` is the Lisp name.** The C++ function name does not matter. The described name is what scripts call, what the platform lists, and the name of the register the primitive can read.
* **Register before `Uniot.begin()`.** `addLispPrimitive()` queues the primitive; it is installed when a script's interpreter is built, and the persisted script starts at the end of `begin()`.

### Using PrimitiveExpeditor

`PrimitiveExpeditor` is a helper class that simplifies primitive creation by handling argument validation, argument extraction, type conversion, error reporting and return value creation.

| Method                                       | Purpose                                                                                                                |
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `describe(name, returnType, count, ...)`     | Declares the primitive: Lisp name, return type, number of arguments, then one `Lisp::` type per argument (up to 16)    |
| `.init(root, env, list)`                     | Binds the description to the current call; the result is the expeditor                                                 |
| `assertDescribedArgs()`                      | Checks the argument count and types against the description and evaluates the arguments; call it first                 |
| `getArgInt(i)`                               | Reads argument `i` as `int`                                                                                            |
| `getArgBool(i)`                              | Reads argument `i` as `bool`                                                                                           |
| `getArgSymbol(i)`                            | Reads argument `i` as `const char *`                                                                                   |
| `getArg(i)`                                  | Returns argument `i` as a raw Lisp object, for `Lisp::Cell` arguments                                                  |
| `getArgsLength()`                            | Number of arguments actually passed                                                                                    |
| `makeInt(v)`, `makeBool(v)`, `makeSymbol(s)` | Builds the return value                                                                                                |
| `terminate(msg)`                             | Stops the script with `[name] msg`; see [Reporting Errors](#reporting-errors)                                          |
| `getAssignedRegister()`                      | Proxy to the register named after the primitive; see [Linking Objects to a Primitive](#linking-objects-to-a-primitive) |

### Step-by-Step Example

Let's create a primitive that sets the brightness of one of several LEDs. Like the built-ins, it takes a logical index rather than a GPIO number, so the script does not depend on the wiring.

**1. Declare the Primitive**

```c++
#include <Uniot.h>

using namespace uniot;

Object set_led_brightness(Root root, VarObject env, VarObject list);
```

**2. Implement the Primitive**

```c++
#define PIN_LED_R 15
#define PIN_LED_G 12
#define PIN_LED_B 13

// Logical LED index -> physical pin
static const uint8_t kLedPins[] = {PIN_LED_R, PIN_LED_G, PIN_LED_B};
static const int kLedCount = sizeof(kLedPins) / sizeof(kLedPins[0]);

Object set_led_brightness(Root root, VarObject env, VarObject list) {
  // Describe the primitive: name, return type, arg count, arg types
  auto expeditor = PrimitiveExpeditor::describe(
    "set_led_brightness",  // Lisp name (also reported to the platform)
    Lisp::Bool,            // Return type
    2,                     // Number of arguments
    Lisp::Int,             // First argument type (LED index)
    Lisp::Int              // Second argument type (brightness)
  ).init(root, env, list);

  // Validate that arguments match the description
  expeditor.assertDescribedArgs();

  // Extract arguments
  int index = expeditor.getArgInt(0);
  int brightness = expeditor.getArgInt(1);

  // Reject an index we have no pin for; this stops the script with a message
  if (index < 0 || index >= kLedCount) {
    expeditor.terminate("led index is out of range");
  }

  // Clamp brightness to the PWM range and perform the hardware operation
  brightness = constrain(brightness, 0, 255);
  analogWrite(kLedPins[index], brightness);

  // Return success status
  return expeditor.makeBool(true);
}
```

{% hint style="info" %}
**Why 0-255 here and 0-1023 for `awrite`?** Arduino's default `analogWrite()` range is 0-255. Uniot Core switches the device to a 10-bit range only when analog output pins are registered with `registerLispAnalogOutput()`, and in that case `awrite` already does what this primitive does. A custom primitive that drives PWM on its own pins sees whatever range the sketch has configured.
{% endhint %}

**3. Register the Primitive**

```c++
void setup() {
  // ... other setup code ...

  for (auto pin : kLedPins) {
    pinMode(pin, OUTPUT);
  }

  // Register the primitive, before begin()
  Uniot.addLispPrimitive(set_led_brightness);

  Uniot.begin();
}
```

**4. Use in Scripts**

Once the device reconnects, the Visual Editor shows a `set_led_brightness` block and generates its `defjs` line:

{% tabs %}
{% tab title="Visual Editor" %}

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-32a6c04cd110a217656e951ae74530ffeb0e0d57%2Fcustom_primitive_example.svg?alt=media" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="UniotLisp" %}
{% code title="Custom primitive" lineNumbers="true" %}

```lisp
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
;
; (defjs set_led_brightness (_0 _1)) ;-> Bool
;
;;; end-user-library

(task 0 100 '
 (progn
  (set_led_brightness 0 128)))
```

{% endcode %}
{% endtab %}
{% endtabs %}

### Reporting Errors

`expeditor.terminate("message")` is the way a primitive refuses to continue. It raises an interpreter error, so nothing after it runs: the script stops, the stop hook (if any) receives `LispStopReason::Failed`, and `[primitive_name] message` is published to the device's **Logs** tab. The built-ins use it for an unregistered index; use it for anything the script cannot recover from, such as an argument out of range or hardware that is not ready.

For conditions the script should handle itself, return a value instead. A sensor read that can fail is better off returning a sentinel, for example `-1000` for "no reading", so the script can skip that pass and try again on the next one.

### Linking Objects to a Primitive

A custom primitive can use the same Register system as `bclicked`. Link a C++ object under the primitive's name and the primitive fetches it by index, so scripts address instances by `0`, `1`, `2` rather than by anything hardware-specific:

```c++
class Relay : public uniot::ObjectRegisterRecord {
  // ... your object; it must derive from ObjectRegisterRecord ...
};

Relay relayA(12), relayB(13);

Object relay_set(Root root, VarObject env, VarObject list) {
  auto expeditor = PrimitiveExpeditor::describe("relay_set", Lisp::Bool, 2, Lisp::Int, Lisp::Bool)
                       .init(root, env, list);
  expeditor.assertDescribedArgs();

  auto relay = expeditor.getAssignedRegister().getObject<Relay>(expeditor.getArgInt(0));
  if (!relay) {
    expeditor.terminate("wrong relay index");
  }

  relay->set(expeditor.getArgBool(1));
  return expeditor.makeBool(true);
}

void setup() {
  // The register name must equal the name passed to describe()
  Uniot.registerLispObject("relay_set", &relayA, FOURCC(rlyA));  // (relay_set 0 ...)
  Uniot.registerLispObject("relay_set", &relayB, FOURCC(rlyB));  // (relay_set 1 ...)
  Uniot.addLispPrimitive(relay_set);
  Uniot.begin();
}
```

Each `registerLispObject()` call appends to the register, so indices follow registration order. The FOURCC id is what the platform shows for that slot on the Registers tab. If an object is destroyed, `getObject()` returns `nullptr` for its slot instead of a dangling pointer.

## Best Practices

### Descriptive Names

Use clear, descriptive names that indicate what the primitive does:

```c++
// Good
Object turn_relay_on(...)
Object read_temperature_sensor(...)
Object set_motor_speed(...)

// Avoid
Object relay(...)  // Ambiguous
Object temp(...)   // Too abbreviated
Object go(...)     // Unclear
```

### Validate Inputs

Check every argument before touching hardware, and let `terminate()` report what went wrong. A silent `makeBool(false)` hides the mistake from whoever is editing the script:

```c++
Object set_pwm(Root root, VarObject env, VarObject list) {
  auto expeditor = PrimitiveExpeditor::describe("set_pwm", Lisp::Bool, 2, Lisp::Int, Lisp::Int)
                     .init(root, env, list);
  expeditor.assertDescribedArgs();

  int index = expeditor.getArgInt(0);
  int value = expeditor.getArgInt(1);

  if (index < 0 || index >= kPwmCount) {
    expeditor.terminate("index is out of range");  // stops the script; shown on the Logs tab
  }

  value = constrain(value, 0, 255);  // out-of-range values are clamped, not fatal
  analogWrite(kPwmPins[index], value);
  return expeditor.makeBool(true);
}
```

### Keep Primitives Short and Non-blocking

A primitive runs inside the scheduler pass that evaluates the script, so everything else on the device waits for it. Do not `delay()` or poll in a loop. Read a value, write a value, or kick off work that a scheduler task finishes later:

```c++
// Good - focused primitives
Object read_sensor(...);
Object calculate_average(...);
Object send_notification(...);

// Avoid - doing too much
Object read_sensor_calculate_and_notify(...);  // Too complex, and probably blocking
```

### Integers Only

UniotLisp has no floating-point type. Scale fractional values before returning them, and say so in the primitive's name or documentation:

```c++
// Return tenths of a degree: 23.4 °C -> 234
return expeditor.makeInt((int)(temperature * 10));
```

### Use Logging

Log operations for debugging and monitoring:

```c++
Object important_operation(Root root, VarObject env, VarObject list) {
  auto expeditor = PrimitiveExpeditor::describe("important_operation", Lisp::Bool, 1, Lisp::Int)
                     .init(root, env, list);
  expeditor.assertDescribedArgs();

  int value = expeditor.getArgInt(0);

  UNIOT_LOG_INFO("Performing operation with value: %d", value);

  bool success = performOperation(value);

  if (success) {
    UNIOT_LOG_DEBUG("Operation completed successfully");
  } else {
    UNIOT_LOG_ERROR("Operation failed");
  }

  return expeditor.makeBool(success);
}
```

### Return Meaningful Values

Choose return types that provide useful information:

```c++
// Return bool for success/failure
Object write_config(...) -> Bool

// Return int for numeric data
Object read_sensor(...) -> Int

// Return symbol for status
Object get_device_state(...) -> Symbol
```

## Conclusion

Primitives are the foundation of dynamic behavior in Uniot Core, enabling both simple GPIO operations and complex device control through scripts.

**Built-in Primitives** provide:

* Immediate access to common hardware operations
* Safe, validated GPIO access through the Register system
* Consistent interface and value ranges across different devices
* No additional C++ code required

**Custom Primitives** enable:

* Extensibility: Add new capabilities without modifying the core framework
* Flexibility: Update device behavior through scripts
* Maintainability: Separate hardware logic from business logic
* Optimization: Performance-critical operations in C++

By combining built-in and custom primitives with scripting, you can create powerful, flexible IoT devices that are:

* **Remotely Reconfigurable**: Update logic via MQTT without reflashing
* **Hardware Abstracted**: Scripts work across different physical setups
* **Safe**: The Register system prevents access to unregistered pins
* **Maintainable**: Clear separation between firmware and scripts

## Further Reading

* [**Scripting**](/general-concepts/scripting): The task model, events, script delivery and debugging
* [**Emulator**](/platform/sandbox/emulator): Run scripts against mocked primitives before deploying
* [**Visual Editor: Primitives**](/platform/sandbox/visual-editor/primitives): The blocks that correspond to the primitives on this page
* [**Uniot Core Reference**](https://github.com/uniot-io/uniot-core/blob/master/docs/reference.md#4-uniotlisp-scripting): The firmware's own reference for scripting, buttons and lifecycle hooks
* [**PrimitiveExpeditor**](https://core.docs.uniot.io/latest/group__uniot-lisp-primitive-expeditor.html): Complete reference for argument handling and validation
* [**Register System**](https://core.docs.uniot.io/latest/group__registers.html): Detailed documentation on GPIO and object registration
* [**UniotLisp Interpreter**](/advanced/uniot-lisp/embedding-instructions): Understanding the embedded UniotLisp environment and syntax


# Dashboard

A dashboard is a grid of widgets, each bound to an Event. Every dashboard, device, and the Emulator in your account share one event bus, so what you do on a widget reaches your devices, and what a device pushes shows up on the widget. You switch between viewing a dashboard and editing it, and each dashboard keeps a desktop and a mobile layout.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-d8aa8792f19b31b0e55a6bb3dc6e205562d332d4%2Fdashboard_main.png?alt=media" alt=""><figcaption><p>A dashboard in view mode</p></figcaption></figure>

## Quick Start

1. **Open** a dashboard from **Dashboards** in the sidebar. A new account already has one.
2. **Edit** with the button in the header.
3. **Add Widget** and pick a type.
4. **Name it and bind it** to an event in the widget settings, then **Save** the settings.
5. **Arrange** the widgets by dragging and resizing, then press **Save** in the header.
6. **Use it** in view mode, with a script on the device that uses the same event name.

{% hint style="info" %}
**New account?** "My First Dashboard" comes with a Switch and an LED, both bound to the event `led`, and a script that ties them to a pin. [Getting Started](/guides/getting-started#step-4-control-it-from-the-dashboard) walks through it.
{% endhint %}

## View and Edit Mode

One button in the header switches between using a dashboard and building it: **Edit** enters edit mode, **Save** leaves it and stores the layout. There is no cancel. A dashboard with no widgets opens in edit mode so you can start adding right away.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-10c6f3259574349666966ddea3b716ab432e1a8a%2Fdashboard_view_modes.png?alt=media" alt=""><figcaption><p>Switch layout and Edit in the dashboard header</p></figcaption></figure>

A dashboard has a desktop and a mobile layout. They share the same widgets and settings; only where each widget sits and how big it is are stored per layout. The one you see is chosen by your screen size, and **Switch layout** flips it at any time, in either mode.

In edit mode, **Add Widget** adds a widget, and hovering a widget shows a gear that opens its settings and a trash icon that removes it immediately. Drag a widget to move it and use the handle in its bottom-right corner to resize it; widgets never overlap. Positions and sizes are stored when you press **Save**, which also refreshes the dashboard's thumbnail from the layout on screen.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-48adc5de3cc1814d96d70419be2ad22423090f85%2Fdashboard_widget_select.png?alt=media" alt=""><figcaption><p>The Add Widget dropdown in edit mode</p></figcaption></figure>

## Widgets

Every widget except Deploy Script is bound to an Event: the widget publishes to it when you use it and reacts when a device/emulator/another widget publishes it. The widget's header shows its name and the footer shows the event name.

Missing a widget type? Tell us on the [community forum](https://community.uniot.io).

## Widget Configuration

The gear on a widget, or picking a type in **Add Widget**, opens its settings with a live preview on the right. **Save** applies, **Close** discards.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-4ce20bfd083bd4896e39cf418f562dd665540d79%2Fdashboard_widget_settings.png?alt=media" alt=""><figcaption><p>Widget settings: Name, Event and Retain, with a live preview</p></figcaption></figure>

* **Name** - shown in the widget header.
* **Event** - letters, digits, and underscore only. It must match the event name your script uses.
* **Retain** - for Switch, Push Button, and Slider. Keeps the last value on the broker, see [Retain](#retain).

Deploy Script has its own fields instead of Event and Retain.

## Widget Types

| Widget        | What it does                                                     | Settings                                                   |
| ------------- | ---------------------------------------------------------------- | ---------------------------------------------------------- |
| Switch        | Sends 1 when on and 0 when off, and flips when the event changes | None                                                       |
| Push Button   | Sends 1 on press and 0 on release                                | None                                                       |
| Slider        | Sends the number you drag to, and moves when the event changes   | Min and Max                                                |
| LED           | Lights when the event value is not 0                             | Colour: red, green, or blue                                |
| Value         | Shows the latest event value                                     | Decimal digits, round or truncate, a divider or multiplier |
| Deploy Script | Sends a script to a device after a confirmation                  | Script, device, store the script on the device             |

Deploy Script always sends the latest saved version of the script. The confirmation warns when the device is offline (the script is applied when it reconnects) and when the device lacks a primitive the script uses. **Store script on the device** keeps the script across power cycles.

## Events

Every dashboard, device, and the Emulator in your account share one event bus. A widget publishes under its event name and every device of the account receives it; a device publishes the same way with `push event`, and every widget bound to that name updates. The name must be spelled exactly as in the script's [push event](/platform/sandbox/visual-editor/special#push-event), [is event](/platform/sandbox/visual-editor/special#is-event), and [pop event](/platform/sandbox/visual-editor/special#pop-event) blocks. Values are numbers; booleans become 1 and 0.

The [Emulator](/platform/sandbox/emulator#events) is on the same bus: widgets react to an emulated script, and the emulated script sees what you press on the dashboard.

### Retain

With **Retain** on, the broker keeps the last value a widget published and replays it to a device when it connects and to the dashboard when it loads, so a switch position or a slider level survives a reboot or a page refresh. With it off, values reach whoever is listening at that moment and are then forgotten. Turning Retain off on a widget also clears the value the broker kept.

{% hint style="warning" %}
**Old retained values come back.** A retained event from an earlier project is replayed to any new widget or device that uses the same name. Use fresh event names, or clear the old value: bind a widget to the name with Retain on, save, then turn Retain off and save again.
{% endhint %}

See [Getting Started](/guides/getting-started#step-4-control-it-from-the-dashboard) for the seeded Switch and LED in action.


# Sandbox

The Sandbox is where you write the scripts your devices run: build them from blocks or write them as code, try them in the [Emulator](/platform/sandbox/emulator), and deploy them to a device — all in the browser, without reflashing anything.

{% hint style="info" %}
The Sandbox needs a desktop browser. On a phone, a script shows its block diagram but can't be edited.
{% endhint %}

## Layout

* **The editor**, on the left, takes most of the screen. In **Blockly** mode it is the [Visual Editor](/platform/sandbox/visual-editor); in **Advanced** mode it is a code editor for [UniotLisp](/advanced/uniot-lisp/language-description).
* **The code preview**, on the right, shows the UniotLisp code your blocks generate, read-only. Show or hide it with **Ctrl+P**; the Sandbox remembers your choice.
* **The** [**Logger**](/platform/sandbox/logger), below the preview, shows the result of the last compile or emulator run.

A new script starts with one block already in place: a [task](/general-concepts/scripting) that repeats every 500 ms. Put what the device should do inside it.

## Visual Editor vs. Code Editor

The switch at the top of the Sandbox moves between the two ways of writing a script.

* **Blockly** — build the script from blocks in the [Visual Editor](/platform/sandbox/visual-editor). The code is generated from them.
* **Advanced** — write the UniotLisp code yourself, or paste it in. Switching from Blockly to Advanced takes the code the blocks generated, so you can start from blocks and continue by hand.

The two don't stay in sync. Code you write in Advanced mode isn't turned back into blocks, and **switching back to Blockly replaces it with the code the blocks generate**, so the Sandbox asks before it does. Once you've written a script by hand, keep it in Advanced mode. The Sandbox remembers the mode a script was saved in and opens it that way.

The code editor in Advanced mode has three tools of its own, which appear at its top right when you move the pointer over it:

| Tool                   | Shortcut        | What it does                                                                                                    |
| ---------------------- | --------------- | --------------------------------------------------------------------------------------------------------------- |
| **Parinfer**           | Shift+Alt+Space | Off by default. When on, it keeps the parentheses balanced as you type, working them out from your indentation. |
| **Beautify**           | Shift+Space     | Re-indents the code                                                                                             |
| **Keyboard shortcuts** | Ctrl+Alt+H      | Lists every shortcut the editor has                                                                             |

{% hint style="info" %}
A script created before the Sandbox began tracking how a script was written asks, the first time you open it, whether it was built with blocks or written as code, so it can open in the right mode.
{% endhint %}

## Compile, emulate, deploy

The buttons at the top right take a script from the editor to a device.

**Compile** (Ctrl+B) turns the script into the code a device runs. The result appears in the [Logger](/platform/sandbox/logger). If compiling fails, the error is shown at the top of the Sandbox, and in Advanced mode the code editor marks the line and underlines the expression that failed.

After you change a script, **Recompile required** appears, and emulating or deploying waits until you compile again — so what you test and deploy is always what you see.

**Emulate** (Ctrl+Enter) runs the compiled script in the [Emulator](/platform/sandbox/emulator), a simulated device in the browser. **Terminate** (Ctrl+Enter again) stops it.

**Deploy** sends the compiled script to a device, which runs it straight away and keeps it across reboots. A device that is offline receives the script when it next connects. It goes to the device selected in the sidebar's device list — the button's tooltip names it — and if none is selected, the Sandbox asks which one. If the script uses a primitive the device doesn't have, or the device hasn't reported its primitives yet, the Sandbox says so and asks before deploying.

**Save** (Ctrl+S) stores the script. **Unsaved changes** appears while there is something to save, and leaving the Sandbox with unsaved changes asks whether to save them, discard them, or stay.

## The selected device

Selecting a device in the sidebar does more than choose where Deploy sends a script. The Sandbox compiles for that device:

* The [Primitives](/platform/sandbox/visual-editor/primitives) category in the Visual Editor offers the functions that device's firmware provides.
* Compiling uses the device's memory limits, so a script that fits here fits there.

Without a selected device, the Primitives category offers the five standard ones — digital and analog read and write, and button clicks — and compiling uses default memory limits.

## Keyboard shortcuts

| Shortcut   | Action                             |
| ---------- | ---------------------------------- |
| Ctrl+S     | Save                               |
| Ctrl+B     | Compile                            |
| Ctrl+Enter | Emulate, or terminate the emulator |
| Ctrl+P     | Show or hide the code preview      |

On a Mac, use Cmd instead of Ctrl.

## Typical workflow

1. **Select your device** in the sidebar, so the Sandbox knows its primitives.
2. **Create a script** and build its logic with blocks, or switch to Advanced and write it.
3. **Compile** it, and fix anything the [Logger](/platform/sandbox/logger) reports.
4. **Emulate** it to check its behavior before it touches hardware.
5. **Deploy** it to the device.
6. **Save** it, to change or deploy it again later.


# Visual Editor

The Visual Editor is a drag-and-drop interface for building device logic without writing code. Built on [Blockly](https://developers.google.com/blockly), it provides an intuitive way to create scripts by connecting visual blocks that represent programming concepts.

Behind the scenes, the Visual Editor automatically translates your block arrangements into [UniotLisp](/advanced/uniot-lisp) code. This approach makes IoT development accessible to beginners while remaining efficient for experienced developers who want to prototype quickly. Focus on application logic instead of syntax details.

## How It Works

1. **Drag blocks** from the toolbox into your workspace
2. **Connect blocks** together to build logic - blocks snap together when they're compatible
3. **Configure blocks** by filling in values, selecting options, or creating variables
4. **Test in the emulator** to verify behavior
5. **Compile to UniotLisp** when ready to deploy or view the generated code

The Visual Editor handles:

* **Hardware interactions** - Read sensors, control actuators, and manage device I/O with Primitive blocks
* **MQTT communications** - Handle events, access payloads, and publish messages
* **Program logic** - Conditions, loops, variables, functions, and data manipulation

## Block Categories

The Visual Editor organizes blocks into categories by function. Each category contains blocks designed for specific purposes:

* [**Special**](/platform/sandbox/visual-editor/special): Provides blocks for controlling the script execution flow and interacting with external MQTT events.
* [**Logic**](/platform/sandbox/visual-editor/logic): Includes blocks for boolean operations, comparisons, and conditional statements to control the flow of your scripts.
* [**Math**](/platform/sandbox/visual-editor/math): Provides blocks for arithmetic, random number generation, and advanced mathematical functions.
* [**Loops**](/platform/sandbox/visual-editor/loops): Enables repeating sequences of instructions based on conditions or specific counts.
* [**Text**](/platform/sandbox/visual-editor/text): Contains blocks for creating, manipulating, and outputting text strings in your scripts.
* [**Variables**](/platform/sandbox/visual-editor/variables): Allows you to store, retrieve, and modify values using named variables in your scripts.
* [**Functions**](/platform/sandbox/visual-editor/functions): Lets you define reusable blocks of code with optional parameters and return values.
* [**Primitives**](/platform/sandbox/visual-editor/primitives): Includes blocks for direct hardware interactions, such as reading sensor data or controlling actuators.


# Special

Special blocks provide essential control over program execution and event handling. These blocks let you create the main program loop, handle MQTT events, and build event-driven device behaviors.

## task

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-723aa9e1b22fe73d660058f96a3ac3ecd1fcbbef%2Fspecial_main.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

The main execution loop for your script. This block repeatedly executes the code inside it at a specified interval. Every script requires exactly one task block - it serves as your program's entry point and controls when your device logic runs.

**Parameters:**

* **Iterations** (Number): How many times to run. Set to `0` for infinite execution (most common)
* **Interval** (Number): Time between executions in milliseconds

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-6ae0230f704b54cf49bd5c6d4cea927ebd638243%2Fspecial_task_example.svg?alt=media" alt=""><figcaption><p>Read temperature every 5 seconds and send an event with the value</p></figcaption></figure></div>

## task pass

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-ad44646b26222a06f7e46e3ec16b4641e8cf5742%2Fspecial_iterator.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Returns the current iteration counter for the task. Useful for creating behaviors that change over time or execute only on specific iterations.

**Returns:**

* **Number**: `-1` for infinite tasks (iterations = 0), otherwise counts down from `n-1` to `0`

{% hint style="warning" %}
This block only works inside a task block. Anywhere else, the editor disables it and marks it with a warning. A disabled block is left out of the code, and the block around it falls back to its own default: below, **set** stores `0` instead of the pass number. There is no error, just a value that never changes.

<img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-323d095538547a24a546c42324c8073061156b42%2Fspecial_iterator_outside.svg?alt=media" alt="" data-size="original">
{% endhint %}

{% hint style="warning" %}
**Known issue:** a variable set to **task pass** keeps a link to the task's counter rather than its value, so on the next run it reads that run's number instead of the one it was given. This only matters if you save the pass number to use on a later run. Until this is fixed, store a copy instead — **task pass + 0**: the arithmetic makes a new number. The fix is coming in the next release of the UniotLisp interpreter.
{% endhint %}

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-6329a8bda827b7d15865a011109a73d6d279bf4a%2Fspecial_task_pass_example.svg?alt=media" alt=""><figcaption><p>Create LED patterns based on iteration</p></figcaption></figure></div>

{% hint style="info" %}
Custom iterator implementation using variables to count up instead of down.

<img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-fb7dbf273d24c9863b3a20be008c63a211fdba77%2Fspecial_iterator_custom.svg?alt=media" alt="" data-size="original">
{% endhint %}

## is event

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-20b5c9e806f7b93dc5aa4bffc95ec9c970fee704%2Fspecial_is_event.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Checks whether a specific MQTT event is waiting in the queue. Use this to conditionally process events when they arrive.

**Parameters:**

* **Event Name** (String): The name of the event to check for

**Returns:**

* **Boolean**: `#t` (true) if the event exists in the queue, `()` (false) if not

{% hint style="warning" %}
This block returns true as long as the event remains in the queue. To remove the event, use the **pop event** block.
{% endhint %}

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-6c704534073fcac29ccfcd8f265dc10743c1c5a6%2Fspecial_is_event_example.svg?alt=media" alt=""><figcaption><p>Check and retrieve an event</p></figcaption></figure></div>

## pop event

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-bd7cdaf7d2882a28ba2fb01b44e51b00947ee7f3%2Fspecial_pop_event.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Retrieves and removes the oldest event of the specified type from the queue. Use this to access event data sent via MQTT.

**Parameters:**

* **Event Name** (String): The name of the event to retrieve

**Returns:**

* **Number**: The event payload value, or `()` (empty) if no event exists

{% hint style="warning" %}
Event payloads are always of type **Number**. If you need to send other data types, encode them as numbers.
{% endhint %}

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-6c704534073fcac29ccfcd8f265dc10743c1c5a6%2Fspecial_is_event_example.svg?alt=media" alt=""><figcaption><p>Check and retrieve an event</p></figcaption></figure></div>

## push event

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-8d0a1f433d3b2ec8d62ad555af99c5397a815b35%2Fspecial_push_event.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Sends an event with a value to the MQTT broker. Use this to publish sensor readings, status updates, or trigger actions on other devices or dashboards.

**Parameters:**

* **Event Name** (String): The name of the event to publish
* **Value** (Number/Boolean): The value to send

{% hint style="warning" %}
Event payloads are always sent as **Number** type. Boolean values are automatically converted: `true` → `1`, `false` → `0`.
{% endhint %}

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-8a50fdb7e201d6ee1642bb1cce17d79b9975cde3%2Fspecial_push_event_example.svg?alt=media" alt=""><figcaption><p>Send sensor readings</p></figcaption></figure></div>


# Logic

Logic blocks enable decision-making in your scripts. Use these blocks to compare values, create conditional behaviors, combine multiple conditions, and control program flow based on device states.

## value

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-ea21c2f83a5bbde2d9fe167aaa0dad698a1ca52b%2Flogic_value.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

A boolean constant that represents true or false. Use this block to provide boolean values to conditions, comparisons, or variables.

**Parameters:**

* **Value** (Boolean): Select `true` or `false`

**Returns:**

* **Boolean**: `#t` for true, `()` for false (Lisp values)

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-0c0e1b8fb11b498148b670aa6b918fa470f8ee46%2Flogic_value_example.svg?alt=media" alt=""><figcaption><p>Set initial values</p></figcaption></figure></div>

## if

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-94f3e5bfa641fd9d0b069dc7af1be25f36cd4da6%2Flogic_if.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Executes code conditionally based on whether a condition is true. This is the fundamental building block for creating decision-based behavior in your scripts.

**Parameters:**

* **Condition** (Boolean): The condition to evaluate

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-2e3f75f06b77fba2976d8ea59988f567ca1af57e%2Flogic_if_example.svg?alt=media" alt=""><figcaption><p>Send event if button was clicked</p></figcaption></figure></div>

### Adding else if and else Clauses

The basic `if` block can be extended with additional conditions and fallback logic. Click the gear icon to open the configuration panel:

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-6f5ef564b9501807ec2f06b1cf40c5dbf9aa14dd%2Flogic_if_settings.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Drag `else if` and `else` blocks from the left panel to build your conditional logic. You can add multiple `else if` clauses but only one `else` clause. Reorder or remove clauses as needed, then click the gear icon to close:

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-742242e3be7602de9f141da5afddedd440a36164%2Flogic_if_settings2.gif?alt=media" alt=""><figcaption></figcaption></figure></div>

## comparison

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-90187fd998e8bbe8ad893155726564e2d267c12d%2Flogic_comparison.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Compares two values using mathematical or equality operators. Use this to check sensor thresholds, compare states, or validate ranges.

**Parameters:**

* **Left Value** (Number): The first value to compare
* **Operator**: Choose from `=` (equal), `≠` (not equal), `<` (less than), `>` (greater than), `≤` (less or equal), `≥` (greater or equal)
* **Right Value** (Number): The second value to compare

**Returns:**

* **Boolean**: `#t` (true) if the comparison is satisfied, `()` (false) otherwise

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-5a401ae4fb2a7884d3ad4d170c0711ae7d6aa7b1%2Flogic_comparison_example.svg?alt=media" alt=""><figcaption><p>Check humidity and send event when it is too high</p></figcaption></figure></div>

## and

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-3005907f8c18e2134ca43f37000768592db7d262%2Flogic_operation_and.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Returns true only if both conditions are true. Use this to combine multiple requirements that must all be satisfied.

**Parameters:**

* **Input A** (Boolean): The first condition
* **Input B** (Boolean): The second condition

**Returns:**

* **Boolean**: `#t` (true) if both inputs are true, `()` (false) otherwise

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-c1eaea0bc261ceaa757589b75b527a0da36fbf88%2Flogic_operation_and_example.svg?alt=media" alt=""><figcaption><p>Multiple condition check</p></figcaption></figure></div>

## or

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-724e3c9edb44195dfea0cb4028a2892beb8bf867%2Flogic_operation_or.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Returns true if at least one condition is true. Use this when any of several conditions should trigger an action.

**Parameters:**

* **Input A** (Boolean): The first condition
* **Input B** (Boolean): The second condition

**Returns:**

* **Boolean**: `#t` (true) if at least one input is true, `()` (false) otherwise

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-87506bffecbb43aa28aebf96b080dd85ce2809f5%2Flogic_operation_or_example.svg?alt=media" alt=""><figcaption><p>Humidity alert</p></figcaption></figure></div>

## not

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-389be8db6dd876886890738e8ad8f41d1b7f12fa%2Flogic_not.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Inverts a boolean value, turning true into false and false into true. Use this to reverse conditions or check for the opposite of a state.

**Parameters:**

* **Input** (Boolean): The boolean value to invert

**Returns:**

* **Boolean**: `#t` (true) becomes `()` (false), and vice versa

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-1dc6da4c57b8272b9a1f8e1a19f80e074b1aab77%2Flogic_not_example.svg?alt=media" alt=""><figcaption><p>Toggle state by button click</p></figcaption></figure></div>


# Math

Math blocks provide numerical operations and calculations for your scripts. Use these blocks to perform arithmetic, apply mathematical functions, and manipulate numeric values from sensors or variables.

## value

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-f95e11d0b78139bc38e694626ffce4dd370284e9%2Fmath_value.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

A numeric constant. Use this block to provide fixed numbers for calculations, comparisons, or as parameters to other blocks.

**Parameters:**

* **Value** (Number): Enter any numeric value (integers or decimals)

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-ad6c824e49529a83464248d061b6bc8e0e5181f8%2Fmath_value_example.svg?alt=media" alt=""><figcaption><p>Define temperature thresholds</p></figcaption></figure>

## math operation

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-34c44ea706ab3cad4e228ab9a63386417d71976c%2Fmath_operation.png?alt=media" alt="" width="138"><figcaption></figcaption></figure></div>

Applies a single-value mathematical operation. Choose from absolute value (removes sign) or negation (flips positive to negative and vice versa).

**Parameters:**

* **Operation**: Select `abs` (absolute value) or `-` (negate)
* **Value** (Number): The number to operate on

**Returns:**

* **Number**: The result of the operation

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-bec27b4d6bf9160151a6d51d6c67b52b2a222ad5%2Fmath_operation_example.svg?alt=media" alt=""><figcaption><p>Temperature difference calculation</p></figcaption></figure>

## arithmetic

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-12e57f7bab39883c7a073fa740e2c5a92289fe97%2Fmath_arithmetic.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Performs basic arithmetic operations between two numbers.

**Parameters:**

* **Left Value** (Number): The first number
* **Operator**: Choose `+` (add), `-` (subtract), `×` (multiply), `÷` (divide), `^` (power)
* **Right Value** (Number): The second number

**Returns:**

* **Number**: The result of the calculation

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-97404438299cab0bb770d54d6b1077c3232c687a%2Fmath_arithmetic_example.svg?alt=media" alt=""><figcaption><p>Convert raw sensor value to voltage</p></figcaption></figure>

## number condition

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-433ce33776d51b4f0f56b9a27f63b2b85471bd77%2Fmath_number_condition.png?alt=media" alt="" width="193"><figcaption></figcaption></figure></div>

Checks whether a number meets a specific condition. Use this for pattern detection or periodic actions.

**Parameters:**

* **Value** (Number): The number to check
* **Condition**: Select `even`, `odd`, `positive`, `negative`, `whole`, `divisible by`

**Returns:**

* **Boolean**: `#t` (true) if the condition is met, `()` (false) otherwise

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-f540f5601b49995de2d96e56a587d7554d2d98c8%2Fmath_number_condition_example.svg?alt=media" alt=""><figcaption><p>Read sensor value every 5th script execution</p></figcaption></figure>

## remainder

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-5cc3a3aa2fd6c42157bfdcef47a25dd97dbae463%2Fmath_remainder.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Calculates the remainder after dividing one number by another (modulo operation). Useful for creating repeating patterns or detecting multiples.

**Parameters:**

* **Dividend** (Number): The number to be divided
* **Divisor** (Number): The number to divide by

**Returns:**

* **Number**: The remainder of the division

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-acfce49a2fc414f66852cfd65ee12d559f1406e2%2Floops_iterator_example.svg?alt=media" alt=""><figcaption><p>Alternate on/off pattern for multiple LEDs</p></figcaption></figure>


# Loops

Loop blocks repeatedly execute code, eliminating the need to duplicate instructions. Use loops to process multiple sensor readings, generate patterns, handle collections of data, or create sequences of operations.

## repeat

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-9274a1ba9e2c4c30ca10a39f285b932f2abb8257%2Floops_repeat.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Executes the enclosed code a fixed number of times. Use this when you know exactly how many iterations you need. Inside it, the [iterator](#iterator) tells you which pass is running.

**Parameters:**

* **Count** (Number): Number of times to repeat. It is checked before every pass, so a count taken from a variable that the loop itself changes changes the number of passes too.

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-849ce8417c627ec1b17ba693103506fe8f357192%2Floops_repeat_example.svg?alt=media" alt=""><figcaption><p>Average sensor reading</p></figcaption></figure></div>

## repeat while

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-868eb50337a5094b4d691b612ff48c3c33ebce3b%2Floops_repeat_while.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Repeats code as long as the condition remains true. The condition is checked before each iteration.

**Parameters:**

* **Condition** (Boolean): The condition to evaluate before each iteration

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-dd2f76819a33d5172a9dee84f45d1156105c7f6a%2Floops_repeat_while_example.svg?alt=media" alt=""><figcaption><p>Process values while above threshold</p></figcaption></figure></div>

## repeat until

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-a4af7e444a7215b11f08e4038d211bc2440a3fd4%2Floops_repeat_until.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Repeats code until the condition becomes true. Like **repeat while**, it checks the condition before each pass, so if the condition is already true, the code inside doesn't run at all.

**Parameters:**

* **Condition** (Boolean): The condition to evaluate before each iteration

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-ce18fb9de5442ee8089a0c366e260fc0e019ddb2%2Floops_repeat_until_example.svg?alt=media" alt=""><figcaption><p>Read until valid value received</p></figcaption></figure></div>

## iterator

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-5a56da9950bf5b1b35e8743f7f158287bf65d6ef%2Floops_iterator.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Returns how many passes the loop around it has completed: `0` on the first pass, `1` on the second, and so on. It works in all three loops. Use it to create indexed operations or patterns.

**Returns:**

* **Number**: The current loop iteration (0, 1, 2, ...)

**Nested loops:** the iterator belongs to the innermost loop around it. Inside an inner loop it counts that loop's passes, starting again from `0` each time the inner loop starts. When the inner loop finishes, the iterator is the outer loop's count again. To use the outer loop's count inside the inner loop, store it in a variable before the inner loop starts:

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-bb508795c066c7aa36280eb47be9460350a8551e%2Floops_nested_example.svg?alt=media" alt=""><figcaption><p>Print the cells of a 3 × 4 grid, numbered 0 to 11</p></figcaption></figure></div>

```lisp
(define row ())

(task 1 1000 '
 (progn
  (while
   (< #itr 3)
   (setq row
    (+ #itr 0))
   (while
    (< #itr 4)
    (print
     (+
      (* row 4) #itr))))))
```

{% hint style="warning" %}
**Known issue:** a variable set to the **iterator** keeps a link to the loop's counter rather than its value, so it changes along with the counter. In the example above, `row` would follow the inner loop, and a count saved inside a loop reads `0` once the loop has finished. Until this is fixed, store a copy instead — **iterator + 0**, as the example does: the arithmetic makes a new number. The fix is coming in the next release of the UniotLisp interpreter, after which the iterator can be stored directly.
{% endhint %}

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-acfce49a2fc414f66852cfd65ee12d559f1406e2%2Floops_iterator_example.svg?alt=media" alt=""><figcaption><p>Alternate on/off pattern for multiple LEDs</p></figcaption></figure></div>

{% hint style="warning" %}
This block only works inside a loop. Anywhere else, and in a **repeat** block's count, the editor disables it and marks it with a warning. A disabled block is left out of the code, and the block around it falls back to its own default. In the example below, the comparison gets an empty value, `()`, and stops the script with `< takes only numbers`; a **set** block would quietly store `0` instead.

<img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-d47ee967aa94dd9c9d53973f3e09eec377a235ce%2Floops_iterator_outside.svg?alt=media" alt="" data-size="original">
{% endhint %}

## How long a loop may run

A loop, together with any loops inside it, may run for 20,000 passes in all. Past that, the script stops with `Loop ran for 20000 iterations without ending; possible endless loop`. Something that should go on for as long as the device runs belongs in the [task](/platform/sandbox/visual-editor/special#task), which repeats on its own.


# Text

Text blocks handle string operations and text-based data. Use these blocks to create text values, convert between strings and numbers, evaluate expressions, and send log messages.

## value

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-837cdeda21e7da5f24eddaedbe40afd32b6d74cd%2Ftext_value.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

A text string constant. Use this block to create fixed text values for labels, messages, or event names.

**Parameters:**

* **Value** (String): Enter any text

**Returns:**

* **String**: The text value

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-a25a8316efe68cf5740ec4a94293d471b10462db%2Ftext_value_example.svg?alt=media" alt=""><figcaption><p>Text value creation</p></figcaption></figure></div>

## quote

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-bd21f9ea01940dff57ebe53ace18eee368d2d200%2Ftext_quote.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Converts an expression or value into a string representation without evaluating it. Useful for capturing code as text.

**Parameters:**

* **Expression**: The value or expression to convert

**Returns:**

* **String**: The expression as text

## eval

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-b4d205dd23399d3b3ccf3a286966932a2ecc46f6%2Ftext_eval.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Evaluates a text string as UniotLisp code and returns the result. Use this to execute dynamic code or parse text-based expressions.

**Parameters:**

* **Expression** (String): Text containing UniotLisp code to evaluate

**Returns:**

* **Any**: The result of evaluating the expression

## print

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-bfa225e8612aac51e6eaf2a918148d2843a40f91%2Ftext_print.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Sends a log message to the MQTT broker. Messages appear on the device details page for monitoring and debugging, and in the Emulator's logs panel when the script runs in the [Emulator](/platform/sandbox/emulator#logs).

**Parameters:**

* **Message** (String): The text to log

**Example:**

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-339b8f8e44139e6d08beec6bc64713669e583d6a%2Ftext_print_example.svg?alt=media" alt=""><figcaption><p>Logging messages</p></figcaption></figure></div>


# Variables

Variables store and manage data in your scripts. Use variables to track changing values like sensor readings, counters, states, or calculated results.

{% hint style="info" %}
Create variables through the Variables section in the toolbox before using them. Variable names are case-sensitive - choose meaningful names to make your code easier to understand.
{% endhint %}

## set

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-f6404685a8acf84e53b61482722d21250a8d36b6%2Fvariables_set.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Assigns a value to a variable. If the variable doesn't exist, it's created automatically.

**Parameters:**

* **Variable**: Select from the dropdown or create a new variable
* **Value** (Any): The value to store

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-677227ca5dfc5b5e2de89b236659f5097e196d76%2Fvariables_set_example.svg?alt=media" alt=""><figcaption><p>Setting variables</p></figcaption></figure>

## get

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-1d6040b99d21d9311e89ae70688c569cf627b4b9%2Fvariables_get.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Retrieves the current value stored in a variable.

**Parameters:**

* **Variable**: Select which variable to read

**Returns:**

* **Any**: The current value of the variable

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-ff75062f70a21082eed18fb7dfd29d9517082e46%2Fvariables_get_example.svg?alt=media" alt=""><figcaption><p>Getting variable values</p></figcaption></figure>

## change

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-4cd06d13a7b4477d6f43a7efefcadfb72b12eace%2Fvariables_change.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Increments or decrements a numeric variable by adding a value to it. Use negative values to subtract.

**Parameters:**

* **Variable**: Select which variable to modify
* **Value** (Number): The amount to add (use negative for subtraction)

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-60a9a26c5283e8e2ae2204081a486301eca22b60%2Fvariables_change_example.svg?alt=media" alt=""><figcaption><p>Modifying variables</p></figcaption></figure>

## Managing Variables

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-b77bc24a224734ad3f937513c4daaebc23cb1f86%2Fvariables_dropdown.png?alt=media" alt="" width="233"><figcaption></figcaption></figure></div>

The variable dropdown menu in each block provides options to:

* **Select** existing variables from your script
* **Rename** the selected variable (updates all references)
* **Delete** the selected variable (removes from all blocks)

{% hint style="warning" %}
Renaming or deleting a variable affects **all blocks** that use it. Be careful when modifying variables used in multiple places.
{% endhint %}


# Functions

Functions let you organize code into reusable blocks. Create custom functions with parameters and return values to break down complex logic into manageable, reusable components.

## function definition

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-33a0cbe20a77c2a7e1d86dfd760bad0a69b9e3da%2Ffunctions_template.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Defines a new function that can be called from anywhere in your script. Click the gear icon to add parameters and configure the return value.

**Configuration:**

* **Function Name**: A unique identifier for your function
* **Parameters** (Optional): Input values the function accepts
* **Return** (Optional): Enable to make the function return a value

**Example uses**: Calculating averages, converting units, validating sensor ranges, or any logic you use multiple times.

## function call

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-bf75140b2d1ecb5d176f9f81baf5a8bc1a08ace9%2Ffunctions_call.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Executes a previously defined function. Provide values for any parameters the function expects.

**Parameters:**

* **Function**: Select which function to call
* **Arguments**: Values matching the function's parameter list

**Returns:**

* **Any**: The function's return value if configured, otherwise performs actions without returning a value


# Primitives

Primitive blocks provide direct access to device hardware. Use these blocks to read sensors, control digital and analog outputs, handle button inputs, and create custom hardware interactions.

## Understanding Registers

Primitives use [register](/general-concepts/primitives#the-register-system) indices instead of physical pin numbers. The register system organizes pins into categories (digital input/output, analog input/output) and assigns each an index. View your device's pin-to-register mapping in the Registers tab on the Uniot Platform.

## Return Values in Primitives

Primitives can either return a value or perform an action without returning meaningful data. The visual blocks for default primitives (like `analog read`, `digital read`, etc.) are predefined and correctly reflect whether they return values or not.

For user-defined primitives, the visual editor cannot automatically determine whether a primitive should return a value or just perform an action. Since primitives are C++ functions called by the UniotLisp Interpreter, every function must return something—even action-only primitives return a boolean value. Therefore, user primitive blocks are rendered with a return value by default. If your primitive doesn't need to return a meaningful value, you can disable the return value field in the block configuration.

## analog read

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-2bd9d886c56b6e028046db07df744e59bd43fa9b%2Fprimitives_analog_read.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Reads an analog value from the specified register. Use this for sensors that output variable voltage levels.

**Parameters:**

* **Register** (Number): The analog input register index

**Returns:**

* **Number**: Value between 0-1023 (10-bit ADC resolution, the same on every chip)

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-5d97e0ab5b2792663a688fbed1f86808cbfdf122%2Fprimitives_analog_read_example.svg?alt=media" alt=""><figcaption><p>Check light level and push event if it's too low</p></figcaption></figure>

## analog write

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-fa3a10df87b3c6b257d4202726e1178ff12c1774%2Fprimitives_analog_write.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Writes an analog (PWM) value to the specified register. Use this to control LED brightness, motor speed, or other variable outputs.

**Parameters:**

* **Register** (Number): The analog output register index
* **Value** (Number): Value between 0-1023 (PWM duty cycle)

**Returns:**

* **Number**: The value written

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-d696917a0cf6490450d3d2ee35e0586cafcfe05e%2Fprimitives_analog_write_example.svg?alt=media" alt=""><figcaption><p>Receive brightness value from dashboard and set LED accordingly</p></figcaption></figure>

## digital read

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-6b261e3d9fe9e0fd59ac5415993c0b83bcec5e08%2Fprimitives_digital_read.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Reads the digital state from the specified register (HIGH or LOW).

**Parameters:**

* **Register** (Number): The digital input register index

**Returns:**

* **Boolean**: `#t` for HIGH, `()` for LOW

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-ffedd00eadb0c25e6aefa2d685c05f49b743d3c9%2Fprimitives_digital_read_example.svg?alt=media" alt=""><figcaption><p>Read switch state and push event when changed</p></figcaption></figure>

## digital write

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-c547ff6a15ce9542496837cdf49f4e83c68e7214%2Fprimitives_digital_write.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Sets a digital register to HIGH or LOW. Use this to control LEDs, relays, or other on/off outputs.

**Parameters:**

* **Register** (Number): The digital output register index
* **Value** (Boolean): `#t` for HIGH, `()` for LOW

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-19c1818af99e1ac51b56caf6ebd9cca3be939838%2Fprimitives_digital_write_example.svg?alt=media" alt=""><figcaption><p>Check for event and control LED state</p></figcaption></figure>

## button clicked

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-a4cb80877adefe123dcef95a57fd5c2f87d73cc1%2Fprimitives_button_clicked.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Detects button press events on the specified register. Returns true when a press-and-release cycle is detected, then resets until the next click.

**Parameters:**

* **Register** (Number): The button's index in the `bclicked` register, in registration order. The WiFi reset button is usually `0`; see [Button Clicked](/general-concepts/primitives#available-built-in-primitives)

**Returns:**

* **Boolean**: `#t` if clicked since last check, `()` otherwise

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-271a832f810ce4ccebf6dc072d43bb058a9b4272%2Fprimitives_button_clicked_example.svg?alt=media" alt=""><figcaption><p>Toggle LED state on button click</p></figcaption></figure>

## user primitive (template)

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-9ab9ba1c41e43cb39d898cd576f5c5aecca79ae2%2Fprimitives_user_primitive.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Creates a custom primitive definition that you'll implement in your firmware. Click the gear icon to configure parameters and return type.

**Configuration:**

* **Primitive Name**: Unique identifier for your custom primitive
* **Parameters**: Define input values the primitive accepts
* **Return Type**: Configure what type of value it returns (boolean by default, or none for action-only primitives)

{% hint style="info" %}
**Note on return values**: Since primitives are C++ functions called by the UniotLisp Interpreter, every function must return a value. If your primitive performs an action without needing to return a meaningful value, it should still return a boolean. The visual block is rendered with a return value by default. If you don't need to use the return value, you have to disable the corresponding field in the block configuration.
{% endhint %}

**Use cases**: Creating custom sensor drivers, protocol handlers, complex hardware control, or specialized operations not covered by standard primitives.

## user primitive (autogenerated)

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-ae85f9c69be1025eed64a633b76422a8135a74a6%2Fprimitives_user_primitive_auto.svg?alt=media" alt=""><figcaption></figcaption></figure></div>

Calls a custom primitive already defined in your device firmware. These blocks are automatically generated based on primitives registered in your firmware code.

**Parameters:**

* **Arguments**: Values matching the primitive's defined parameters

**Returns:**

* **Type varies**: Depends on how the primitive was defined in firmware

{% hint style="info" %}
**Note on return values**: Since the visual editor cannot determine whether a user primitive is intended to return a value or just perform an action, autogenerated blocks are rendered with a return value by default. This is because all C++ functions must return something—even action-only primitives return a boolean value. If your primitive doesn't need to return a meaningful value, you have to disable the return value field in the block configuration.
{% endhint %}

**Example:**

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-934b1880b6dfc783d57c145b07fe14bc4f21d704%2Fprimitives_user_primitive_auto_example.svg?alt=media" alt=""><figcaption><p>Custom LED strip control</p></figcaption></figure>


# Logger

The Logger is the Sandbox's output pane. It shows the interpreter's report on the last run of your script — when you compile it, or while it runs in the [Emulator](/platform/sandbox/emulator) — with these parts:

* **`states`** - The run's timeline: every interaction with a primitive, such as a value read from or written to a pin, every event sent or received, and every line printed with the **print** block from the "Text" section, in the order they happened.
* **`log`** - Only the lines the script printed.
* **`out`** - The value the script finished with.
* **`err`** - If the script stopped with an error, its message and where in the code it occurred.

While the Emulator runs, the Logger follows it. The Emulator also shows printed lines and errors on their own, in its Logs panel.

The Logger is an essential tool for understanding the behavior of your script and ensuring its reliability before deployment.


# Emulator

The Emulator runs your script in the browser on the same UniotLisp interpreter a device uses. Every primitive the script calls is answered by an interactive component instead of a GPIO pin, so you can test logic, watch outputs, and debug before deploying to hardware.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-1d900c91b55a568790c296784d70f28c0497abb0%2Femulator_window.png?alt=media" alt=""><figcaption><p>Primitive cards on the left, script logs on the right</p></figcaption></figure>

## Quick Start

1. **Compile** the script in the Sandbox
2. **Run** it with the play button or Ctrl+Enter (Cmd+Enter on Mac)
3. **Interact** - flip switches, turn knobs, click buttons
4. **Read the logs** on the right for `print` output and errors
5. **Stop, edit, compile, run again** until it behaves, then **Deploy**

{% hint style="info" %}
The Emulator is a floating window. You can open other pages while it runs; it keeps going until you close it or the script finishes.
{% endhint %}

## What Gets Emulated

* **Built-in primitives** get a matching control: a switch for `dread`, an LED for `dwrite`, a knob for `aread`, a gauge for `awrite`, a button for `bclicked`.
* **Your own primitives** become [user primitive](#user-primitive) cards whose return value you configure.
* **A device, optionally.** Select one in the sidebar before starting and the Emulator uses its primitives, [registers](/general-concepts/primitives#the-register-system), and memory size, and flags anything the script uses that the device does not have. The **Deploy** button then appears in the header.

Only one emulation runs at a time. While it runs, the device selector is locked and other scripts in your list are greyed out.

{% hint style="info" %}
**Writing UniotLisp by hand?** The Emulator only knows the primitives declared in the script's [user library block](/general-concepts/primitives#user-library-block), built-ins included. The Visual Editor writes that block for you; in the Code Editor you declare them yourself, or compilation fails with "Undefined symbol".
{% endhint %}

## Starting and Stopping

| Action                      | How                                                                                                                       |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Start from the Sandbox      | Compile first, then the play button or Ctrl+Enter. The button stays disabled while the code shows **Recompile required**. |
| Start from the scripts list | **Emulate** in a script's menu. Always runs without a device.                                                             |
| Stop                        | The cross in the header, the stop button in the Sandbox, Ctrl+Enter again, or **Deploy** from the header.                 |
| Script finishes on its own  | The window stays open with the title **Finished** so you can still read everything.                                       |

{% hint style="info" %}
Editing the script during a run does not affect it. The Sandbox shows **Recompile required**, and Compile waits until you stop the Emulator.
{% endhint %}

## The Window

### Header

| Control          | What it does                                                                                                                           |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Status dot       | Green and pulsing while running. Yellow when a card has a warning, red when the run ended with an error. Hover it for the reason.      |
| Title            | "Running..." or "Finished", prefixed with the device name when one is selected.                                                        |
| Deploy           | Only with a device selected. Sends the emulated script to it and closes the Emulator.                                                  |
| Logs             | Shows or hides the [logs panel](#logs). Remembered for later runs.                                                                     |
| Debug (bug icon) | Lights a red dot on the card being called and **pauses 300 ms after every call**, so you can follow the order of calls. Slows the run. |
| Help             | Reminds you the Emulator keeps running while you browse.                                                                               |
| Close            | Stops the run and closes the window.                                                                                                   |

Drag the window by its header. Drag cards to reorder them.

### Cards

One card per primitive, one control per register, with the register index on the control. Hover the card name to see the primitive's name in code (**Analog Read** is `aread`).

A triangle icon on a card means it has something to tell you. Hover it to read the message:

| Message                                                                         | Meaning                                                                                    |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Primitive is not used in the script                                             | The device has it, the script never calls it. Informational.                               |
| Unavailable in the selected device                                              | The script calls a primitive the device does not have.                                     |
| Registers are out of bounds                                                     | The script uses a register the device does not have. Check the device's **Registers** tab. |
| Return type mismatch, Parameter type mismatch, Parameter index is out of bounds | The script calls a device primitive with a different signature than the device declares.   |
| The limit of 10 registers has been reached                                      | A card shows at most ten registers.                                                        |
| User function error                                                             | The JavaScript function of a user primitive failed. The run stops.                         |

A script with no primitives shows "No primitives were found in the script" instead of cards.

## Components

### Digital Read

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-b5cfd9ce35fe89c3be22e54fdf6e7acb390747d7%2Fdigital_read.png?alt=media" alt=""><figcaption></figcaption></figure></div>

A switch. On, `dread` returns `#t` (pin HIGH); off, it returns `()` (pin LOW).

### Digital Write

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-db7f378e044bf01e6bcdb5185b4f9e34e3133c73%2Fdigital_write.png?alt=media" alt=""><figcaption></figcaption></figure></div>

An LED, lit while the script holds the pin HIGH. Display only.

### Analog Read

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-878c99ed37ce23f72a940f050dc064bc4906cc41%2Fanalog_read.png?alt=media" alt=""><figcaption></figcaption></figure></div>

A knob. Drag or click it to set 0 to 1024. The script sees the new value about half a second after you stop turning.

### Analog Write

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-d4a19c72e1e8ca56d31e3beac8eba3c9df0b30c6%2Fanalog_write.png?alt=media" alt=""><figcaption></figcaption></figure></div>

A gauge showing the last value written, 0 to 1024. Display only.

### Button Clicked

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-f2315ba40247e3f2b71a4e41a064c1011c74912a%2Fbutton_clicked.png?alt=media" alt=""><figcaption></figcaption></figure></div>

A push button. One click is one press: the next `bclicked` call returns **true** and clears it. Holding the button does not repeat the press.

### User Primitive

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-67449eb4f4f559796c163183955d54eee090d7b7%2Fuser_primitive.png?alt=media" alt=""><figcaption></figcaption></figure></div>

Your own primitives, and any device primitive beyond the built-ins. The blocks on the right show each argument's value and type as the script calls it. The block on the left shows the return value: blue once you enable one, red with `ERR` when a function fails. Click its gear to configure it.

#### Configuring Return Values

<div align="left"><figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-cb7d5ee1e48fc44794f94d1cd62f58c77af705f3%2Fuser_primitive_settings.gif?alt=media" alt=""><figcaption></figcaption></figure></div>

* **Enable Return Value** - off, the primitive returns its type's default (`0`, `#t`, or `_`).
* **Return Type** - `Int`, `Bool`, or `Symbol`. Fixed to the declared type for device primitives.
* **Enter Manually** - a number, **True**/**False**, or a symbol of up to 30 characters (characters a symbol cannot contain become `?`, spaces become `_`).
* **Use Function** - a JavaScript function computed on every call. Double-click the empty field for a template.

The function is validated on **Apply**: one synchronous function, no `async`, no generators. Inside it you have:

| Available                                 | Notes                                                                                                   |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Arguments                                 | The strings the interpreter sent: `"42"`, `"#t"`, `"()"`                                                |
| `state.get(key)`, `state.set(key, value)` | Kept between calls for the run. Up to 100 keys; values are number, boolean, string, or `null` (deletes) |
| `Math`, `Date.now()`                      | Nothing else: no network, timers, console, or page access                                               |
| Return value                              | A string, boolean, or finite number, within 500 ms                                                      |

```javascript
// Returns how many times the primitive has been called
(_1 /* Int */) => {
  const count = (state.get("calls") || 0) + 1;
  state.set("calls", count);
  return count;
};
```

If the function throws, times out, or returns anything else, the card turns red, the logs get an `ERROR` line with the cause, and the run stops.

<figure><img src="https://1776027180-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsSE1iwGRAGqUrb5YXfFa%2Fuploads%2Fgit-blob-54be1da85bb5ee1b48d29cc1da73dafdbec8d982%2Femulator_error.png?alt=media" alt=""><figcaption><p>A failed function marks the card, logs the cause, and ends the run</p></figcaption></figure>

## Logs

The panel on the right shows what the script prints, the way the **Logs** tab on a device page does for a deployed script. Every [print](/platform/sandbox/visual-editor/text#print) adds a timestamped line; errors are added as they happen with their full message:

```
[15:04:05.678] 42
[15:04:06.178] (1 2 3)
[15:04:07.178] ERROR  Undefined symbol: foo
```

* Follows the newest line unless you scroll up; scroll back down to resume.
* Keeps the last 500 lines. Cleared on every run, or any time with the trash icon.
* Hide or show it with the **Logs** button in the header; the choice is remembered.

{% hint style="info" %}
The [Logger](/platform/sandbox/logger) pane in the Sandbox is a different view: the compiler's output and, during a run, a trace of the last primitive calls and their answers. Use it to see what the script asked for; use the Emulator's logs to see what the script said.
{% endhint %}

## Events

[is event, pop event, and push event](/platform/sandbox/visual-editor/special) use the real MQTT broker:

* **push event** publishes on your account's event topic with sender type `emulator`. Dashboard widgets and devices listening for that event react as they would to a device.
* Events from widgets or devices are queued for the script, five per event name. The last retained value of an event seeds the queue on the first check.
* The Emulator's own pushes come back to it, so a script can see what it pushed.

{% hint style="warning" %}
**Pushed events are real.** A deployed device subscribed to the event will act on it. Test with event names that are not wired to production devices.
{% endhint %}

## Limits

| What                    | Limit                                                                                      |
| ----------------------- | ------------------------------------------------------------------------------------------ |
| `task` timing           | Intervals are waited in real time. A task set to run forever stops after 9,999 iterations. |
| Memory                  | The selected device's UniotLisp heap, or 14,336 bytes without a device.                    |
| Registers per primitive | 10                                                                                         |
| Custom function time    | 500 ms per call                                                                            |
| Logs                    | 500 lines                                                                                  |
| Event queue             | 5 values per event name                                                                    |

See [Debugging Scripts](/general-concepts/scripting#debugging-scripts) for how logs and errors reach you from a deployed device.


# Firmware Installer

[install.uniot.io](https://install.uniot.io) installs and updates the [Uniot Badge](/guides/uniot-badge) firmware from your browser. It tells you which firmware the badge is running and which is available, and when it's done it checks that the new version has started. You don't need to install anything on your computer.

{% hint style="info" %}
The installer has firmware for the Uniot Badge only. To put Uniot Core on another board, build your own firmware with PlatformIO, as in [Getting Started](/guides/getting-started).
{% endhint %}

## What you need

* **Desktop Chrome, Edge or Opera.** The installer talks to the badge over Web Serial, which Firefox, Safari and phone browsers don't support.
* **A USB-C cable that carries data.** With a charge-only cable, the badge never appears in the browser's list.
* **On Linux, access to serial ports.** On most distributions this means your user is in the `dialout` group (`uucp` on Arch). Log out and back in after adding yourself to it.

## Install or update

1. Open [install.uniot.io](https://install.uniot.io), plug the badge in, and press **Connect**.
2. Choose the badge in the list your browser shows. The installer asks the badge what it's running and shows it next to the latest release:
   * **On the badge** — the installed version and radio build, or that no Uniot firmware answered: a new board, or one running something else.
   * **Available** — the latest release, with a link to what's new in it.
3. Choose the **WiFi radio** build — see [Which radio build](#which-radio-build). The build the badge already has is marked **installed**.
4. Press the main button. It says what it will do: **Update to** a newer version, **Reinstall** the one the badge has, or **Install** on a badge running anything else.

The installer downloads the firmware, checks it against its checksum, writes it, and restarts the badge. Keep the badge plugged in until it has finished. When the badge reports the new version, the installer says so: *The badge is running 1.0.0 · Compatible*.

Nothing is written until you press the main button. **Disconnect** releases the badge at any point before that.

## What an update keeps

An update keeps the badge's WiFi settings, its identity on the platform and its script. When it's done, the badge reconnects and carries on, with nothing to set up again. The same is true when you switch radio builds or reinstall.

A new board, or one that you've erased, starts with nothing stored. After installing:

1. Join the WiFi network named `UNIOT-…` from your phone or computer.
2. On the page that opens, enter your WiFi details and your Uniot account ID.
3. When the badge is online, authorize it on the **Devices** page — see [Authorize the Device](/guides/getting-started#authorize-the-device).

## Which radio build

Some ESP32-C3 modules can't hold a WiFi connection at the chip's full transmit power, and nothing on the board tells you which ones.

| Build          | Transmit power | Use it when                                                                                                |
| -------------- | -------------- | ---------------------------------------------------------------------------------------------------------- |
| **Compatible** | 8.5 dBm        | By default. It works on every badge, at a shorter range. It's what badges ship with.                       |
| **Full range** | 19.5 dBm       | You want more range and your badge's radio copes. If the badge never connects, install Compatible instead. |

Switching between them keeps the badge's settings.

## Erasing

**Erase everything first** is off unless you tick it. It removes the badge's WiFi settings, script and identity. Afterwards the badge has to be set up and authorized again, like a new one. You only need it for a board that previously ran other firmware.

## Testing the hardware

**Test the hardware instead** installs a small test program in place of the firmware. It checks all four peripherals without a network or an account. The ring sweeps and the badge buzzes, then the ring follows your hand as you move it over the distance sensor. If a red dot chases around the ring instead, the sensor didn't answer. The button drives the vibration motor throughout.

When you're done, press **Install the firmware** to go back. The test doesn't touch the badge's settings, so it comes back as it was.

## The serial console

Below the installer, the serial console shows the badge's log while it is connected. Choose how much to show with the level filter, **Restart** the badge, or **Copy**, **Save** or **Clear** the log. If something goes wrong, the log is the first thing to look at, and the thing to attach when you ask for help.

## Troubleshooting

**The badge isn't in the browser's list.** Try another cable. Many USB-C cables only carry power. On Linux, check that your user can open serial ports (see [What you need](#what-you-need)).

**The port is in use.** Another program has the badge open. Close any serial monitor, the Arduino IDE or PlatformIO's monitor, or another browser tab connected to it, then try again.

**Couldn't start the installation.** The badge didn't switch into the mode that accepts new firmware. Unplug it, then hold the **BOOT** button on its ESP32-C3 board while you plug it back in. Release the button and try again. The chip's bootloader can always accept new firmware this way, even when the firmware on it doesn't start.

**This is a different board.** The installer only has firmware for the Uniot Badge, which is built on an ESP32-C3.

**The badge keeps restarting after an update.** Watch the serial console. If it doesn't settle, install again, and tick **Erase everything first** if it still doesn't.

**The badge never connects to WiFi.** If it runs the **Full range** build, install **Compatible**. Otherwise, reset its WiFi settings: click the button four times, then press and hold it. The badge opens its `UNIOT-…` network again, where you can enter the details anew. Powering it off and on five times in quick succession does the same.

## Building your own

The installer also works for a badge you've built yourself on a breadboard, if it uses the badge's pins. The released firmware has them built in. It finds boards through the USB-serial chips common on ESP32 development boards, not only through the C3's built-in USB. If you've moved any pins, build the firmware from source instead — see the [firmware repository](https://github.com/uniot-io/uniot-promo-badge-firmware#building-one-without-a-badge).


# Uniot Core

Uniot Core is the open-source firmware framework every Uniot device runs. It connects a device to the platform, keeps it connected, and runs the scripts you deploy to it — so your firmware only has to describe the hardware. It is written for the Arduino framework on ESP8266 and ESP32 boards, and built with PlatformIO.

The source, the full API reference and the examples are in the [uniot-core](https://github.com/uniot-io/uniot-core) repository. This page is a map of it.

## What's inside

### Task scheduler

Runs periodic and one-shot work without blocking the device, with `setTimeout`, `setInterval` and `setImmediate`. See [Task Scheduler](https://github.com/uniot-io/uniot-core/blob/master/docs/reference.md#1-task-scheduler) in the reference.

### Event system

A publish-subscribe bus the parts of the firmware use to talk to each other without depending on one another. See [Event System](https://github.com/uniot-io/uniot-core/blob/master/docs/reference.md#2-event-system).

### WiFi management

Connects to the network and reconnects when it drops. Credentials come either from your code or from a captive portal where the device's user enters them. A reset button — or, if you enable it, a quick series of reboots — lets a device that can no longer reach its network start over. See [WiFi Management](https://github.com/uniot-io/uniot-core/blob/master/docs/reference.md#3-wifi-management).

### UniotLisp scripting

An embedded interpreter runs the scripts you deploy from the platform, so a device's behaviour changes without reflashing. Your firmware decides what scripts can reach: which pins, which buttons, and which functions of your own. See [Scripting](/general-concepts/scripting) and [Primitives](/general-concepts/primitives) in these docs, and [UniotLisp Scripting](https://github.com/uniot-io/uniot-core/blob/master/docs/reference.md#4-uniotlisp-scripting) in the reference.

### Storage management

Keeps credentials, configuration, the deployed script and your own data in flash, in the compact CBOR format. See [Storage Management](https://github.com/uniot-io/uniot-core/blob/master/docs/reference.md#5-storage-management).

### Time management

Keeps the clock in sync over NTP, and saves it so that a device starts with roughly the right time after a reboot, before the network is back. See [Time Management](https://github.com/uniot-io/uniot-core/blob/master/docs/reference.md#6-time-management).

Messages between the device and the platform travel over MQTT, and the device signs what it publishes with its own Ed25519 key.

## Compatibility

| Family  | Boards                                     |
| ------- | ------------------------------------------ |
| ESP8266 | ESP-12E, ESP-12F, NodeMCU, Wemos D1 Mini   |
| ESP32   | ESP32 DevKit, ESP32-C3, ESP32-S2, ESP32-S3 |

## Getting started

Add Uniot Core to a PlatformIO project:

```ini
lib_deps =
    uniot-io/uniot-core@^0.9.0
```

[Getting Started](/guides/getting-started) walks through a first project from an empty folder to a device on your dashboard.

## Build flags

Uniot Core is configured with build flags in `platformio.ini`. One is required — `UNIOT_CREATOR_ID`, and the build fails without it. The rest tune logging, storage, memory and the network, and are listed with their defaults in [Build Flags](https://github.com/uniot-io/uniot-core#build-flags) in the repository.

## Examples

Each is a complete PlatformIO project in the repository.

| Example                                                                              | Shows                                                                   |
| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| [WittyCloud](https://github.com/uniot-io/uniot-core/tree/master/examples/WittyCloud) | RGB output, a light sensor and a button, all exposed to scripts         |
| [My9231Lamp](https://github.com/uniot-io/uniot-core/tree/master/examples/My9231Lamp) | A custom primitive, on a smart bulb with no button or status LED        |
| [S20Socket](https://github.com/uniot-io/uniot-core/tree/master/examples/S20Socket)   | Relay control and a scriptable GPIO                                     |
| [LispHooks](https://github.com/uniot-io/uniot-core/tree/master/examples/LispHooks)   | Powering a sensor on and off around the script that uses it             |
| [Buttons](https://github.com/uniot-io/uniot-core/tree/master/examples/Buttons)       | Two buttons exposed to scripts, and clearing presses no script has read |

For a complete device, see the [Uniot Badge firmware](https://github.com/uniot-io/uniot-promo-badge-firmware).

## Reference

* [API reference](https://github.com/uniot-io/uniot-core/blob/master/docs/reference.md#api-reference) — every method, with its parameters
* [Device status](https://github.com/uniot-io/uniot-core/blob/master/docs/reference.md#device-status) — what a device reports to the platform
* [Troubleshooting](https://github.com/uniot-io/uniot-core/blob/master/docs/reference.md#troubleshooting) — WiFi, memory and upload problems
* [Changelog](https://github.com/uniot-io/uniot-core/blob/master/CHANGELOG.md) — what changed in each release, and what to check before updating a device

Uniot Core is licensed under the [GNU General Public License v3.0](https://github.com/uniot-io/uniot-core/blob/master/LICENSE).


# Uniot Lisp

The **UniotLisp** interpreter is a lightweight implementation of the Lisp programming language, designed for embedded systems and resource-constrained environments. This document provides an overview of its implementation, available primitives, instructions for extending functionality, and examples for developers.\
\
This guide is divided into two main parts to cater to different user needs:

1. [**Language Description**](/advanced/uniot-lisp/language-description)
   * Explains the UniotLisp language features, including data types, operators, conditionals, loops, macros, and more.
   * Provides examples and usage scenarios to illustrate how to effectively use the language.
   * Ideal for developers and programmers looking to write UniotLisp code.
2. [**Embedding Instructions**](/advanced/uniot-lisp/embedding-instructions)
   * Covers setup, initialization, extending the interpreter, managing environments, error handling, and utilizing the API.
   * Includes practical examples to demonstrate embedding techniques.
   * Perfect for developers who want to extend the functionality of the interpreter and understand the details better.


# Language Description

This page describes the UniotLisp language itself: its values, its forms, and how they behave. It is for anyone writing UniotLisp by hand, or wanting to understand the code the [Visual Editor](/platform/sandbox/visual-editor) generates.

What a script can do on a device — run tasks, exchange events, read and drive pins — comes from functions the device provides. Those are covered in [Scripting](/general-concepts/scripting) and [Primitives](/general-concepts/primitives).

## Reading the examples

UniotLisp reads one expression at a time, evaluates it, and returns a value. In the examples, `; ->` shows what an expression evaluates to:

```lisp
(+ 1 2)   ; -> 3
```

When an expression stops with an error instead, the example says so:

```lisp
(/ 7 0)   ; -> error: Division by zero
```

## Values

### Integers

Integers are 32-bit and signed. Arithmetic that would overflow stops with an error rather than wrapping around, and a number too large to represent is refused when the script is read.

```lisp
42                ; -> 42
-7                ; -> -7
(* 2000000000 2)  ; -> error: Integer overflow in *
```

There are no floating-point numbers. To work with fractions, scale your values — hundredths of a degree instead of degrees, for example.

### Truth: `()` and `#t`

`()` is the only false value. It is also the empty list.

`#t` is the true literal, but **any value other than `()` is true — including `0`**:

```lisp
(if () 'yes 'no)   ; -> no
(if 0 'yes 'no)    ; -> yes
(if #t 'yes 'no)   ; -> yes
```

This matters on a device, where a pin read or an event value arrives as a number, and `0` is a true value like any other. To treat `0` as false, use [`bool`](#truth-and-predicates).

### Symbols

Symbols are names, such as `x`, `counter` or `my-function`. Two symbols with the same name are the same object. UniotLisp has no string type, so symbols often stand in for text:

```lisp
'hello   ; -> hello
```

### Lists

Lists are built from cons cells: pairs of two values. A proper list ends in `()`; a dotted list ends in any other value.

```lisp
'(a b c)     ; -> (a b c)
'(a . b)     ; -> (a . b)
'(a b . c)   ; -> (a b . c)
```

## Quoting and evaluation

A symbol is evaluated to the value it names, and a list to the result of calling its first element. **`quote`** prevents that, returning its argument as data. The single quote `'` is shorthand for it.

```lisp
(quote (a b c))   ; -> (a b c)
'(a b c)          ; -> (a b c)
'x                ; -> x
```

**`eval`** does the opposite: it evaluates data as code.

```lisp
(eval '(+ 1 2))   ; -> 3
(define a 10)
(eval '(+ a 5))   ; -> 15
```

**`list`** builds a list from its arguments, which — unlike with `quote` — are evaluated first:

```lisp
(list 1 2 3)             ; -> (1 2 3)
(list (+ 1 2) (* 3 4))   ; -> (3 12)
```

## Lists

| Function                | What it does                                                  |
| ----------------------- | ------------------------------------------------------------- |
| `(cons a b)`            | A new pair with `a` first and `b` second                      |
| `(car list)`            | The first element                                             |
| `(cdr list)`            | Everything after the first element                            |
| `(setcar pair value)`   | Replaces the first element of a pair                          |
| `(setcdr pair value)`   | Replaces the rest of a pair                                   |
| `(length list)`         | The number of elements                                        |
| `(list a b ...)`        | A list of its arguments                                       |
| `(apply function list)` | Calls a function with the elements of a list as its arguments |

```lisp
(cons 'a 'b)             ; -> (a . b)
(cons 'a '(b))           ; -> (a b)
(car '(a b))             ; -> a
(cdr '(a b))             ; -> (b)
(length '(1 2))          ; -> 2
(apply + (list 1 2 3))   ; -> 6
```

`car` and `cdr` of `()` are `()`, so walking off the end of a list is not an error:

```lisp
(car ())   ; -> ()
(cdr ())   ; -> ()
```

## Arithmetic

`+`, `-`, `*`, `/` and `%` take any number of arguments; `abs` gives the magnitude of a number.

```lisp
(+ 1 2 3)   ; -> 6
(- 3)       ; -> -3
(- 5 2 7)   ; -> -4
(* 2 3 4)   ; -> 24
(abs -5)    ; -> 5
(+)         ; -> 0
(*)         ; -> 1
```

`/` and `%` are integer operations that truncate towards zero:

```lisp
(/ 7 2)    ; -> 3
(/ -7 2)   ; -> -3
(% 10 3)   ; -> 1
(% -7 2)   ; -> -1
```

Division by zero and overflow both stop the script with an error.

## Comparison

`<`, `<=`, `>` and `>=` compare numbers. Given more than two arguments, they check every neighbouring pair:

```lisp
(< 2 3)     ; -> #t
(< 3 3)     ; -> ()
(<= 3 3)    ; -> #t
(< 1 2 3)   ; -> #t
(< 1 3 2)   ; -> ()
```

**`eql`** is equality. Numbers are equal when their values are; everything else only when it is the very same object. Two lists with the same contents are not `eql`, because they are two different lists.

```lisp
(eql 1 1)                 ; -> #t
(eql 'a 'a)               ; -> #t
(eql (list 1) (list 1))   ; -> ()
```

**`=`** compares numbers, and also compares a number with a truth value by treating any non-zero number as true. That makes it convenient for testing a flag — but it is not an equality, so use `eql` when you mean "the same".

```lisp
(= 11 11)   ; -> #t
(= 1 #t)    ; -> #t
(= 0 ())    ; -> #t
```

## Truth and predicates

These ask what kind of value something is:

| Predicate   | True when the value is              |
| ----------- | ----------------------------------- |
| `numberp`   | a number                            |
| `symbolp`   | a symbol                            |
| `consp`     | a pair — a non-empty list           |
| `listp`     | a list, including `()`              |
| `atom`      | anything but a pair, including `()` |
| `functionp` | a function                          |

```lisp
(numberp 1)       ; -> #t
(consp '(a))      ; -> #t
(atom 'a)         ; -> #t
(listp ())        ; -> #t
(functionp car)   ; -> #t
```

**`not`** inverts truth under the language's own rule, where only `()` is false:

```lisp
(not ())   ; -> #t
(not 0)    ; -> ()
```

**`bool`** converts a value to `#t` or `()` under the *numeric* rule instead: `0` and `()` are false, everything else is true. It exists for device values — a pin read or an event value carries its truth as `0` or `1`, and `not` on one of those asks the wrong question.

```lisp
(bool 0)    ; -> ()
(bool 1)    ; -> #t
(bool ())   ; -> ()
(bool 'a)   ; -> #t
```

`bool` never fails, whatever it is given, and applying it to a value that is already `#t` or `()` changes nothing. If a script uses it often, give it a shorter name — `?` is a valid symbol:

```lisp
(define ? bool)
(? 0)   ; -> ()
```

## Conditionals and sequencing

**`if`** takes a condition, an expression to evaluate when it is true, and optionally one for when it is false:

```lisp
(if (< 1 2) 'smaller 'larger)   ; -> smaller
(if () 'yes)                    ; -> ()
```

**`progn`** evaluates several expressions in order and returns the last one's value — the way to do more than one thing where a single expression is expected:

```lisp
(progn 1 2 3)   ; -> 3
```

**`and`** and **`or`** stop as soon as the answer is known, and return the value that decided it:

```lisp
(and 1 2)    ; -> 2
(and 1 ())   ; -> ()
(or () 5)    ; -> 5
(and)        ; -> #t
(or)         ; -> ()
```

Because `and` stops at the first false value, it can guard an expression that would otherwise fail — such as `(and (consp x) (car x))`.

## Loops and recursion

**`while`** repeats its body as long as its condition is true. Inside the loop, `#itr` is the number of the current iteration, counting from zero; in nested loops it belongs to the innermost one.

```lisp
(define total 0)
(while (< #itr 3) (setq total (+ total 1)))
total   ; -> 3
```

When an inner loop finishes, `#itr` is the outer loop's count again. To use the outer count inside the inner loop, store it first:

```lisp
(define row ())
(define cells 0)
(while (< #itr 3)
  (setq row (+ #itr 0))
  (while (< #itr 4) (setq cells (+ cells (+ (* row 4) #itr)))))
cells   ; -> 66
```

{% hint style="warning" %}
**Known issue:** `(setq row #itr)` stores a link to the loop counter rather than its value, so `row` changes along with `#itr`: it follows the inner loop, and a count saved inside a loop reads `0` once the loop has finished. Until this is fixed, store a copy, as above: `(+ #itr 0)` makes a new number. The fix is coming in the next release of the interpreter, after which `(setq row #itr)` keeps its value.
{% endhint %}

On a device, a script usually doesn't loop forever itself. It runs its work in a [task](/general-concepts/scripting), which the device repeats on a schedule. A loop that never ends is stopped with an error rather than hanging the device.

**A call in tail position uses no extra memory**, so a function that calls itself as the last thing it does runs in constant space, however long the list. This is the usual way to walk a list:

```lisp
(defun sum (l acc)
  (if (eql l ()) acc (sum (cdr l) (+ acc (car l)))))
(sum '(1 2 3) 0)   ; -> 6
```

Recursion that is *not* in tail position does use memory with each call, and is limited by the space the device sets aside for evaluation. Write recursive functions with an accumulator, as `sum` does, and they won't reach that limit.

## Definitions

| Form                                 | What it does                              |
| ------------------------------------ | ----------------------------------------- |
| `(define name value)`                | Binds a variable                          |
| `(defun name (params ...) body ...)` | Defines a function                        |
| `(lambda (params ...) body ...)`     | Makes a function without a name           |
| `(setq name value)`                  | Assigns to a variable that already exists |

`define` and `defun` may be used again on the same name to replace it.

```lisp
(define a (+ 1 2))
a                          ; -> 3
(defun double (x) (+ x x))
(double 4)                 ; -> 8
(define triple (lambda (x) (* 3 x)))
(triple 4)                 ; -> 12
```

`setq` only assigns to a variable that already exists:

```lisp
(setq a 5)            ; -> 5
(setq undefined 1)    ; -> error: Undefined symbol: undefined
```

A function must be called with exactly as many arguments as it has parameters, and the parameters must have different names:

```lisp
(double 1 2)            ; -> error: Cannot apply function: too many arguments
(defun f (x x) x)       ; -> error: Duplicate parameter: x
```

A parameter list may end in `. name`, which collects the remaining arguments into a list — or be a single name, which collects all of them:

```lisp
(defun rest-of (a . rest) rest)
(rest-of 1 2 3)          ; -> (2 3)
(defun all-of args args)
(all-of 1 2)             ; -> (1 2)
```

Variables live as long as something still uses them. A function can keep one of its own between calls:

```lisp
(define counter
  ((lambda (count)
     (lambda () (setq count (+ count 1)) count))
   0))
(counter)   ; -> 1
(counter)   ; -> 2
```

## Macros

**`defmacro`** defines a macro: it receives its arguments unevaluated, and returns a form that is evaluated in its place.

```lisp
(defmacro unless (test . body)
  (list 'if test () (cons 'progn body)))
(unless (= 1 1) 'no)    ; -> ()
(unless (= 1 2) 'yes)   ; -> yes
```

**`macroexpand`** shows the form a macro produces. It is a function, so quote what you give it:

```lisp
(macroexpand '(unless (= 1 1) 'no))   ; -> (if (= 1 1) () (progn (quote no)))
```

**`gensym`** returns a new symbol that no other code can name, for macros that need a variable of their own.

## Output

**`print`** writes a value and returns it, so it can sit inside a larger expression:

```lisp
(print 'hello)      ; -> hello
(+ 1 (print 2))     ; -> 3
```

On a device, printed lines go to the device's log; in the Sandbox, they appear in the [Logger](/platform/sandbox/logger) and the [Emulator](/platform/sandbox/emulator). See [Debugging Scripts](/general-concepts/scripting#debugging-scripts).

## Comments

`;` begins a comment that runs to the end of the line.

```lisp
; This whole line is a comment
(+ 1 2) ; so is the rest of this one
```

## Writing your own helpers

UniotLisp deliberately leaves out what can be written in UniotLisp itself: a script pays in memory only for what it defines. Here are common helpers, a few lines each. Copy the ones a script needs.

One rule runs through all of them: **call yourself in tail position.** These versions carry their result along as they go, and run in constant space on a list of any length.

```lisp
(defun reverse (l acc)
  (if (eql l ()) acc (reverse (cdr l) (cons (car l) acc))))

(defun member (x l)
  (if (eql l ()) () (if (eql x (car l)) l (member x (cdr l)))))

(defun nth (n l) (if (= n 0) (car l) (nth (- n 1) (cdr l))))

(defun reduce (f l acc)
  (if (eql l ()) acc (reduce f (cdr l) (f acc (car l)))))

(defun map (f l) (map1 f l ()))
(defun map1 (f l acc)
  (if (eql l ()) (reverse acc ()) (map1 f (cdr l) (cons (f (car l)) acc))))

(defun filter (f l) (filter1 f l ()))
(defun filter1 (f l acc)
  (if (eql l ())
      (reverse acc ())
      (filter1 f (cdr l) (if (f (car l)) (cons (car l) acc) acc))))

(defun min (a b) (if (< a b) a b))
(defun max (a b) (if (> a b) a b))
```

```lisp
(reverse '(1 2 3) ())               ; -> (3 2 1)
(nth 1 '(a b c))                    ; -> b
(reduce + '(1 2 3) 0)               ; -> 6
(map double '(1 2 3))               ; -> (2 4 6)
(filter (lambda (x) (> x 1)) '(1 2 3))   ; -> (2 3)
(max 3 7)                           ; -> 7
```

Control structures are macros:

```lisp
(defmacro when (test . body) (list 'if test (cons 'progn body) ()))

;; cond expands one clause at a time, so it takes any number of them
(defmacro cond (clause . rest)
  (if (eql rest ())
      (list 'if (car clause) (cons 'progn (cdr clause)) ())
      (list 'if (car clause) (cons 'progn (cdr clause)) (cons 'cond rest))))
```

```lisp
(when (< 1 2) 'first 'second)          ; -> second
(cond ((< 5 3) 'less) ((> 5 3) 'more)) ; -> more
```

A local variable is a `lambda` applied where it stands:

```lisp
((lambda (x y) (+ x y)) 1 2)   ; -> 3
```

## Coming from an older version

Scripts written before UniotLisp 0.3.0 may need changes. Those that matter most:

* **`eq` is now `eql`.**
* **Only `()` is false.** `0` is true, so `(not 0)` is `()` — use `bool` for device values.
* **`and` and `or` return the value that decided them**, not always `#t`.
* **Overflow is an error**, and **`/` is exact integer division**.
* **Calling a function with the wrong number of arguments is an error.**
* **`define` and `defun` replace an existing definition** instead of refusing it.

The full list is in the [UniotLisp changelog](https://github.com/uniot-io/uniot-lisp/blob/master/CHANGELOG.md).


# Embedding Instructions

This page is for C and C++ developers who want to run the UniotLisp interpreter inside their own program: create it, give it functions of their own, and evaluate code with it.

{% hint style="info" %}
If you are writing firmware with [Uniot Core](/advanced/uniot-core), you don't need this page. Uniot Core creates and runs the interpreter for you, and adds functions to it through a higher-level API — see [Primitives](/general-concepts/primitives).
{% endhint %}

The interpreter is a single C file with no dependencies beyond the C standard library. It allocates one heap when it starts and never calls `malloc` again, which is what makes it suitable for microcontrollers.

## Getting the source

The source is in the [uniot-lisp](https://github.com/uniot-io/uniot-lisp) repository: `src/libminilisp.c` and `src/libminilisp.h`. With PlatformIO, add it to your project by version tag:

```ini
lib_deps =
    https://github.com/uniot-io/uniot-lisp.git#0.4.0
```

In any other build, compile `libminilisp.c` along with your own sources and include `libminilisp.h`.

## A minimal host

This complete program creates an interpreter, evaluates an expression, and prints the result:

```c
#include <stdio.h>
#include "libminilisp.h"

static void print_out(const char *msg, int size) { printf("%.*s\n", size, msg); }
static void print_log(const char *msg, int size) { printf("log: %.*s\n", size, msg); }
static void print_err(const char *msg, int size) { fprintf(stderr, "error: %.*s\n", size, msg); }

int main(void) {
    void *root = NULL;
    DEFINE1(env);

    lisp_set_printers(print_out, print_log, print_err);

    lisp_create(24576, 8192);   // a 24 KB heap, 8 KB of stack for evaluation
    if (!lisp_is_created()) {
        return 1;
    }

    *env = make_env(root, &Nil, &Nil);
    define_constants(root, env);
    define_primitives(root, env);

    lisp_eval(root, env, "(+ 1 2)");   // prints 3

    lisp_destroy();
    return 0;
}
```

Step by step:

1. **`root` and `DEFINE1(env)`** set up the garbage collector's view of your variables — see [Keeping objects alive](#keeping-objects-alive). The environment holds every definition, so it must be one of them.
2. **`lisp_set_printers`** installs the functions that receive output — see [Output and errors](#output-and-errors).
3. **`lisp_create`** allocates the heap. Check **`lisp_is_created`** afterwards: a heap that can't be allocated, or is larger than the interpreter can address, leaves it false.
4. **`make_env`** creates the global environment, and **`define_constants`** and **`define_primitives`** fill it with the language's built-in values and functions.
5. **`lisp_eval`** reads and evaluates a string of code.
6. **`lisp_destroy`** frees the heap. Call `lisp_create` again to start over.

When the environment must outlive the function that creates it — in firmware, where it is created in `setup()` and used in every later call — declare the root frame at file scope instead, as the interpreter's own REPL does:

```c
static void *root = NULL;
static void *root_frame[3];
static Obj **env;

void setup_interpreter(void) {
    root_frame[0] = root;
    root_frame[1] = NULL;
    root_frame[2] = ROOT_END;
    root = root_frame;
    env = (Obj **)(root_frame + 1);
    // ... then lisp_create, make_env and the rest as above
}
```

## Heap and stack

```c
void lisp_create(size_t size, size_t max_eval_stack);
```

**`size`** is the heap, in bytes. Every value a script creates lives in it. The largest heap that can be asked for is `LISP_MAX_HEAP`, 65535 bytes; `lisp_create` refuses anything larger. **`lisp_mem_used()`** returns how much of the heap is in use.

**`max_eval_stack`** limits how much of the C stack a single evaluation may use, in bytes. Recursion in a script runs on the C stack, and overrunning it would crash the whole program, so the interpreter measures what it uses as it goes and stops the script with an error before that happens. Pass `0` to remove the limit.

To choose a value, start from the stack available where you call `lisp_eval`, subtract what your error handling needs, and leave a margin. The global **`eval_stack_max`** records the most stack any evaluation has used since `lisp_create`; run your heaviest scripts and read it.

For reference, Uniot Core gives scripts a 24 KB heap and 3 KB of evaluation stack on an ESP32, and 12 KB and 1.25 KB on an ESP8266.

## Evaluating code

| Function                                                  | What it does                                                                                                      |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `bool lisp_eval(void *root, Obj **env, const char *code)` | Reads and evaluates every expression in `code`, in order. Returns `false` if one of them failed, and stops there. |
| `bool safe_eval(void *root, Obj **env, Obj **expr)`       | Evaluates one expression that has already been read. Returns `false` if it failed.                                |
| `Obj *eval(void *root, Obj **env, Obj **obj)`             | Evaluates one expression, raising errors to the caller — for use inside [primitives](#adding-primitives).         |
| `Obj *eval_list(void *root, Obj **env, Obj **list)`       | Evaluates each element of a list and returns a list of the results.                                               |

The value of each expression is passed to the output printer.

## Output and errors

```c
typedef void (*print_def)(const char *msg, int size);
void lisp_set_printers(print_def out, print_def log, print_def err);
```

The interpreter never writes anywhere itself. It hands text to three functions you provide:

| Printer | Receives                                           |
| ------- | -------------------------------------------------- |
| `out`   | The value of each expression `lisp_eval` evaluates |
| `log`   | What the script prints with `print`                |
| `err`   | The message of an error that stopped the script    |

The text is not null-terminated; use `size`. Two rules apply to every printer, and neither is checked:

* **Copy the text before returning.** It lives in a buffer the interpreter reuses.
* **Don't evaluate Lisp from inside a printer**, directly or indirectly. The error printer in particular runs partway through the interpreter recovering from the error.

When `lisp_eval` returns `false`, **`lisp_error_idx()`** and **`lisp_error_end()`** give the span of the expression that failed, as offsets into the code you passed — enough to highlight it in an editor:

```c
const char *code = "(+ 1 2)\n(foo 3)\n(+ 4 5)";
if (!lisp_eval(root, env, code)) {
    int start = lisp_error_idx();   // 8
    int end = lisp_error_end();     // 15: the span is "(foo 3)"
}
```

Here the first expression prints `3`, the second fails with `Undefined symbol: foo`, and the third is never evaluated.

## Adding primitives

A primitive is a function written in C that a script can call by name. This is how a host gives scripts access to its hardware or services.

```c
typedef struct Obj *Primitive(void *root, struct Obj **env, struct Obj **args);
```

A primitive receives its arguments **unevaluated**, as a list. It decides whether and how to evaluate them — usually all of them, with `eval_list`. It returns a value, and reports a problem by calling `error()`, which stops the script and never returns.

This one adds two integers:

```c
static Obj *prim_add_two(void *root, Obj **env, Obj **args) {
    if (length(*args) != 2)
        error("add-two takes two arguments");

    Obj *values = eval_list(root, env, args);
    Obj *a = values->car;
    Obj *b = values->cdr->car;
    if (a->type != TINT || b->type != TINT)
        error("add-two takes only numbers");

    int32_t sum;
    if (__builtin_add_overflow(a->value, b->value, &sum))
        error("Integer overflow in add-two");
    return make_int(root, sum);
}
```

Register it after `define_primitives`, under the name scripts will use:

```c
add_primitive(root, env, "add-two", prim_add_two);
```

```lisp
(add-two 3 4)     ; -> 7
(add-two 3)       ; -> error: add-two takes two arguments
(add-two 'a 4)    ; -> error: add-two takes only numbers
```

The pieces a primitive works with:

|                                                  |                                                                                                 |
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| `int length(Obj *list)`                          | The number of elements, or `-1` if it isn't a proper list                                       |
| `Obj *make_int(void *root, int32_t value)`       | A new integer                                                                                   |
| `Obj *make_symbol(void *root, const char *name)` | A new symbol                                                                                    |
| `True`, `Nil`                                    | The values `#t` and `()`, to return as truth values                                             |
| `obj->type`                                      | One of `TINT`, `TCELL`, `TSYMBOL`, `TPRIMITIVE`, `TFUNCTION`, `TMACRO`, `TENV`, `TTRUE`, `TNIL` |
| `obj->value`                                     | The number, when `type` is `TINT`                                                               |
| `obj->car`, `obj->cdr`                           | The two halves, when `type` is `TCELL`                                                          |
| `obj->name`                                      | The name, when `type` is `TSYMBOL`                                                              |
| `error(fmt, ...)`                                | Stops the script with a `printf`-style message                                                  |

Integer arithmetic in the language raises on overflow, and a primitive that does its own arithmetic should do the same, as `add-two` does.

### Keeping objects alive

The garbage collector reclaims every object it can't find a reference to, and it can run whenever anything is allocated — inside `make_int`, `eval_list`, or any other call that creates a value. A pointer held only in an ordinary C variable is invisible to it, so an object you still need can be reclaimed while you hold it.

`add-two` is safe without extra care because it reads both numbers out of the list before its one allocation, `make_int`. A primitive that has to keep an object across an allocation must declare it with one of the **`DEFINE1`** to **`DEFINE7`** macros, which make it visible to the collector.

This one evaluates its two arguments one at a time and returns the first. It holds the first result while it evaluates the second, which can allocate, so the first must be declared:

```c
static Obj *prim_first_of(void *root, Obj **env, Obj **args) {
    if (length(*args) != 2)
        error("first-of takes two arguments");

    DEFINE1(first);
    *first = eval(root, env, &(*args)->car);
    eval(root, env, &(*args)->cdr->car);   // may collect; *first survives because it is declared
    return *first;
}
```

```lisp
(first-of (list 1 2) (list 3 4))   ; -> (1 2)
```

The macros declare `Obj **` variables, so use them through `*`. Declare them before any loop, never inside one.

## Adding constants

A constant is a name with a fixed value. Scripts can read it but not change it.

```c
add_constant_int(root, env, "LED_COUNT", 10);

DEFINE1(greeting);
*greeting = make_symbol(root, "hello");
add_constant(root, env, "GREETING", greeting);
```

```lisp
LED_COUNT            ; -> 10
(setq LED_COUNT 5)   ; -> error: Cannot change constant LED_COUNT
```

**`get_variable(root, env, name)`** looks a name up from C — to read a value a script has set, for example. It raises if the name isn't defined.

## Declaring functions the host implements elsewhere

**`handle_pruner`** is for a host that implements some functions somewhere other than C — in JavaScript in a browser, say — and wants a script to declare them in Lisp. Call it from a primitive that acts as a declaration form, passing the name of a single dispatch primitive:

```c
static Obj *prim_defjs(void *root, Obj **env, Obj **args) {
    return handle_pruner(root, env, args, "js_call", true);
}
```

```lisp
(defjs vibro (times))
```

This defines `vibro` as a function of one argument whose body calls `(js_call 'vibro times)`. With `include_name` false, the name is left out and the body is `(js_call times)`. Declaring a name that is already defined is an error.

## Letting other work run

```c
typedef void (*yield_def)();
void lisp_set_cycle_yield(yield_def yield);
```

A script's loop can run for a long time without returning control. Set a yield function and the interpreter calls it on every `while` iteration and every tail call, so the host can feed a watchdog timer or let other tasks run.

A script that loops forever is stopped with an error rather than hanging the host — see [Build configuration](#build-configuration).

## Garbage collection

The collector is chosen when the library is compiled:

| `MINILISP_GC`                      |                                                                                                                                                |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `MINILISP_GC_MARK_SWEEP` (default) | Objects never move, and collecting needs no memory beyond the heap. Suits small devices, where a second heap-sized block may not be available. |
| `MINILISP_GC_COPYING`              | Allocation is faster, but every collection needs a second block the size of the heap, and objects move.                                        |

Mark-sweep is the default everywhere on purpose: the two collectors run out of memory under different conditions, so the same script could fit on one and fail on the other.

**`gc(root)`** runs a collection immediately. Setting **`always_gc`** to `true` runs one before every allocation, which makes a primitive that forgets to [keep an object alive](#keeping-objects-alive) fail at once instead of occasionally.

## Build configuration

These are compile-time definitions; set them with `-D` to override the default.

| Definition                     | Default                  | Effect                                                                                                                                                                             |
| ------------------------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MINILISP_MAX_LOOP_ITERATIONS` | 20000                    | `while` iterations allowed in one top-level expression, counted across all its loops, before it is stopped as endless                                                              |
| `MINILISP_MAX_TAIL_CALLS`      | same                     | Tail calls allowed in one chain before it is stopped as endless                                                                                                                    |
| `MINILISP_GC`                  | `MINILISP_GC_MARK_SWEEP` | The collector, as above                                                                                                                                                            |
| `MINILISP_GC_MARK_STACK`       | 128                      | The mark-sweep collector's work stack, in entries. Running out is not an error, only slower.                                                                                       |
| `MINILISP_GC_DEBUG`            | 0                        | `1` adds `debug_gc` (log each collection), `verify_gc` (check the heap after each) and `fail_gc_alloc` (simulate running out of memory) — for debugging a collector or a primitive |
| `MINILISP_GC_STATS`            | 0                        | `1` adds the `lisp_stats` counters and `lisp_stats_reset()` for measuring collector performance, timed by a clock you assign to `lisp_stats_clock`                                 |

These are fixed in the header:

| Limit                  | Value                                                      |
| ---------------------- | ---------------------------------------------------------- |
| `LISP_MAX_HEAP`        | 65535 — the largest heap                                   |
| `SYMBOL_MAX_LEN`       | 64 — the longest symbol name                               |
| `LISP_MESSAGE_MAX_LEN` | 128 — the longest error or log message                     |
| `LISP_PRINT_MAX_LEN`   | 256 — the longest printed value; output is truncated to it |

`LISP_VERSION` holds the library version as a single integer: `major * 10000 + minor * 100 + patch`, so 0.4.0 is `400`.


