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 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, 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

  1. Install PlatformIO:

    pip install platformio
  2. Create a new project:

    pio project init --board esp32doit-devkit-v1
  3. Configure 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=2048

    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 (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 bytes

    • MQTT_MAX_PACKET_SIZE: Maximum MQTT packet size in bytes

  4. 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

  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. Executes periodic task printing a message every second

  5. 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 period

Event 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 reset

2. 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:

  1. Quick press the reset button 5-8 times

  2. Then hold for 3-5 seconds

  3. 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 values

  • Lisp::Bool - Boolean values

  • Lisp::Symbol - Symbols

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 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 seconds

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)

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

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 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_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:

// 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 size

Log 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 trace

Logging

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:

Documentation

Complete API documentation is generated with Doxygen and available at:

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

  1. Check credentials: Ensure SSID and password are correct

  2. Signal strength: Move closer to the router

  3. Reset configuration: Press reset button multiple times rapidly, then hold

  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:

    UNIOT_LOG_INFO("Free heap: %u", ESP.getFreeHeap());

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