Try any driver free for 30 days

Docs · Control4 guides

DriverWorks and Lua for Control4 dealers

DriverWorks is Control4's driver framework. A DriverWorks driver is Lua code plus an XML manifest, packaged as a .c4z file. You do not need to write drivers to benefit from understanding them: knowing how they work makes reading the Lua output, setting properties, and diagnosing third-party drivers much faster. Official docs: the DriverWorks SDK and API reference published by Control4 (control4.github.io/docs-driverworks-api).

What is inside a .c4z driver

A .c4z is a zip archive containing:

  • driver.xml: the manifest. Name, manufacturer, model, version, creator, minimum OS, proxies, connections, properties, commands, actions, events, conditionals, and documentation reference.
  • driver.lua: the Lua source. Commercial drivers often ship it encrypted, so you can read the XML but not the code.
  • www/: icons, the documentation page shown in Composer's Documentation tab, and other assets.

A .c4i file is an info-only driver definition, mostly used for drivers built into the OS.

The driver version in driver.xml is what Composer shows and compares on update. Many drivers also show it in a read-only Driver Version property.

driver.xml: proxies and connections

Proxies decide what the driver is to Control4: a TV, receiver, switcher, light, keypad, thermostat, media service, or a generic UI button. The proxy is what the room, the Navigator, and programming talk to. Some drivers have several proxies (for example a receiver plus a "Bluetooth" button plus a media service).

Connections are the binding points you see in the Connections view: control connections (IR, serial, relay, contact), audio and video inputs and outputs, network connections, and room control. Each has a binding ID. A common convention: low numbers for control connections, the 1000s through 4000s for video and audio inputs and outputs, 5001 and up for proxies, 6001 and up for network connections, and 7000 and up for room selection.

Changing a driver's connections in an update makes Director migrate bindings, which briefly disconnects Composer. Well-behaved driver updates keep connections stable.

Properties and OnPropertyChanged

Properties are the fields on the driver's Properties tab in System Design: IP address, port, username, password, polling interval, log level, and read-only status fields.

In Lua, current values live in the global Properties table. When a dealer changes one, Director calls:

function OnPropertyChanged(strProperty)
  local value = Properties[strProperty]
  -- react to the change
end

Drivers update read-only properties (Status, Last Error, Driver Version, Connected) with C4:UpdateProperty(name, value). These are the first thing to check when a driver misbehaves. Properties are private to the driver. Other drivers and programming read state through variables, not properties.

Commands, actions, and ExecuteCommand

Two kinds of driver commands:

  • Commands: callable from programming (they appear on the Commands tab in Programming view) and from other drivers.
  • Actions: buttons on the driver's Actions tab in System Design, used for setup and maintenance (Pair, Discover, Test Connection, Reset).

Both arrive in Lua through ExecuteCommand. Actions come in as the command LUA_ACTION with the action name in the parameters:

function ExecuteCommand(strCommand, tParams)
  tParams = tParams or {}
  if strCommand == "LUA_ACTION" then
    local action = tParams.ACTION
    -- run the action
    return
  end
  -- handle programming commands
end

A driver that forgets the LUA_ACTION branch has Action buttons that silently do nothing.

Commands sent from one driver to another with C4:SendToDevice also arrive in the target's ExecuteCommand, not ReceivedFromProxy.

Proxy commands: ReceivedFromProxy and SendToProxy

When a user presses something on a Navigator, or the room tells a device to do something (power on, select input, set volume, set light level), the proxy sends it to the driver:

function ReceivedFromProxy(idBinding, strCommand, tParams)
  -- e.g. ON, OFF, SET_INPUT, SET_VOLUME_LEVEL, SET_LEVEL
end

idBinding tells the driver which proxy the command came from. The driver reports device state back to the proxy with C4:SendToProxy(idBinding, notification, params), which is how the Navigator shows the right input, volume, or light level. If a device changes but the app does not reflect it, the driver is not sending these notifications, often because it is not receiving feedback from the device.

Events, variables, and conditionals

  • Events: declared in driver.xml, fired in Lua with C4:FireEvent("Event Name"). They show up in Programming view under the driver.
  • Variables: created with C4:AddVariable and updated with C4:SetVariable. Programming can compare them and other drivers can read them. A driver that wants its state visible to programming publishes it as a variable.
  • Conditionals: declared in driver.xml, answered in Lua by a function that returns true or false. They show on the Conditionals tab in Programming.

