For the complete documentation index, see llms.txt. This page is also available as Markdown.

Uniot Core

Uniot Core is a lightweight, open-source framework for building IoT devices on ESP8266 and ESP32 microcontrollers. It handles the heavy lifting of task scheduling, network management, and device communication, letting you focus on what makes your device unique. With an embedded Lisp interpreter for runtime scripting and a developer-friendly API, Uniot Core gives you both flexibility and control.

From home automation to custom devices and prototypes, Uniot Core simplifies the development process while providing the power and reliability needed for production deployments.

Key Features

Core Capabilities

  • Non-blocking Task Scheduler: Execute periodic and one-shot tasks efficiently without blocking

  • Event-Driven Architecture: Decoupled communication between components via publish-subscribe pattern

  • Embedded Lisp Interpreter: Dynamic scripting and runtime reconfiguration capabilities

  • Automatic WiFi Management: Network connectivity with automatic reconnection and captive portal

  • MQTT Integration: Full-featured MQTT client for cloud connectivity

  • Hardware Abstraction: Unified GPIO management and peripheral control

Security

  • COSE Message Signing: CBOR Object Signing and Encryption (COSE) with Ed25519, used to authenticate the device to the MQTT broker

  • Script Verification (planned): Cryptographic signature verification for remotely delivered scripts

  • Credential Storage: WiFi and user credentials persisted on-device

  • Fault-Isolated Scripting: Lisp scripts run on a dedicated fixed-size heap; script errors are contained and the interpreter is torn down without rebooting the device

Storage & Persistence

  • CBOR-based Storage: Efficient binary serialization for configuration and data

  • Crash Dump Capture: Automatic crash dump saved to flash for post-mortem debugging (ESP8266)

  • LittleFS Support: Modern filesystem for reliable flash storage

  • WiFi Credentials Storage: Persistent credential management

Time Management

  • NTP Synchronization: Automatic time synchronization

  • Persistent Date/Time: Maintain time across reboots

  • Event-based Time Tracking: Time-aware event processing

Developer Experience

  • Web-Familiar API: Timer functions (setTimeout, setInterval, setImmediate) inspired by JavaScript

  • Comprehensive Logging: Multi-level logging system for debugging

  • Doxygen Documentation: Complete API documentation with examples

  • PlatformIO Integration: Modern build system with dependency management

Compatibility

Currently, Uniot Core is optimized for ESP8266 and ESP32 microcontrollers, two of the most popular platforms for IoT development.

Supported Boards

  • ESP8266: ESP-12E (tested), ESP-12F, NodeMCU, Wemos D1 Mini, and other ESP8266-based boards

  • ESP32: ESP32 DevKit (tested), ESP32-C3 (tested), and other ESP32-family boards — ESP32-S2/S3 should work but are not routinely tested

Build environments for the tested boards are maintained in the repository's platformio.ini (ESP12E, ESP32, ESP32C3).

Why Arduino & C++?

The decision to base Uniot Core on the Arduino framework and implement it in C++ is rooted in a commitment to:

  • Accessibility: Arduino's user-friendly nature makes IoT development approachable

  • Performance: C++17 provides efficiency and modern language features

  • Ecosystem: Vast library ecosystem and thriving developer community

  • Portability: Easy adaptation to new hardware platforms

Installation

Prerequisites

  • PlatformIO installed

  • ESP8266 or ESP32 development board

  • USB cable for programming

Using PlatformIO

  1. Install PlatformIO:

  2. Create a new project:

  3. Configure platformio.ini:

    Platform and Framework: Ensure that the platform and framework settings match your microcontroller (e.g., espressif8266 for ESP8266 or espressif32 for ESP32).

    Build Flags:

    • -std=gnu++17: Required C++17 standard

    • UNIOT_CREATOR_ID: Device creator identifier (required — the build fails without it)

    • UNIOT_LOG_ENABLED: Enable/disable logging (1 or 0)

    • UNIOT_USE_LITTLEFS: Use LittleFS filesystem (1 or 0)

    • UNIOT_LOG_LEVEL: Logging verbosity (see Configuration section)

    • UNIOT_LISP_HEAP: Heap size for Lisp interpreter in bytes

    • MQTT_MAX_PACKET_SIZE: Maximum MQTT packet size in bytes

  4. Build and upload:

Multi-Environment Configuration

To target several boards from one project, define an environment per board and share common settings in [env]:

Quick Start

Here's a minimal example to get you started with Uniot Core:

What This Does

  1. Connects to WiFi with automatic reconnection

  2. Provides visual feedback via LED (blinking patterns for different states)

  3. Allows configuration reset via button press

  4. Makes GPIO 12 scriptable — remote UniotLisp scripts can drive it with (dwrite 0 ...)

  5. Executes periodic task printing a message every second

  6. Manages everything automatically through the event loop

Call at least one configWiFi* method before Uniot.begin() — that is what creates the network controller. Without it there is no status LED, reset button, or WiFi status events.

