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:

  1. Built-in Primitives: Pre-implemented functions for common hardware operations (GPIO, buttons, etc.)

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

  1. Script Received: Device receives script via MQTT

  2. Interpretation: Interpreter parses and executes the script

  3. Primitive Call: Script calls a primitive function

  4. C++ Execution: Primitive executes C++ code

  5. Result Return: Result is converted back to Lisp data type

  6. Script Continues: Script processes the result

Type System

Primitives use a simple type system for argument validation:

Lisp Type
C++ Type
Description

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

If you create scripts using the visual editor, this block is automatically generated for you. You can skip this section unless you're writing scripts manually.

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-library and ;;; end-user-library markers

  • Each declaration is on a single line, prefixed with ; (comment)

  • The ;-> arrow indicates the return type

  • Multiple 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

  1. Registration: C++ code registers physical GPIO pins using Uniot.registerLisp*() methods

  2. Mapping: Each registered pin gets a logical index (0, 1, 2, ...)

  3. Access: Scripts use logical indices to access pins through built-in primitives

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

Primitive
Purpose
Registration Method
Signature

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

Writes 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

Reads 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)

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

Reads 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

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

Auto-brightness
Color cycle on button click

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

Last updated