LED-Warrior14 DALI initialisation

English posts only please

Moderator: Guido Körber

Post Reply
shmonov
Posts: 1
Joined: Wed Dec 22, 2021 5:32 pm

LED-Warrior14 DALI initialisation

Post by shmonov »

Hello,

I'm designing a DALI bus light control system for vertical farming.
I recently bought Codemercs' LED-Warrior14-02 Master controller modules because they are said to support all the DALI commands (not the first 32 commands like LED-Warrior09 do).
I use an ESP32 board, then an I2C-friendly logic level converter (because the ESPs are not 5V-tolerant), then the LED-Warrior14-02 module (to which my DALI lamps are connected).
I have no problem with broadcast commands and can turn on/off all the lamps or set them to a desired level.
However I have some problems implementing DALI network initialisation. To be more specific, I don't know where to look for the reply after sending a 0xA9 (COMPARE) command.
Do you have any libraries, code snippets or literature, explaining how to perform DALI network initialisation properly?

Thank you,
Alexander Shmonov
Guido Körber
Site Admin
Posts: 2864
Joined: Tue Nov 25, 2003 10:25 pm
Location: Germany/Berlin
Contact:

Re: LED-Warrior14 DALI initialisation

Post by Guido Körber »

The LW14 is fine with a 3.3 V I2C and since it is the device and not the master this should work without a level shifter.

A LW14-02MOD should only be used when the complete circuit is powered off the DALI bus via the LW14-02MOD.

The DALI deployment is a somewhat complex process. We don't have complete sample code for this. The specifications for this are only available in the standard.

A device which is still in the selection after a Compare command will answer with a "Yes" = 0xFF.
Slysven
Posts: 4
Joined: Sat Nov 09, 2024 8:50 pm

Re: LED-Warrior14 DALI initialisation

Post by Slysven »

I appreciate that this reply is a bit of a necro-posting but people interested in a full feature DALI software library might want to keep an eye on the python-dali project - I recently found it and the project owner seems keen to add the LED-Warrior14 (at least) device into the list interfaces/devices it supports. Specifically the Issue 147 is the one that I raised for this module...
Guido Körber
Site Admin
Posts: 2864
Joined: Tue Nov 25, 2003 10:25 pm
Location: Germany/Berlin
Contact:

Re: LED-Warrior14 DALI initialisation

Post by Guido Körber »

Thank you for this info, this looks great. If any support is needed, just let us know.
Slysven
Posts: 4
Joined: Sat Nov 09, 2024 8:50 pm

Re: LED-Warrior14 DALI initialisation

Post by Slysven »

A bit off-topic for the original post but something you might want to consider in any future PWB work - the pads around the row of 6 points where the connections are made on the LED-Warrior14 (and others that use the same PWB) really ought to have larger copper areas to them to make them stronger (and less likely to break when the end user tries to connect or indeed, remove and reconnect, to them).

Back in the 80s when I was designing single and then double-sided PWBs (for TVs) it was a given that pads for external connections really needed to be larger than normal to account for the strain that wires or connectors soldered to those pads would exert on them even in normal conditions never mind what someone investigation a fault or a servicing operation might place on them...!
Slysven
Posts: 4
Joined: Sat Nov 09, 2024 8:50 pm

Re: LED-Warrior14 DALI initialisation

Post by Slysven »

Okay, we have success, using the LGPL 3+ licensed python-dali repository as of commit 82fcf1e72aa37e1a83282f0f516f74559d5320e7 with the following script (created by the project owner!) placed into the examples sub-directory on a Raspberry Pi (2B in my case) connected to a LED-Warrior14(-01) at the default I2C address of 0x23 as a file which I chose to call commission.py and with the user being a member of the i2c group and having the python3-smbus package installed in the system:

Code: Select all

#!/usr/bin/env python3
# Sample code for CodeMercs LW14 with python-dali on Raspberry Pi I2C

from dali.command import Command
from dali.gear.general import EnableDeviceType, Off
from dali.address import GearBroadcast
from dali.sequences import sleep as seq_sleep
from dali.sequences import progress as seq_progress
from dali.exceptions import CommunicationError, UnsupportedFrameTypeError
from dali.sequences import Commissioning
import dali.frame
import time
import logging
import struct

struct_signature = struct.Struct(">HHH")


class I2C_RPI:
    def __init__(self, address):
        import smbus
        self.bus = smbus.SMBus(1)
        self.address = address

    def ping(self):
        self.bus.write_quick(self.address)

    def write_byte(self, register, data):
        self.bus.write_byte_data(self.address, register, data)

    def read_byte(self, register):
        return self.bus.read_byte_data(self.address, register)

    def write_bytes(self, register, data):
        self.bus.write_i2c_block_data(self.address, register, data)

    def read_bytes(self, register, length):
        return bytes(self.bus.read_i2c_block_data(
            self.address, register, length))


