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 JavaScriptComprehensive 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
Install PlatformIO:
Create a new project:
Configure
platformio.ini:Platform and Framework: Ensure that the platform and framework settings match your microcontroller (e.g.,
espressif8266for ESP8266 orespressif32for ESP32).Build Flags:
-std=gnu++17: Required C++17 standardUNIOT_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 bytesMQTT_MAX_PACKET_SIZE: Maximum MQTT packet size in bytes
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
Connects to WiFi with automatic reconnection
Provides visual feedback via LED (blinking patterns for different states)
Allows configuration reset via button press
Makes GPIO 12 scriptable — remote UniotLisp scripts can drive it with
(dwrite 0 ...)Executes periodic task printing a message every second
Manages everything automatically through the event loop
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:
Quick-press the reset button 4 or more times
Then hold it for ~3 seconds
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 valuesLisp::Bool- Boolean values (#t/())Lisp::BoolInt- Accepts either a boolean or an integerLisp::Symbol- SymbolsLisp::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 integerexpeditor.makeBool(value)- Return booleanexpeditor.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:
Time Management
NTP synchronization and time persistence:
API Reference
UniotCore Class
The Uniot global instance provides the main API:
Configuration Methods
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
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
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
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
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_updatefor remote control via MQTTWiFi 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:
uniot-cbor - CBOR serialization
uniot-lisp - Lisp interpreter
uniot-pubsubclient - MQTT client
uniot-crypto - Cryptography support
uniot-esp-async-web-server - Async web server
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
API Reference (Doxygen): https://core.docs.uniot.io — generate locally with
./scripts/generate_docs.shUniotLisp Language: Language Description
Scripting Guide: Scripting
Primitives: Primitives
Best Practices
Memory Management
Task Scheduling
Event Handling
Error Handling
Troubleshooting
WiFi Not Connecting
Check credentials: Ensure SSID and password are correct
Signal strength: Move closer to the router
Reset configuration: Quick-press the reset button 4+ times, then hold it ~3 seconds (all within ~5 seconds)
Check logs: Enable debug logging to see connection attempts
Memory Issues
Reduce Lisp heap size:
UNIOT_LISP_HEAP=5000Limit timer count: Remove unused timers with
cancelTimer()Monitor free heap:
Upload Failures
Hold boot button: Some boards require holding BOOT during upload
Check USB driver: Ensure CH340/CP2102 driver is installed
Try different baud rate: Set
upload_speed = 115200in platformio.ini
Last updated