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:
Purpose
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:
Example
If you have a custom primitive set_led_brightness that takes two integers (pin and brightness) and returns a boolean:
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:
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:
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:
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:
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:
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:
Example - Button Event Handler:

Complete Built-in Primitives Example
C++ Code:
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:
Creating Custom Primitives
Basic Structure
A primitive is a C++ function with a specific signature:
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
2. Implement the Primitive
3. Register the Primitive
4. Use in Scripts

Best Practices
Descriptive Names
Use clear, descriptive names that indicate what the primitive does:
Validate Inputs
Always validate arguments to prevent undefined behavior:
Use Logging
Log operations for debugging and monitoring:
Keep Primitives Simple
Each primitive should do one thing well:
Return Meaningful Values
Choose return types that provide useful information:
Conclusion
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