class LW14:
    def __init__(self, i2c, logger=None):
        self.bus = i2c
        self.log = logger or logging.getLogger(__name__)

        # Simple presence check — raises IOError if device absent
        self.bus.ping()

        # Read and check signature
        vendor, product, version = struct_signature.unpack(
            self.bus.read_bytes(0xf0, 6))

        self.log.debug("LW14 vendor: %d", vendor)
        self.log.debug("LW14 product: %d", product)
        self.log.debug("LW14 version: %x", version)

        if product != 14:
            raise Exception(f"Unsupported product ID {product}; expected 14")

        if version < 0x2000:
            raise Exception(f"Unsupported LW14 version {version:02x}; minimum "
                            f"version required is 2000")

        # Clear "telegram received", "overrun", "valid reply" bits
        self.bus.read_byte(0x01)

        # Read status register to clear the "frame error" bit
        self._read_status()
        
        # Check that no unexpected status bits are set
        status = self._read_status()
        if status & 0x3b:
            raise Exception("Unexpected bits set in status register")

    @staticmethod
    def _wrap(command):
        response = yield command
        return response

    def _send_frame(self, frame, wait_for_response=False):
        if len(frame) not in (16, 24):
            raise UnsupportedFrameTypeError

        # Wait for device to be ready
        status = 0x40
        while status & 0x40:
            status = self._read_status()
            if status & 0x80:
                # Bus fault — we can't continue
                raise CommunicationError(
                    "Bus fault while waiting to transmit")
            if status & 0x0b:
                # "Valid reply" or one of the "telegram byte(s) received" bits
                # is still set — read command register to clear
                self.log.debug("Reading command reg to clear old reply")
                self.bus.read_byte(0x01)
            if status & 0x40:
                # Still busy — wait a bit before polling again
                time.sleep(0.002)

        log.debug("Status before sending frame is %x", status)

        # Write frame to command register; the LW14 will transmit as
        # soon as the bus is free, according to the priority in the
        # config register
        self.bus.write_bytes(0x01, frame.as_byte_sequence)
        
        if wait_for_response:
            frame_error = False

            log.debug("Waiting for response")
            # Poll the status register. We are waiting for bit 0x04
            # ("Reply timeframe") and bit 0x40 ("Busy") to become unset.
            status = 0x44
            while status & 0x44:
                status = self._read_status()
                if status & 0x80:
                    # Bus fault — we can't continue
                    raise CommunicationError(
                        "Bus fault while waiting for response")
                if status & 0x10:
                    # Frame error
                    log.debug("Frame error bit set while waiting for response")
                    frame_error = True
                if status & 0x44:
                    # Still waiting
                    time.sleep(0.0002)

            log.debug("Finished waiting, status is %x", status)

            if frame_error:
                return dali.frame.BackwardFrameError(0)

            # If "valid reply" is set, check that it was a single
            # byte.  If so, it's a response. Otherwise it's an error.
            if status & 0x08:
                while status & 0x03 == 0:
                    # Poll until the reply is fully received and its length
                    # is available
                    time.sleep(0.001)
                    status = self._read_status()
                    if status & 0x10:
                        # Frame error
                        log.debug("Frame error bit set while reading response")
                        return dali.frame.BackwardFrameError(0)
                if (status & 0x03) == 1:
                    return dali.frame.BackwardFrame(self.bus.read_byte(0x01))
                raise CommunicationError(
                    "Forward frame received while waiting for backward frame")
            
    def _read_status(self):
        return self.bus.read_byte(0x00)

    def _set_priority(self, priority):
        self.log.debug("Setting priority %d", priority)
        self.bus.write_byte(0x02, priority)

    def send(self, command, priority=2, progress=None):
        # If "command" is not a sequence, wrap it in a trivial sequence
        seq = self._wrap(command) if isinstance(command, Command) else command

        self._set_priority(priority)

        response = None
        while True:
            try:
                cmd = seq.send(response)
            except StopIteration as r:
                return r.value

            if isinstance(cmd, seq_sleep):
                log.debug("Waiting for %f seconds", cmd.delay)
                time.sleep(cmd.delay)
            elif isinstance(cmd, seq_progress):
                if progress:
                    progress(cmd)
            else:
                self.log.debug("Sending %s", cmd)
                if cmd.devicetype != 0:
                    self._send_frame(EnableDeviceType(cmd.devicetype).frame)
                    self._set_priority(1)
                if cmd.sendtwice:
                    self._send_frame(cmd.frame)
                    self._set_priority(1)
                response_frame = self._send_frame(
                    cmd.frame, cmd.response is not None)
                self._set_priority(1)
                response = None
                if cmd.response:
                    response = cmd.response(response_frame)
                    self.log.debug("Response is %s", response)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    log = logging.getLogger()
    d = LW14(I2C_RPI(0x23))
    # Turn everything on while we work...
    d.send(Off(GearBroadcast()))
    # Assign addresses to all attached control gear
    d.send(Commissioning(readdress=True), priority=3, progress=log.info)

I was able to reliably (re)set all the short addresses for the devices on my DALI system and assign a random short-address starting at 0 and increasing monotonically for each and all of them. The behaviour should there be a collision between two of the sixty four possible devices in the 2^24 possible random addresses they might choose to use is not clear to me but the chance of it happening more than once seems vanishingly small and fixable by running it again. So this seems a viable starting point for setting up a system where you don't want to have to set a short address by connecting to each device individually ...
Post Reply