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:
Built-in Primitives: Pre-implemented functions for common hardware operations (GPIO, buttons, etc.)
Custom Primitives: User-defined functions for application-specific logic
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
Script Received: Device receives script via MQTT
Interpretation: Interpreter parses and executes the script
Primitive Call: Script calls a primitive function
C++ Execution: Primitive executes C++ code
Result Return: Result is converted back to Lisp data type
Script Continues: Script processes the result
Type System
Primitives use a simple type system for argument validation:
Lisp::Int
int
Integer values
Lisp::Bool
bool
Boolean values (true/false)
Lisp::BoolInt
bool
Combined Boolean/Integer value
Lisp::Symbol
String
Symbols and identifiers
User Library Block
Every script must begin with a special metadata block that describes the primitives available to the script. The UniotLisp interpreter uses this block to understand which primitives are available, enabling the interpreter to properly validate and execute primitive calls.
Structure
The user library block is enclosed by special markers:
;;; 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-libraryPurpose
Interpreter Requirement: The interpreter uses this information to know which primitives are in your script
Type Validation: Helps the interpreter validate primitive calls with correct argument types
Documentation: Serves as inline documentation for developers reading the script
When You Need to Care About This
Visual Editor Users: You don't need to do anything - the block is automatically generated and included in your scripts
Manual Script Writers: Required - you must manually add this block at the beginning of every script you write, declaring all primitives used in that script
Syntax: defjs Declarations
Each primitive is declared using the defjs syntax:
; (defjs primitive-name (param1 param2 ...)) ;-> ReturnTypeExample
If you have a custom primitive set_led_brightness that takes two integers (pin and brightness) and returns a boolean:
;;; begin-user-library
;; This block describes the library of user functions.
;; So the editor knows that your device implements it.
; (defjs set_led_brightness (pin brightness)) ;-> 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 15 255))))Notes
The block is delimited by
;;; begin-user-libraryand;;; end-user-librarymarkersEach declaration is on a single line, prefixed with
;(comment)The
;->arrow indicates the return typeMultiple primitives can be declared in the same block
The block appears at the very beginning of the script
Required for interpreter: The UniotLisp interpreter needs this block to recognize and validate primitive function calls
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
Registration: C++ code registers physical GPIO pins using
Uniot.registerLisp*()methodsMapping: Each registered pin gets a logical index (0, 1, 2, ...)
Access: Scripts use logical indices to access pins through built-in primitives
Validation: Primitives validate indices against registered pins before hardware access
Registering GPIO Pins
Before using built-in primitives, you must register GPIO pins:
void setup() {
// Register digital output pins (GPIO 12, 13, 14)
Uniot.registerLispDigitalOutput(12, 13, 14);
// Script indices: pin 12 = index 0, pin 13 = index 1, pin 14 = index 2
// Register digital input pins (GPIO 0, 4)
Uniot.registerLispDigitalInput(0, 4);
// Script indices: pin 0 = index 0, pin 4 = index 1
// Register analog output pins for PWM (GPIO 12, 13, 14)
Uniot.registerLispAnalogOutput(12, 13, 14);
// Script indices: pin 12 = index 0, pin 13 = index 1, pin 14 = index 2
// Register analog input pins (GPIO A0)
Uniot.registerLispAnalogInput(A0);
// Script indices: A0 = index 0
Uniot.begin();
}Important: Each pin type (digital output, digital input, analog output, analog input) has its own index namespace.
Quick Reference
dwrite
Write digital pin (HIGH/LOW)
registerLispDigitalOutput()
(dwrite index state)
dread
Read digital pin (HIGH/LOW)
registerLispDigitalInput()
(dread index)
awrite
Write analog pin (PWM)
registerLispAnalogOutput()
(awrite index value)
aread
Read analog pin (ADC)
registerLispAnalogInput()
(aread index)
bclicked
Check button click
registerLispButton()
(bclicked index)
Available Built-in Primitives
1. dwrite - Digital Write
dwrite - Digital WriteWrites a digital value (HIGH/LOW) to a registered output pin.
Parameters:
index(Int): Logical pin index (0-based)state(Bool): Pin state (true = HIGH, false = LOW)
C++ Registration:
Uniot.registerLispDigitalOutput(12, 13, 14);Example - Blink LED:

2. dread - Digital Read
dread - Digital ReadReads a digital value (HIGH/LOW) from a registered input pin.
Parameters:
index(Int): Logical pin index (0-based)
Returns: Boolean state (true = HIGH, false = LOW)
C++ Registration:
Uniot.registerLispDigitalInput(0, 4);Example - Read PIR sensor and turn LED:

3. awrite - Analog Write (PWM)
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)
C++ Registration:
Uniot.registerLispAnalogOutput(12, 13, 14);Example - RGB LED Control:

4. aread - Analog Read
aread - Analog ReadReads an analog value from a registered input pin.
Parameters:
index(Int): Logical pin index (0-based)
Returns: Integer value (0-1023)
C++ Registration:
Uniot.registerLispAnalogInput(A0);Example - Auto Light:

