Skip to main content

Extension Plugin Guide

Extension plugins are background services that can bridge external protocols, register virtual devices, lock LEDs for direct control, and provide custom HTML UI pages.

Directory Structure

plugins/extension.my_bridge/
├── manifest.json # Metadata + permissions
├── init.lua # Entry script
├── lib/ # Optional Lua modules
│ └── protocol.lua
├── locales/ # Optional i18n files
├── data/ # Persistent data directory
└── page/ # Optional embedded HTML UI
└── dist/
└── index.html

Lifecycle

Core startup → Load enabled extensions
→ Initialize Lua environment
→ plugin.on_start() ← Extension started
→ [Event loop]
plugin.on_scan_devices() ← Manual scan triggered
plugin.on_devices_changed() ← Device list changed
plugin.on_led_locks_changed()← LED lock changed
plugin.on_device_frame() ← Real-time LED data
plugin.on_page_message() ← Message from HTML page
→ plugin.on_stop() ← Extension stopping

Lifecycle Hooks

on_start()

Called when the extension is loaded. Initialize connections, spawn external processes, register devices.

function plugin.on_start()
ext.log("Extension starting")
-- Connect to external service, register devices, etc.
end

on_scan_devices()

Called when the user manually triggers a device scan.

function plugin.on_scan_devices()
ext.log("Scanning for devices...")
-- Discover and register devices
end

on_devices_changed(devices)

Called when the global device list changes.

function plugin.on_devices_changed(devices)
-- React to new/removed devices
end

on_led_locks_changed(locks)

Called when any LED lock state changes.

function plugin.on_led_locks_changed(locks)
-- locks: current lock state
end

on_device_frame(port, outputs)

Called with real-time LED color data for every active device frame. This is high-frequency (up to 30+ fps).

function plugin.on_device_frame(port, outputs)
-- outputs: {output_id = {r,g,b,r,g,b,...}, ...}
local colors = outputs["out1"]
if colors then
-- Forward colors to external system
end
end

on_page_message(data)

Called when the embedded HTML page sends a message.

function plugin.on_page_message(data)
if data.action == "refresh" then
-- Handle page request
ext.page_emit({status = "ok", devices = ext.get_devices()})
end
end

on_stop()

Called when the extension is being unloaded. Clean up all resources.

function plugin.on_stop()
ext.log("Extension stopping")
-- Close connections, kill processes, unregister devices
end

Registering Virtual Devices

Extensions can register virtual devices that appear in Skydimo like physical hardware:

local port = ext.register_device({
controller_port = "openrgb://device_0",
manufacturer = "Corsair",
model = "Vengeance RGB",
serial_id = "ABC123",
description = "RAM Module",
controller_id = "extension.openrgb",
device_type = "dram",
outputs = {
{
id = "zone0",
name = "Zone 0",
leds_count = 8,
output_type = "linear",
}
}
})

These devices receive effects from Skydimo and you forward the rendered colors to the real hardware via on_device_frame.

Updating Devices

-- Change device nickname
ext.set_device_nickname(port, "My RAM Stick")

-- Update output configuration
ext.update_output(port, "zone0", {
leds_count = 16,
matrix = nil -- or {width=4, height=4, map={...}}
})

-- Remove a device
ext.remove_extension_device(port)

LED Locking

Extensions can lock specific LEDs for direct color control, overriding the active effect:

