---
title: Lunatone DALI-2 USB Driver
description: Reference for the LunatoneLuba driver — a DALI-2 bus master through a Lunatone DALI-2 USB interface. Levels, unit status and failures, settings read/write with verification, D4i memory banks and addressing.
---

# Lunatone DALI-2 USB (LunatoneLuba)

**DALI-2 bus master through a [Lunatone DALI-2 USB](https://www.lunatone.com/) interface.**
One connector controls every piece of DALI control gear on the interface's bus: set levels,
read status and LED failure flags, read and write the DALI configuration, read D4i
identity / energy / diagnostics banks, and give gear short addresses.

The driver talks Lunatone's LUBA protocol over the interface's USB virtual COM port
(38400 baud, 8N1). Tier-1 free JavaScript driver, distributed as a
`.muxdriver` package. **Needs Muxit 0.43.1 or later**: LUBA frames contain bytes ≥ `0x80`,
which older builds could not send from a JS driver. On an older build the `writeMode`
property reads `"text"` and those frames fail with a clear error instead of going out corrupted.

## Connector config

The driver's template is a connector named `dali` that pairs with the **DALI status**
dashboard and the `examples/dali-monitor` script in the example workspace:

```javascript
// connectors/dali.js
export default {
  driver: "LunatoneLuba",
  config: {
    port: "COM5",        // Required. The interface's COM port ("/dev/ttyACM0" on Linux).
    line: 0,             // Optional. DALI line of the interface. Default 0.
    busPower: true,      // Optional. Switch the interface's own bus supply on (true) or off (false).
                         //           Omit to leave it as it is.
    timeoutMs: 500,      // Optional. Timeout per LUBA request. Default 500.
    priority: 1,         // Optional. DALI frame priority 1-5. Default 1.
    autoOpen: true,      // Optional. Open the port when the connector starts. Default true.
  },
};
```

The full template adds, on top of the driver's own properties and actions:

- **Dashboard properties**, computed from the cached `busStatus` result, so polling them
  costs no bus traffic: `busState` (`OK` / `FAIL` / `NO BUS` / `NO UNITS` / `NO DATA` /
  `OFFLINE`), `statusText`, `failuresText`, `unitCount`, `busPowered`, `levels` (array, one
  level per unit) and `lastUpdate`. `broadcastLevel` is writable: writing it sets every unit.
- **Short methods**: `refresh` and `rescan` (update the status, return the text summary),
  `overview(rescan?)` and `summary(rescan?)`, `allOff()`, `level(address, level)`,
  `actual(address)`, `status(address)`, `settings(address)`, `configure(address, settings)`,
  `d4i(address)`, `bank(address, bank, first?, last?)`, `linear(address)`, `identify(address)`,
  and `handOver()` / `takeBack()` to release and re-open the port.

Create the connector from the Extensions panel to get it, or copy
`connectors/_examples/dali.js`.

A DALI bus needs a supply. If no gear on the bus provides one, set `busPower: true` so
the interface supplies it; `busStatus` reports `NO VOLTAGE` otherwise.

## Addresses

Actions that send a command take `address` as a short address `0`–`63`, a group `"g0"`–`"g15"`,
or `"broadcast"`. Queries and reads need a short address. Levels are DALI arc power values
`0`–`254` (0 = off); the gear clamps them to its MIN/MAX level.

## Properties

| Property | Type | Description |
|----------|------|-------------|
| `connected` | bool | COM port open and LUBA interface configured |
| `port` | string | Serial port of the interface |
| `units` | object | Result of the last `busStatus` call (cached, no bus traffic); `null` before the first call |
| `interfaceSettings` | object | `{mode, eventFilter, hardware, busPowerOn}` as applied at `open` |
| `writeMode` | string | `"raw"` (binary-safe), `"hex"` (test transport) or `"text"` (Muxit older than 0.43.1) |
| `lastError` | string | Last bus or protocol error with ISO timestamp; empty when none |

All properties are read-only. Polling them costs no bus traffic.

## Actions

**Overview and port**

| Action | Args | Returns |
|--------|------|---------|
| `busStatus` | `rescan?` | `{ok, time, interface, units, text}`: per-unit level and failures plus a readable summary. Never throws for bus problems. First call (or `rescan: true`) scans addresses 0–63 (~2 s). |
| `open` | — | `"OK"`. Open the port and configure the interface. |
| `release` | — | `"OK"`. Close the port so another program (e.g. MultiOne) can use the interface. |
| `interfaceInfo` | — | `{gtin, id, pcbVersion, assemblyVersion, articleNumber, info, raw}` |
| `lineStatus` | — | `{line, available, nextId, framesInBuffer, busVoltageError, initialiseMode, quiescentMode}` |
| `scan` | — | `[{address, duplicate}]`: short addresses that answer QUERY CONTROL GEAR PRESENT |

**Light level**

| Action | Args | Returns |
|--------|------|---------|
| `setLevel` | `address`, `level` | `{frame, result, answer}`. Direct Arc Power 0–254. |
| `off` / `recallMax` / `recallMin` | `address` | `{frame, result, answer}` |
| `identify` | `address` | `{frame, result, answer}`. The unit signals itself for ~10 s (DALI-2). |

**Status and settings** (short address only)

| Action | Args | Returns |
|--------|------|---------|
| `queryActualLevel` | `address` | 0–254, or `null` when no gear answers |
| `queryStatus` | `address` | QUERY STATUS decoded into flags |
| `readStatus` | `address` | Status flags, actual level, gear failure, DT6 LED failure flags, `failures` (list of strings) and `ok` |
| `readSettings` | `address` | DALI version, device types, light source type, MIN/MAX/power-on/system-failure levels, fade, groups; dimming curve for DT6 gear |
| `query` | `address`, `opcode` | The 8-bit answer to any DALI query opcode, or `null` |
| `queryDimmingCurve` | `address` | `"logarithmic"`, `"linear"`, or `null` |
| `readMemoryBank` | `address`, `bank`, `first?`, `last?` | `{bank, implemented, bytes, hex, decoded?}` |
| `readD4i` | `address` | Banks 0, 1, 202, 205 and 206 decoded: identity, luminaire data, energy, gear and light-source diagnostics (~200 transactions) |

**Changing stored configuration**

| Action | Args | Returns |
|--------|------|---------|
| `storeSettings` | `address`, `settings` | Writes `maxLevel`, `minLevel`, `powerOnLevel`, `systemFailureLevel`, `fadeTime`, `fadeRate`, `groups`; for a short address, reads back and lists `mismatches` |
| `setDimmingCurve` | `address`, `curve` | DT6: `"linear"` or `"logarithmic"` |
| `reset` | `address` | DALI RESET: levels, fade, groups and scenes back to reset values (short address kept) |
| `setShortAddress` | `address`, `newAddress` | Change or delete (`null`) one unit's short address; refuses an address in use |
| `assignAddresses` | `mode?`, `first?` | `[{address, random}]`. `"unaddressed"` (default) numbers gear without a short address; `"all"` deletes every short address first. ~3–4 s per unit. |
| `sendRaw16` | `hex`, `answer?`, `twice?` | Send any 16-bit forward frame, e.g. `"FFA0"` |

These change what the gear stores, and `assignAddresses` with `mode: "all"` renumbers the
whole bus. The template's `ai.instructions` tell the AI to ask before calling them. The
driver documents each action's full return format in its `details`, which the AI reads on
demand.

## Streams

| Stream | Content |
|--------|---------|
| `busEvents` | Bus-level events as JSON strings: `busError`, `systemFailure`, `busRestored`, `busSupplyWarning` |

Bind a terminal widget to `dali:busEvents` to watch them.

## Example script

```javascript
const dali = connector("dali");

const st = dali.busStatus({ rescan: true });
log.info(st.text);

dali.setLevel("broadcast", 128);
const d4i = dali.readD4i(0);
log.info(`unit 0: ${d4i.energy?.activeEnergyWh} Wh, ${d4i.gearDiagnostics?.temperatureC} °C`);
```

## Sharing the interface with MultiOne

Settings that are not part of DALI (such as a Philips driver's output current) need the
vendor's tool. Call `release` to free the COM port, use the other program, then `open` to
take the interface back.

## Notes

- All bus access is serialised: one transaction, or one multi-frame sequence such as
  DTR0 + STORE or a memory-bank read, at a time.
- D4i decoding follows IEC 62386-102 ed2 (bank 0) and DiiA Parts 251–253. The raw bytes are
  always returned next to the decoded fields; check them against your gear before relying
  on a decoded value in limits.
- Revision history: `drivers/js/lunatone-luba/CHANGELOG.md`.