5. bclicked - Button Clicked
bclicked - Button ClickedChecks if a registered button was clicked (and resets the click state).
Parameters:
index(Int): Logical pin index (0-based)
Returns: Boolean (true if button was clicked)
C++ Registration:
auto button = new uniot::Button(0, LOW, 30);
Uniot.registerLispButton(button, FOURCC(_btn));Example - Button Event Handler:

Complete Built-in Primitives Example
C++ Code:
#include <Uniot.h>
#define PIN_LED_R 15
#define PIN_LED_G 12
#define PIN_LED_B 13
#define PIN_BUTTON 4
#define PIN_LDR A0
#define BTN_PIN_LEVEL LOW
#define LED_PIN_LEVEL HIGH
void setup() {
// Configure WiFi
Uniot.configWiFiCredentials("MyWiFi", "password");
Uniot.configUser("uniot_account_id");
Uniot.configWiFiStatusLed(PIN_LED_R, LED_PIN_LEVEL);
Uniot.configWiFiResetButton(PIN_BUTTON, BTN_PIN_LEVEL);
// Register GPIO for UniotLisp access
Uniot.registerLispDigitalOutput(PIN_LED_R, PIN_LED_G, PIN_LED_B);
Uniot.registerLispDigitalInput(PIN_BUTTON);
Uniot.registerLispAnalogOutput(PIN_LED_R, PIN_LED_G, PIN_LED_B);
Uniot.registerLispAnalogInput(PIN_LDR);
// Register button object
auto button = new uniot::Button(PIN_BUTTON, BTN_PIN_LEVEL, 5);
Uniot.registerLispButton(button);
Uniot.begin();
}
void loop() {
Uniot.loop();
}Script:


Register System Internals
The Register system consists of two main components:
GpioRegister
Manages GPIO pin mappings:
Stores physical pin numbers in named registers
Maps logical indices to physical GPIO numbers
Validates pin access attempts
ObjectRegister
Manages object references (like Button instances):
Stores pointers to registered objects
Associates objects with identifiers (FOURCC codes)
Provides type-safe object retrieval
PrimitiveExpeditor Access
Built-in primitives access the Register system through PrimitiveExpeditor:
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:
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
Using PrimitiveExpeditor
PrimitiveExpeditor is a helper class that simplifies primitive creation by handling:
Argument type validation
Argument extraction
Type conversion
Error handling
Return value creation
Step-by-Step Example
Let's create a primitive that controls an LED based on brightness:
1. Declare the Primitive
#include <Uniot.h>
using namespace uniot;
Object set_led_brightness(Root root, VarObject env, VarObject list);2. Implement the Primitive
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", // Primitive name (used in error messages)
Lisp::Bool, // Return type
2, // Number of arguments
Lisp::Int, // First argument type (pin)
Lisp::Int // Second argument type (brightness)
).init(root, env, list);
// Validate that arguments match the description
expeditor.assertDescribedArgs();
// Extract arguments (automatically type-checked)
int pin = expeditor.getArgInt(0);
int brightness = expeditor.getArgInt(1);
// Clamp brightness to valid PWM range
brightness = std::max(0, std::min(255, brightness));
// Perform the hardware operation
analogWrite(pin, brightness);
// Return success status
return expeditor.makeBool(true);
}3. Register the Primitive
void setup() {
// ... other setup code ...
// Register the primitive
Uniot.addLispPrimitive(set_led_brightness);
Uniot.begin();
}4. Use in Scripts

Best Practices
Descriptive Names
Use clear, descriptive names that indicate what the primitive does:
// Good
Object turn_relay_on(...)
Object read_temperature_sensor(...)
Object set_motor_speed(...)
// Avoid
Object relay(...) // Ambiguous
Object temp(...) // Too abbreviated
Object go(...) // UnclearValidate Inputs
Always validate arguments to prevent undefined behavior:
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 pin = expeditor.getArgInt(0);
int value = expeditor.getArgInt(1);
// Validate pin number
if (pin < 0 || pin > 16) {
UNIOT_LOG_ERROR("Invalid pin: %d", pin);
return expeditor.makeBool(false);
}
// Clamp value to valid range
value = std::max(0, std::min(255, value));
analogWrite(pin, value);
return expeditor.makeBool(true);
}Use Logging
Log operations for debugging and monitoring:
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);
}Keep Primitives Simple
Each primitive should do one thing well:
// Good - focused primitives
Object read_sensor(...);
Object calculate_average(...);
Object send_notification(...);
// Avoid - doing too much
Object read_sensor_calculate_and_notify(...); // Too complexReturn Meaningful Values
Choose return types that provide useful information:
// 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(...) -> SymbolConclusion
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 across different devices
No additional C++ code required
Custom Primitives enable:
Extensibility: Add new capabilities without modifying 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: Register system prevents unauthorized pin access
Maintainable: Clear separation between firmware and scripts
Further Reading
PrimitiveExpeditor: Complete reference for argument handling and validation
Register System: Detailed documentation on GPIO and object registration
UniotLisp Interpreter: Understanding the embedded UniotLisp environment and syntax
Last updated