-- Lock LEDs 0-9 for direct control
ext.lock_leds("COM3", "out1", {0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

-- Set colors on locked LEDs
ext.set_leds("COM3", "out1", {
{0, 255, 0, 0}, -- LED 0: red
{1, 0, 255, 0}, -- LED 1: green
{2, 0, 0, 255}, -- LED 2: blue
})

-- Release locks
ext.unlock_leds("COM3", "out1", {0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

Networking (TCP)

Extensions can make TCP connections. Requires the "network:tcp" permission.

-- Connect
local handle = ext.tcp_connect("127.0.0.1", 6742)
-- With timeout:
local handle = ext.tcp_connect("127.0.0.1", 6742, 5000)

-- Send data
local bytes_sent = ext.tcp_send(handle, data_string)

-- Receive data
local data = ext.tcp_recv(handle, 1024) -- Up to 1024 bytes
local data = ext.tcp_recv(handle, 1024, 5000) -- With 5s timeout
local data = ext.tcp_recv_exact(handle, 256) -- Exactly 256 bytes
local data = ext.tcp_recv_exact(handle, 256, 5000) -- With timeout

-- Close
ext.tcp_close(handle)

Process Management

Extensions can spawn and manage external processes. Requires the "process" permission.

-- Spawn a process
local handle = ext.spawn_process("openrgb.exe", {"--server"}, {
hidden = true,
working_dir = ext.data_dir
})

-- Check if running
if ext.is_process_alive(handle) then
ext.log("Process is running")
end

-- Kill process
ext.kill_process(handle)

Effect Management

Extensions can query and control effects on devices:

-- Get all available effects
local effects = ext.get_effects()

-- Get effect parameter schema
local params = ext.get_effect_params("rainbow")

-- Set effect on a device output
ext.set_effect("COM3", "out1", "rainbow")
ext.set_effect("COM3", "out1", "rainbow", {speed = 3.0})

Page Communication

Extensions can include an embedded HTML page that appears in the Skydimo UI:

manifest.json
{
"page": "page/dist/index.html"
}

Communicate between Lua and the page:

-- Send data to the HTML page
ext.page_emit({type = "update", devices = ext.get_devices()})

-- Receive from the page (via on_page_message hook)
function plugin.on_page_message(data)
if data.type == "set_color" then
ext.set_leds(data.port, data.output, data.colors)
end
end

Notifications

Send toast notifications to the user:

-- Simple notification
ext.notify("Connection Established", "Connected to OpenRGB server")

-- With level
ext.notify("Warning", "Device not responding", "warn") -- "info", "warn", "error"

-- Persistent notification (stays until dismissed)
ext.notify_persistent("conn_status", "Connecting...", "Attempting to connect to server")

-- Dismiss it later
ext.dismiss_persistent("conn_status")

Complete Example: Protocol Bridge

local plugin = {}

local conn = nil
local devices = {}

function plugin.on_start()
ext.log("Bridge extension starting")

-- Spawn external service
local handle = ext.spawn_process("bridge-server.exe", {"--port", "9000"}, {
hidden = true
})

-- Wait for server to start
ext.sleep(2000)

-- Connect
conn = ext.tcp_connect("127.0.0.1", 9000, 5000)
if not conn then
ext.error("Failed to connect to bridge server")
return
end

-- Discover and register devices
discover_devices()
end

function discover_devices()
ext.tcp_send(conn, "LIST_DEVICES\n")
local response = ext.tcp_recv(conn, 4096, 3000)
if not response then return end

-- Parse response and register each device
for name, leds in response:gmatch("(%S+):(%d+)") do
local port = ext.register_device({
controller_port = "bridge://" .. name,
manufacturer = "Bridge",
model = name,
controller_id = "extension.my_bridge",
device_type = "light",
outputs = {{
id = "main",
name = "Main",
leds_count = tonumber(leds),
output_type = "linear"
}}
})
devices[port] = {name = name, conn = conn}
end
end

function plugin.on_device_frame(port, outputs)
local dev = devices[port]
if not dev then return end

local colors = outputs["main"]
if colors then
local packet = "SET_LEDS:" .. dev.name .. ":" .. table.concat(colors, ",")
ext.tcp_send(conn, packet .. "\n")
end
end

function plugin.on_scan_devices()
discover_devices()
end

function plugin.on_stop()
-- Unregister all devices
for port in pairs(devices) do
ext.remove_extension_device(port)
end
-- Close connection
if conn then ext.tcp_close(conn) end
end

return plugin

See the Extension API Reference for the complete API.