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: Support for CBOR Object Signing and Encryption (COSE) standard
Script Verification: Cryptographic signature verification for remote scripts
Secure Storage: Protected credential management for WiFi and user authentication
Sandboxed Execution: Isolated Lisp interpreter environment for safe script execution
Storage & Persistence
CBOR-based Storage: Efficient binary serialization for configuration and data
Crash Recovery: Automatic crash detection and reporting
LittleFS Support: Modern filesystem for reliable flash storage
WiFi Credentials Storage: Secure 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, ESP-12F, NodeMCU, Wemos D1 Mini, and compatible boards
ESP32: ESP32 DevKit, ESP32-C3, ESP32-S2, ESP32-S3, and compatible boards
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:
pip install platformioCreate a new project:
pio project init --board esp32doit-devkit-v1Configure
platformio.ini:[env:esp32] platform = espressif32 ; Use espressif8266 for ESP8266 boards framework = arduino board = esp32doit-devkit-v1 monitor_speed = 115200 lib_deps = uniot-io/uniot-core@^0.8.1 build_unflags = -std=gnu++11 build_flags = -std=gnu++17 -D UNIOT_CREATOR_ID=\"YOUR_CREATOR_ID\" -D UNIOT_LOG_ENABLED=1 -D UNIOT_USE_LITTLEFS=1 -D UNIOT_LOG_LEVEL=UNIOT_LOG_LEVEL_INFO -D UNIOT_LISP_HEAP=10000 -D MQTT_MAX_PACKET_SIZE=2048Platform 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 (default: "UNIOT")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:
pio run --target upload
Quick Start
Here's a minimal example to get you started with Uniot Core:
#include <Uniot.h>
void setup() {
Serial.begin(115200);
// Configure WiFi credentials
Uniot.configWiFiCredentials("YourSSID", "YourPassword");
// Configure WiFi status LED
Uniot.configWiFiStatusLed(LED_BUILTIN);
// Configure reset button
Uniot.configWiFiResetButton(0, LOW);
// Create a periodic task
Uniot.setInterval([]() {
Serial.println("Hello from Uniot!");
}, 1000);
// Initialize and start the platform
Uniot.begin();
}
void loop() {
// Execute scheduled tasks and process events
Uniot.loop();
}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
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:
// Create a one-shot timer (like JavaScript's setTimeout)
Uniot.setTimeout([]() {
Serial.println("This runs once after 5 seconds");
}, 5000);
// Create a repeating timer (like JavaScript's setInterval)
auto timerId = Uniot.setInterval([]() {
Serial.println("This repeats every 2 seconds");
}, 2000);
// Cancel a timer
Uniot.cancelTimer(timerId);
// Execute immediately on next cycle
Uniot.setImmediate([]() {
Serial.println("This runs immediately");
});
// Create a custom task with more control
auto task = Uniot.createTask("my_task", [](SchedulerTask& self, short remaining) {
// Task implementation
Serial.println("Custom task executed");
});
task->attach(1000); // Attach with 1 second periodEvent System
The event bus enables decoupled communication between components:
// Add an event listener
auto listenerId = Uniot.addSystemListener(
[](unsigned int topic, int message) {
Serial.printf("Event received: topic=%u, msg=%d\n", topic, message);
},
FOURCC(test), // First topic
FOURCC(data) // Additional topics
);
// Emit an event
Uniot.emitSystemEvent(FOURCC(test), 42);
// Remove listener when done
Uniot.removeSystemListener(listenerId);
// WiFi status LED listener (convenience method)
Uniot.addWifiStatusLedListener([](bool state) {
digitalWrite(MY_LED, state ? HIGH : LOW);
});WiFi Management
Uniot Core provides two ways to connect your device to WiFi:
1. Hardcoded Credentials (programmatic setup):
// Configure WiFi credentials
Uniot.configWiFiCredentials("MyNetwork", "password123");
// Configure user identification (Uniot account ID)
Uniot.configUser("your_uniot_account_id");
// Status LED with custom pin and active level
Uniot.configWiFiStatusLed(LED_BUILTIN, HIGH);
// Reset button configuration
Uniot.configWiFiResetButton(0, LOW, true); // Pin, active level, register with Lisp
// Automatic reset on repeated reboots (recovery mechanism)
Uniot.configWiFiResetOnReboot(5, 10000); // 5 reboots within 10 seconds triggers reset2. Captive Portal (user-friendly setup):
If no valid WiFi credentials are found, the device automatically enters Access Point mode with a captive portal where users can:
Select or enter WiFi network credentials
Enter their Uniot account ID
LED Status Indicators:
5 blinks/sec (200ms period): Error/alarm state
2 blinks/sec (500ms period): Connecting/busy
1 blink/sec (1000ms period): Waiting/Access Point mode
Brief flash: Connected/idle
Resetting WiFi Configuration:
To clear current WiFi settings and switch to Access Point mode:
Quick press the reset button 5-8 times
Then hold for 3-5 seconds
The device will clear WiFi configuration and start the captive portal
Lisp Scripting
Uniot Core includes an embedded Lisp interpreter for dynamic runtime scripting. Scripts can be sent via MQTT and executed on the device without reflashing firmware.
Register Hardware for Lisp Access:
// Register GPIO pins for Lisp access
Uniot.registerLispDigitalOutput(LED_BUILTIN, 12, 13);
Uniot.registerLispDigitalInput(0, 4);
Uniot.registerLispAnalogInput(A0);
Uniot.registerLispAnalogOutput(12, 13, 14);
// Register a button object
auto button = new uniot::Button(0, LOW);
Uniot.registerLispButton(button);Event Communication:
// Publish events from C++ to Lisp scripts
Uniot.publishLispEvent("sensor_reading", 42);
// Intercept events from Lisp scripts
Uniot.setLispEventInterceptor([](const uniot::LispEvent& event) {
Serial.printf("Lisp event: %s = %d from %s\n",
event.eventID.c_str(),
event.value,
event.sender.id.c_str());
return true; // Return false to reject the event
});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.
Object my_add(Root root, VarObject env, VarObject list) {
// Describe the primitive: name, return type, number of args, arg types
auto expeditor = PrimitiveExpeditor::describe("my_add", Lisp::Int, 2, Lisp::Int, Lisp::Int)
.init(root, env, list);
// Validate arguments match the description
expeditor.assertDescribedArgs();
// Get arguments
int arg1 = expeditor.getArgInt(0);
int arg2 = expeditor.getArgInt(1);
// Perform your custom logic
int result = arg1 + arg2;
// Return result
return expeditor.makeInt(result);
}
// Register the primitive
Uniot.addLispPrimitive(my_add);Argument Types:
Lisp::Int- Integer valuesLisp::Bool- Boolean valuesLisp::Symbol- Symbols
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 use the RegisterManager to link objects:
// Register a custom object that can be accessed by primitives
Uniot.registerLispObject("my-object", myObjectPointer, FOURCC(myid));Storage Management
Uniot Core uses CBOR (Concise Binary Object Representation) for efficient data serialization and persistent storage:
// WiFi credentials and user ID are automatically stored
Uniot.configWiFiCredentials("SSID", "Password");
Uniot.configUser("your_account_id");
// Custom CBOR storage for your application data
uniot::CBORStorage storage("mydata.cbor");
// Store data
storage.object()
.put("temperature", 25.5)
.put("humidity", 60)
.put("location", "Living Room")
.put("timestamp", uniot::Date::now());
storage.store();
// Restore data
if (storage.restore()) {
double temp = storage.object().getDouble("temperature");
int humidity = storage.object().getInt("humidity");
String location = storage.object().getString("location");
}Time Management
NTP synchronization and time persistence:
// Enable periodic date saving (survives reboots)
Uniot.enablePeriodicDateSave(300); // Save every 5 minutes
// In your code, access time functions
Serial.println(uniot::Date::getFormattedTime());
Serial.println(uniot::Date::getFormattedDate());
uint64_t timestamp = uniot::Date::now(); // Unix timestamp in secondsAPI Reference
UniotCore Class
The Uniot global instance provides the main API:
Configuration Methods
configWiFiCredentials(ssid, password)
Set WiFi network credentials
configWiFiStatusLed(pin, activeLevel)
Configure status LED
configWiFiResetButton(pin, activeLevel, registerLisp)
Configure reset button
configWiFiResetOnReboot(maxReboots, windowMs)
Auto-reset on repeated reboots
configUser(userId)
Set user identifier
enablePeriodicDateSave(periodSeconds)
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 EventBus instance
EventBus&
getScheduler()
Access Scheduler instance
Scheduler&
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 sensor monitoring
Hardware: WittyCloud ESP8266 development board
Key Code:
// Register all I/O for Lisp access
Uniot.registerLispDigitalOutput(PIN_RED, PIN_GREEN, PIN_BLUE);
Uniot.registerLispAnalogOutput(PIN_RED, PIN_GREEN, PIN_BLUE);
Uniot.registerLispAnalogInput(PIN_LDR);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:
// Custom primitive for Lisp-based control
Object lamp_update(Root root, VarObject env, VarObject list) {
auto expeditor = PrimitiveExpeditor::describe("lamp_update", Lisp::Bool, 5,
Lisp::Int, Lisp::Int, Lisp::Int, Lisp::Int, Lisp::Int)
.init(root, env, list);
// ... control lamp via Lisp scripts
}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:
build_flags =
-std=gnu++17
-D UNIOT_CREATOR_ID=\"UNIOT\" # Device creator identifier
-D UNIOT_LOG_ENABLED=1 # Enable logging
-D UNIOT_USE_LITTLEFS=1 # Use LittleFS filesystem
-D UNIOT_LOG_LEVEL=UNIOT_LOG_LEVEL_DEBUG # Log level
-D UNIOT_LISP_HEAP=10000 # Lisp interpreter heap size
-D MQTT_MAX_PACKET_SIZE=2048 # MQTT packet sizeLog Levels
UNIOT_LOG_LEVEL_NONE // No logging
UNIOT_LOG_LEVEL_ERROR // Errors only
UNIOT_LOG_LEVEL_WARN // Warnings and errors
UNIOT_LOG_LEVEL_INFO // Info, warnings, and errors
UNIOT_LOG_LEVEL_DEBUG // All messages including debug
UNIOT_LOG_LEVEL_TRACE // All messages including traceLogging
Uniot Core includes a comprehensive logging system:
UNIOT_LOG_ERROR("Error occurred: %d", errorCode);
UNIOT_LOG_WARN("Warning: %s", message);
UNIOT_LOG_INFO("Device connected: %s", deviceId);
UNIOT_LOG_DEBUG("Debug value: %d", value);
UNIOT_LOG_TRACE("Function called: %s", __func__);
// Conditional logging
UNIOT_LOG_ERROR_IF(condition, "Error if condition is true");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
Documentation
Complete API documentation is generated with Doxygen and available at:
Online: https://core.docs.uniot.io
Local: Generate with
./scripts/generate_docs.sh
Best Practices
Memory Management
// Prefer stack allocation for small objects
uniot::Bytes data(64);
// Use smart pointers for dynamic allocation
auto task = uniot::MakeShared<MyTask>();
auto buffer = uniot::MakeUnique<uint8_t[]>(1024);Task Scheduling
// Keep task execution time short
Uniot.setInterval([]() {
// Quick operation
sensor.read();
}, 100);
// For longer operations, use state machine pattern
Uniot.createTask("long_task", [](SchedulerTask& self, short remaining) {
static int state = 0;
switch(state) {
case 0: /* Do step 1 */ state++; break;
case 1: /* Do step 2 */ state++; break;
case 2: /* Done */ state = 0; self.detach(); break;
}
});Event Handling
// Remove listeners when no longer needed
void cleanup() {
Uniot.removeSystemListener(myListenerId);
}
// Use specific topics instead of listening to everything
Uniot.addSystemListener(handler,
uniot::events::network::CONNECTED, // Specific events only
uniot::events::network::DISCONNECTED
);Error Handling
// Always check return values
if (!Uniot.cancelTimer(timerId)) {
UNIOT_LOG_WARN("Timer %u not found", timerId);
}
// Validate configuration
auto success = Uniot.getAppKit().setWiFiCredentials(ssid, password);
UNIOT_LOG_ERROR_IF(!success, "Invalid WiFi credentials");Troubleshooting
WiFi Not Connecting
Check credentials: Ensure SSID and password are correct
Signal strength: Move closer to the router
Reset configuration: Press reset button multiple times rapidly, then hold
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:
UNIOT_LOG_INFO("Free heap: %u", ESP.getFreeHeap());
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