Watching other devices: C4:RegisterVariableListener reports changes to another device's variable. It also reports the current value when you register. A driver that treats that first report as a change will fire alerts or actions right after loading. Treat the first report as a baseline.

Network, serial, and HTTP in drivers

Network (TCP and UDP): drivers declare a network connection, then Director manages the socket. Data arrives in ReceivedFromNetwork(idBinding, nPort, strData). Connection changes arrive in OnConnectionStatusChanged(idBinding, nPort, strStatus) with ONLINE or OFFLINE. That status is the authoritative "is this device reachable" signal.

Serial: the driver's serial connection is bound to a controller port. Data arrives in ReceivedFromSerial(idBinding, strData). Outbound uses C4:SendToSerial.

HTTP and cloud APIs: C4:url (builder style) and the older C4:urlGet, C4:urlPost, C4:urlPut, C4:urlDelete functions. All are asynchronous with a callback. A response code of 0 means the request never completed (DNS, TLS, or timeout), not an HTTP error.

Other notes for driver writers: C4:JsonEncode and C4:JsonDecode handle JSON (check how the encoder treats bare strings and arrays before sending to a strict external API). C4:PersistSetValue and C4:PersistGetValue store data across reboots.

Timers and lifecycle

Lifecycle functions Director calls:

  • OnDriverInit: first, before the driver is fully set up.
  • OnDriverLateInit: after properties are loaded and the project is ready. Safe to read properties here.
  • OnDriverDestroyed: driver is being unloaded (update, removal, Director restart).

Timers: C4:SetTimer(milliseconds, callback, repeat) returns a timer object you can cancel. Older drivers use C4:AddTimer with an OnTimerExpired handler. Repeating timers are how drivers poll devices.

Why drivers should be inert at load

A good driver does almost nothing until it is needed. At load (OnDriverInit and OnDriverLateInit) it should only do cheap local setup: read properties, set status fields, prepare tables.

Things that do not belong at load: opening network connections to every device, reading other devices' variables, enumerating the project, starting polling loops, or calling cloud APIs. When a controller boots, every driver loads at once. Heavy work at load slows the whole system, can make Composer disconnect, and can push Director into instability.

Other habits of well-behaved drivers:

  • Connect on demand, when a command needs the device.
  • Reconnect with a capped retry and growing backoff. Never retry forever every few seconds while a device is simply turned off.
  • Wrap calls in error handling so one failure does not kill the driver.
  • Keep connections stable between versions.

If a site gets slow or unstable after adding or updating a driver, that driver is the first suspect. Remove or disable it and see if the system recovers.

Reading a driver's Lua output

In System Design, select a DriverWorks driver. The Lua tab shows its output (anything the driver prints) and, on many versions, a command box to run Lua in that driver's context.

How to use it:

  1. Set the driver's debug or log level property up (names vary: Debug Mode, Log Level, Log Mode). Choose Print or Print and Log so output shows on the Lua tab.
  2. Reproduce the problem (press the button, run the action, trigger the event).
  3. Read the output for errors (Lua errors include a line number), connection status messages, and the raw data going to and from the device.
  4. Turn debug back down when finished. Verbose logging left on adds load and fills logs.

What to look for:

  • No output at all: the driver may not be loaded (syntax error or failed load). Check the Driver Version property; a blank version often means the driver never loaded.
  • OFFLINE or connection refused: network or address problem. Check identify, IP, and port.
  • Authentication or 401 errors: credentials or pairing.
  • Data received but the UI does not update: feedback parsing or proxy notification problem. Send the log to the driver developer.

Director's logs (Tools > Device Logs and system diagnostics) capture errors drivers write to the error log, useful when the problem happened while you were not watching.

Driver troubleshooting quick list

  1. Check the read-only status properties (Status, Last Error, Connected, Driver Version).
  2. Confirm identify or IP address, and that the device answers on the network.
  3. Confirm the driver version is current, and update if not.
  4. Turn on debug, reproduce, read the Lua tab.
  5. If properties look missing or stale after an update, add a fresh instance or restart Director.
  6. If the system became unstable after adding the driver, remove it and confirm stability returns, then contact the developer with the log.
  7. Licensed third-party drivers: check the license status property. An expired demo looks like a broken driver.

Still stuck? Ask Fixxy (bottom right) or open a ticket.