Core Components

Task Scheduler

The task scheduler provides non-blocking execution of periodic and one-shot tasks:

Event System

The event bus enables decoupled communication between components:

WiFi Management

Uniot Core provides two ways to supply WiFi credentials to the device:

1. Hardcoded Credentials (programmatic setup):

Set the network credentials and Uniot account ID directly in code. Useful for development boards or fleets where credentials are known at build time.

2. Captive Portal (user-friendly setup):

If no valid WiFi credentials are stored, the device automatically enters Access Point mode (network name UNIOT-XXXXXX, where the suffix is the device's chip ID in uppercase hex) with a captive portal where the end user can:

  • Select or enter WiFi network credentials

  • Enter their Uniot account ID

Credentials from both methods land in the same persistent storage. Be aware, however, that configWiFiCredentials() stores its values every time it runs — if the call stays in setup(), it overwrites whatever the user entered through the captive portal on every reboot. Remove (or guard) the call once the device is meant to be configured by its end user.

Optional WiFi Interaction Helpers

The methods below configure how the user interacts with the WiFi subsystem (visual feedback, manual reset, recovery). They apply regardless of which credential method is used and can be combined freely.

LED Status Indicators:

The LED level toggles on each period, so a full on/off blink cycle takes twice the period:

  • Fast blink (200ms toggle, ≈2.5 blinks/sec): Error/alarm state

  • Medium blink (500ms toggle, ≈1 blink/sec): Connecting/busy

  • Slow blink (1000ms toggle, ≈0.5 blinks/sec): Waiting/Access Point mode

  • Off: Connected/idle

Resetting WiFi Configuration:

To clear current WiFi settings and switch to Access Point mode:

  1. Quick-press the reset button 4 or more times

  2. Then hold it for ~3 seconds

  3. The device will clear WiFi configuration and start the captive portal

The whole sequence must be completed within ~5 seconds of the first press — after that the click counter resets. A ~3-second hold with fewer than 4 preceding clicks triggers a manual reconnect instead.

UniotLisp Scripting

Uniot Core includes an embedded UniotLisp interpreter for dynamic runtime scripting. Scripts can be sent via MQTT and executed on the device without reflashing firmware.

Register Hardware for UniotLisp Access:

Each registerLisp* call exposes the listed GPIO pins to UniotLisp through a matching primitive: dwrite (digital write), dread (digital read), awrite (analog/PWM write), aread (analog read), and bclicked for buttons.

Pins are not addressed by their raw GPIO number from Lisp. Instead, the register subsystem assigns each pin a 0-based logical index in the order it was registered for that primitive. Scripts then use that index — for example, (dwrite 0 #t) toggles whichever pin was registered first as a digital output. This indirection lets the same script run on different boards without knowing the underlying pin map.

Register all pins for a primitive in a single call: calling the same registerLisp* method again replaces the previous pin set rather than appending to it.

What Scripts Look Like:

Once hardware is registered, scripts delivered over MQTT can drive it. A script is built around tasks — (task times period 'expression) evaluates expression times times (0 = forever) every period milliseconds. For example, with a digital output and a button registered, this script turns the output on when the button is clicked, checking every 100 ms:

See the UniotLisp language description for the full syntax and built-in functions.

Event Communication:

Creating Custom Primitives:

Custom primitives extend the Lisp interpreter with your own functions. A primitive is a C++ function that can be called from Lisp scripts.

Argument Types:

  • Lisp::Int - Integer values

  • Lisp::Bool - Boolean values (#t / ())

  • Lisp::BoolInt - Accepts either a boolean or an integer

  • Lisp::Symbol - Symbols

  • Lisp::Cell - An unevaluated list (e.g., a quoted expression)

  • Lisp::Any - Any type

Arguments are read with getArgInt(i), getArgBool(i), or getArgSymbol(i).

Return Types:

  • expeditor.makeInt(value) - Return integer

  • expeditor.makeBool(value) - Return boolean

  • expeditor.makeSymbol(value) - Return symbol

For more complex primitives that interact with hardware or access device state, you can link a C++ object into the register. The object must inherit from uniot::ObjectRegisterRecord, and the name must match the name in the primitive's describe() call — the primitive then retrieves the object through expeditor.getAssignedRegister():

Storage Management

Uniot Core uses CBOR (Concise Binary Object Representation) for efficient data serialization and persistent storage:

CBORObject stores integers (up to 64-bit), strings, and byte arrays — there is no floating-point support. Scale fractional values to integers (e.g., store 25.5 °C as 255 tenths of a degree).

Time Management

NTP synchronization and time persistence:

API Reference

UniotCore Class

The Uniot global instance provides the main API:

Configuration Methods

Method
Description

configWiFiCredentials(ssid, password = "")

Set WiFi network credentials

configWiFiStatusLed(pin, activeLevel = HIGH)

Configure status LED

configWiFiResetButton(pin, activeLevel = LOW, registerLisp = true)

Configure reset button

configWiFiResetOnReboot(maxReboots, windowMs = 10000)

Auto-reset on repeated reboots

configUser(userId)

Set user identifier

enablePeriodicDateSave(periodSeconds = 300)

Enable time persistence

Timer Methods

Method
Description
Returns

setTimeout(callback, ms)

Execute once after delay

TimerId

setInterval(callback, ms, times)

Execute repeatedly

TimerId

setImmediate(callback)

Execute on next cycle

TimerId

cancelTimer(id)

Cancel a timer

bool

isTimerActive(id)

Check if timer is active

bool

getActiveTimersCount()

Get active timer count

int

Event Methods

Method
Description
Returns

addSystemListener(callback, topics...)

Add event listener

ListenerId

removeSystemListener(id)

Remove listener by ID

bool

removeSystemListeners(topics...)

Remove all listeners for topics

size_t

isSystemListenerActive(id)

Check if listener is active

bool

getActiveListenersCount()

Get active listener count

int

emitSystemEvent(topic, message)

Emit an event

void

addWifiStatusLedListener(callback)

Add WiFi LED listener

ListenerId

Lisp Integration Methods

Method
Description

addLispPrimitive(primitive)

Add custom Lisp primitive

setLispEventInterceptor(interceptor)

Set Lisp event interceptor

publishLispEvent(eventID, value)

Publish event to Lisp

registerLispDigitalOutput(pins...)

Register GPIO outputs

registerLispDigitalInput(pins...)

Register GPIO inputs

registerLispAnalogOutput(pins...)

Register PWM outputs

registerLispAnalogInput(pins...)

Register analog inputs

registerLispButton(button, id = ...)

Register button object

registerLispObject(name, ptr, id)

Register generic object

System Methods

Method
Description
Returns

begin(eventBusPeriod)

Initialize and start platform

void

loop()

Process tasks and events

void

createTask(name, callback)

Create custom task

TaskPtr

getAppKit()

Access AppKit instance

AppKit&

getEventBus()

Access event bus instance

CoreEventBus&

getScheduler()

Access scheduler instance

TaskScheduler&

Examples

The repository includes several working examples demonstrating different features of Uniot Core:

WittyCloud

RGB LED controller with light sensor for WittyCloud development board.

Location: examples/WittyCloud/

Features:

  • RGB LED control (digital and PWM)

  • LDR (Light Dependent Resistor) reading

  • Button input handling

  • All GPIO registered for Lisp scripting

  • Periodic status logging (free heap, current time)

Hardware: WittyCloud ESP8266 development board

Key Code:

My9231Lamp

Smart RGB+WW+CW lamp controller with custom Lisp primitives.

Location: examples/My9231Lamp/

Features:

  • MY9231 LED driver control (5-channel: RGB + Warm White + Cool White)

  • Custom Lisp primitive lamp_update for remote control via MQTT

  • WiFi status indication using lamp colors

  • Compatible with Sonoff B1 and similar smart bulbs

Hardware: ESP8266-based smart bulb (Sonoff B1)

Key Code:

S20Socket

Smart socket/relay controller with Lisp scriptable GPIO.

Location: examples/S20Socket/

Features:

  • Relay control via GPIO

  • Status LED indication

  • GPIO pins registered for Lisp access (scriptable on/off control)

  • WiFi configuration with reset button

Hardware: Sonoff S20 Smart Socket or compatible ESP8266 relay board

Use Case: Control appliances remotely, schedule operations, integrate with home automation

Configuration

Build Flags

Configure Uniot Core behavior through build flags in platformio.ini:

Log Levels

To disable logging entirely, set UNIOT_LOG_ENABLED=0 — there is no "none" level.

Logging

Uniot Core includes a comprehensive logging system:

Dependencies

Uniot Core automatically manages these dependencies:

Testing

The repository ships an on-device test suite built on the Unity framework, covering the CBOR layer, Lisp integration and primitives, registers, and utility types. Tests run on real hardware:

Documentation

Best Practices

Memory Management

Task Scheduling

Event Handling

Error Handling

Troubleshooting

WiFi Not Connecting

  1. Check credentials: Ensure SSID and password are correct

  2. Signal strength: Move closer to the router

  3. Reset configuration: Quick-press the reset button 4+ times, then hold it ~3 seconds (all within ~5 seconds)

  4. Check logs: Enable debug logging to see connection attempts

Memory Issues

  1. Reduce Lisp heap size: UNIOT_LISP_HEAP=5000

  2. Limit timer count: Remove unused timers with cancelTimer()

  3. Monitor free heap:

Upload Failures

  1. Hold boot button: Some boards require holding BOOT during upload

  2. Check USB driver: Ensure CH340/CP2102 driver is installed

  3. Try different baud rate: Set upload_speed = 115200 in platformio.ini

Last updated