All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bidib.wizard.dmx.client.controller.DmxModelerController Maven / Gradle / Ivy

There is a newer version: 2.0.29
Show newest version
package org.bidib.wizard.dmx.client.controller;

import java.io.File;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;

import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.collections4.Predicate;
import org.apache.commons.collections4.SetUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.bidib.jbidibc.core.node.ConfigurationVariable;
import org.bidib.jbidibc.messages.SoftwareVersion;
import org.bidib.jbidibc.messages.utils.ByteUtils;
import org.bidib.jbidibc.messages.utils.NodeUtils;
import org.bidib.wizard.api.locale.Resources;
import org.bidib.wizard.api.model.NodeInterface;
import org.bidib.wizard.api.model.NodeListProvider;
import org.bidib.wizard.api.model.NodeProvider;
import org.bidib.wizard.api.model.listener.DefaultNodeListListener;
import org.bidib.wizard.api.model.listener.NodeListListener;
import org.bidib.wizard.api.service.node.NodeService;
import org.bidib.wizard.client.common.uils.SwingUtils;
import org.bidib.wizard.client.common.view.DefaultBusyFrame;
import org.bidib.wizard.client.common.view.DockKeys;
import org.bidib.wizard.client.common.view.DockUtils;
import org.bidib.wizard.common.labels.WizardLabelWrapper;
import org.bidib.wizard.common.model.settings.WizardSettingsInterface;
import org.bidib.wizard.common.service.SettingsService;
import org.bidib.wizard.core.model.connection.ConnectionRegistry;
import org.bidib.wizard.dmx.client.controller.listener.DmxModelerControllerListener;
import org.bidib.wizard.dmx.client.controller.listener.ProgressStatusCallback;
import org.bidib.wizard.dmx.client.controller.listener.TransferListener;
import org.bidib.wizard.dmx.client.event.DmxChannelWrapperEvent;
import org.bidib.wizard.dmx.client.event.DmxTimelineWrapperEvent;
import org.bidib.wizard.dmx.client.model.DmxChannelValue;
import org.bidib.wizard.dmx.client.model.DmxDataRow;
import org.bidib.wizard.dmx.client.model.DmxFixedPattern.FixedPatternItem;
import org.bidib.wizard.dmx.client.model.DmxOverlay.OverlayItem;
import org.bidib.wizard.dmx.client.model.DmxScenery;
import org.bidib.wizard.dmx.client.model.DmxSceneryModel;
import org.bidib.wizard.dmx.client.model.TimeDataIndex;
import org.bidib.wizard.dmx.client.schema.dmxscenery.DmxNodeType;
import org.bidib.wizard.dmx.client.schema.dmxscenery.DmxSceneriesExchange;
import org.bidib.wizard.dmx.client.schema.dmxscenery.DmxSceneriesType;
import org.bidib.wizard.dmx.client.schema.dmxscenery.DmxSceneryType;
import org.bidib.wizard.dmx.client.schema.dmxscenery.DmxTimeType;
import org.bidib.wizard.dmx.client.schema.dmxscenery.DmxTimesType;
import org.bidib.wizard.dmx.client.view.DmxDimmerConfigView;
import org.bidib.wizard.dmx.client.view.DmxSceneryView;
import org.bidib.wizard.dmx.client.view.DmxToolBarProvider;
import org.bidib.wizard.dmx.client.view.dialog.CvTransferProgressDialog;
import org.bidib.wizard.model.dmx.DmxChannel;
import org.bidib.wizard.model.dmx.DmxChannelWrapper;
import org.bidib.wizard.model.dmx.DmxDimmer;
import org.bidib.wizard.model.dmx.DmxTimelineWrapper;
import org.bidib.wizard.model.dmx.LineColors;
import org.oxbow.swingbits.dialog.task.TaskDialogs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.vlsolutions.swing.docking.Dockable;
import com.vlsolutions.swing.docking.DockingConstants;
import com.vlsolutions.swing.docking.DockingDesktop;
import com.vlsolutions.swing.docking.RelativeDockablePosition;
import com.vlsolutions.swing.docking.event.DockableStateChangeEvent;
import com.vlsolutions.swing.docking.event.DockableStateChangeListener;

public class DmxModelerController implements DmxModelerControllerListener, TransferListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(DmxModelerController.class);

    private static final String FILENAME_DMX_SCENERY_KEY = "dmxSceneryFileName";

    private static final String FILENAME_DMX_SCENERY_PATTERN =
        "Scenery-default-%010X." + DmxScenery.SCENERIES_EXTENSION;

    private final JFrame parent;

    private DockingDesktop desktop;

    private DockableStateChangeListener dockableStateChangeListener;

    private NodeListProvider nodeListProvider;

    private DmxSceneryModel dmxSceneryModel;

    private DmxSceneryView dmxSceneryView;

    private DmxToolBarProvider dmxToolBarProvider;

    private NodeListListener nodeListListener;

    private final NodeInterface node;

    @Autowired
    private NodeService nodeService;

    @Autowired
    private SettingsService settingsService;

    @Autowired
    private WizardLabelWrapper wizardLabelWrapper;

    private final Supplier nodeProviderSupplier;

    private String sceneryFilename;

    public DmxModelerController(final NodeInterface node, final JFrame parent,
        final Supplier nodeProviderSupplier) {
        this.parent = parent;
        this.node = node;
        this.nodeProviderSupplier = nodeProviderSupplier;

        this.dmxSceneryModel = new DmxSceneryModel(this.node);
    }

    @Override
    public String getSceneryFilename() {
        return sceneryFilename;
    }

    public void setSceneryFilename(String sceneryFilename) {
        this.sceneryFilename = sceneryFilename;
    }

    private static String prepareSceneryFilename(final NodeInterface node) {

        long uniqueId = NodeUtils.getUniqueIdIgnoreClassbits(node.getUniqueId());
        String filename = String.format(FILENAME_DMX_SCENERY_PATTERN, uniqueId);
        LOGGER.info("Prepared DMX scenery filename: {}", filename);
        return filename;
    }

    /**
     * Start the controller.
     * 
     * @param desktop
     *            the desktop
     * @param nodeListProvider
     *            the nodeListProvider
     */
    public void start(final DockingDesktop desktop, final NodeListProvider nodeListProvider) {
        this.nodeListProvider = nodeListProvider;
        this.desktop = desktop;

        LOGGER.info("Start controller.");

        // check if the booster table view is already opened
        String searchKey = DockKeys.DMX_SCENERY_VIEW;
        LOGGER.info("Search for view with key: {}", searchKey);
        Dockable view = desktop.getContext().getDockableByKey(searchKey);
        if (view != null) {
            LOGGER.info("Select the existing DMX scenery view.");
            DockUtils.selectWindow(view);
            return;
        }

        final String defaultFilename = prepareSceneryFilename(this.node);

        final WizardSettingsInterface wizardSettings = settingsService.getWizardSettings();
        String storedDmxSceneryFileName = wizardSettings.getWorkingDirectory(FILENAME_DMX_SCENERY_KEY);
        if (StringUtils.isNotBlank(storedDmxSceneryFileName)) {
            LOGGER.info("Use stored location for DMX scenery files: {}", storedDmxSceneryFileName);

            try {
                final File storedFile = new File(storedDmxSceneryFileName);
                if (storedFile.getParentFile().exists()) {
                    this.sceneryFilename = new File(storedFile.getParentFile(), defaultFilename).getPath();
                    LOGGER.info("Prepared the default DMX scenery file: {}", this.sceneryFilename);
                }
            }
            catch (Exception ex) {
                LOGGER.warn("Get the dmx scenery file location from stored scenery filename failed.", ex);
            }
        }

        if (StringUtils.isBlank(this.sceneryFilename)) {
            LOGGER.info("Create the defautl DMX scenery file location.");
            String storedWorkingDirectory = wizardSettings.getWorkingDirectory(WORKING_DIR_DMX_SCENERY_KEY);
            if (StringUtils.isBlank(storedWorkingDirectory)) {
                // create default location
                final String defaultConfigurationDirectory = settingsService.getMiscSettings().getBidibConfigDir();
                final File location = new File(defaultConfigurationDirectory, "config");
                if (!location.exists()) {
                    try {
                        FileUtils.forceMkdir(location);
                    }
                    catch (Exception ex) {
                        LOGGER.warn("Create directory for DMX scenery files failed.", ex);
                    }
                }
                storedWorkingDirectory = new File(location, defaultFilename).getPath();
                LOGGER.info("Prepared the location for DMX scenery files: {}", storedWorkingDirectory);

            }
            else {
                storedWorkingDirectory = new File(storedWorkingDirectory, defaultFilename).getPath();
                LOGGER.info("Prepared the location for DMX scenery files: {}", storedWorkingDirectory);
            }
            this.sceneryFilename = storedWorkingDirectory;
        }

        createDmxSceneryView();
    }

    private Dockable createDmxSceneryView() {

        LOGGER.info("Create the DMX scenery view.");

        this.dmxToolBarProvider = new DmxToolBarProvider(desktop, this);
        this.dmxToolBarProvider.createToolBar();

        this.dmxSceneryView =
            new DmxSceneryView(desktop, this.node, this.dmxSceneryModel, this.settingsService, this.wizardLabelWrapper,
                this, dmxToolBarProvider);
        dmxSceneryView.createPanel();

        // add the view below the nodelist panel
        final Dockable nodeListPanel = desktop.getContext().getDockableByKey(DockKeys.NODE_LIST_PANEL);
        desktop.split(nodeListPanel, dmxSceneryView, DockingConstants.SPLIT_BOTTOM, 0.2);

        // create the nodeList listener
        this.nodeListListener = new DefaultNodeListListener() {

            @Override
            public void listNodeAdded(final NodeInterface node) {
                LOGGER.info("The nodelist has a new node: {}", node);
                nodeNew(node);
            }

            @Override
            public void listNodeRemoved(final NodeInterface node) {
                LOGGER.info("The nodelist has a node removed: {}", node);
                nodeRemoved(node);
            }
        };
        // register as nodeList listener at the main model
        nodeListProvider.addNodeListListener(nodeListListener);

        this.dockableStateChangeListener = new DockableStateChangeListener() {

            @Override
            public void dockableStateChanged(DockableStateChangeEvent event) {
                LOGGER.info("State of dockable has changed, event: {}", event);

                if (event.getNewState().getDockable().equals(dmxSceneryView) && event.getNewState().isClosed()) {
                    LOGGER.info("DmxSceneryView was closed, free resources.");

                    try {
                        desktop.removeDockableStateChangeListener(dockableStateChangeListener);
                    }
                    catch (Exception ex) {
                        LOGGER
                            .warn("Remove dockableStateChangeListener from desktop failed: "
                                + dockableStateChangeListener, ex);
                    }
                    finally {
                        dockableStateChangeListener = null;
                    }

                    cleanup();

                    // release the view instance
                    dmxSceneryView = null;
                }
            }
        };
        desktop.addDockableStateChangeListener(this.dockableStateChangeListener);

        // read the initial CV values from the tools section
        readAllCvValuesFromNode();

        return dmxSceneryView;
    }

    private void cleanup() {
        LOGGER.info("Cleanup and free resources.");

        try {
            // remove node listener from communication factory
            if (nodeListListener != null) {
                DmxModelerController.this.nodeListProvider.removeNodeListListener(nodeListListener);
            }
        }
        catch (Exception ex) {
            LOGGER.warn("Unregister controller as node listener failed.", ex);
        }

        // close the DMX dimmer config views
        closeDmxDimmerConfigViews(dmxSceneryModel);

        if (DmxModelerController.this.dmxToolBarProvider != null) {
            DmxModelerController.this.dmxToolBarProvider.removeView(dmxSceneryView);
        }

        if (dmxSceneryView != null) {
            dmxSceneryView.cleanup();
        }

    }

    @Override
    public void transmit(final ProgressStatusCallback callback) {
        LOGGER.info("Transmit the data.");

        try {
            if (callback != null) {
                callback.statusChanged(10);
            }

            readToolsDataCvValues(callback);
        }
        finally {
            if (callback != null) {
                callback.transferFinished();
            }
        }
    }

    @Override
    public void loadScenery(String fileName) {
        LOGGER.info("Load scenery from file: {}", fileName);

        final DmxSceneriesType dmxSceneriesType = DmxSceneriesExchange.builder().loadDmxSceneries(new File(fileName));

        final Map configVariables = node.getConfigVariables();
        final List configVariablesToWrite = new LinkedList<>();

        if (dmxSceneriesType != null && CollectionUtils.isNotEmpty(dmxSceneriesType.getDmxNode())) {

            // TODO support multiple dmx nodes
            if (CollectionUtils.isNotEmpty(dmxSceneriesType.getDmxNode().get(0).getDmxScenery())) {

                final DmxSceneryType dmxSceneryType = dmxSceneriesType.getDmxNode().get(0).getDmxScenery().get(0);

                final DmxScenery dmxScenery =
                    new DmxScenery(dmxSceneryType.getSceneryName())
                        .withDmxScenery(dmxSceneryModel, dmxSceneryType, configVariables,
                            dmxDimmers -> checkForUnmappedChannels(dmxDimmers, configVariablesToWrite),
                            dmxChannelValue -> {

                                Integer value = dmxChannelValue.getNewValue();
                                if (value != null) {
                                    String cvNum = dmxChannelValue.getConfigVar().getName();
                                    configVariablesToWrite
                                        .add(ConfigurationVariable.from(cvNum, ByteUtils.getIntLowByteValue(value)));

                                    LOGGER.info("Add new value to write for dmxChannelValue: {}", dmxChannelValue);
                                }
                                else {
                                    LOGGER.info("No new value to write for dmxChannelValue: {}", dmxChannelValue);
                                }
                            });

                LOGGER.info("Add new DMX scenery: {}", dmxScenery);

                // checkForUnmappedChannels(dmxScenery.getDmxDimmers(), configVariablesToWrite);

                final DmxScenery existingScenery =
                    IterableUtils.find(dmxSceneryModel.getSceneries(), new Predicate() {

                        @Override
                        public boolean evaluate(DmxScenery currentScenery) {
                            if (currentScenery.getName().equals(dmxScenery.getName())) {
                                LOGGER.info("Found scenery to replace: {}", currentScenery);
                                return true;
                            }
                            return false;
                        }
                    });
                if (existingScenery != null) {
                    // remove existing scenery
                    dmxSceneryModel.removeScenery(existingScenery);
                }

                dmxSceneryModel.addScenery(dmxScenery);

                // add the time values
                if (dmxSceneriesType.getDmxNode().get(0).getDmxTimes() != null) {
                    if (CollectionUtils.isNotEmpty(dmxSceneriesType.getDmxNode().get(0).getDmxTimes().getDmxTime())) {

                        final List dmxTimes =
                            dmxSceneriesType.getDmxNode().get(0).getDmxTimes().getDmxTime();
                        for (DmxTimeType dmxTimeType : dmxTimes) {
                            int cvNumLow = dmxTimeType.getCvNumberLow();
                            int cvNumHigh = dmxTimeType.getCvNumberHigh();
                            int timeOffset = dmxTimeType.getTimeOffset();

                            int[] ticks = calculateTimeTicks(timeOffset);
                            // TODO

                            ConfigurationVariable timeTickCv = from(configVariables, cvNumLow);
                            if (timeTickCv.setValueIfDifferent(Integer.toString(ticks[1]))) {
                                configVariablesToWrite.add(timeTickCv);
                            }
                            timeTickCv = from(configVariables, cvNumHigh);
                            if (timeTickCv.setValueIfDifferent(Integer.toString(ticks[0]))) {
                                configVariablesToWrite.add(timeTickCv);
                            }
                        }
                    }
                }

                // TODO process the dmxScenery

                if (!configVariablesToWrite.isEmpty()) {
                    LOGGER.info("Write changed scenery CV variables to node.");
                    writeCvValues(this.node, configVariablesToWrite);
                }
                else {
                    LOGGER.info("No changed scenery CV variables found to write on node.");
                }

                // this.dmxSceneryModel.addScenery(dmxScenery);

                // reset the dirty flag
                this.dmxSceneryModel.setHasPendingChanges(false);
            }
        }
    }

    @Override
    public void saveScenery(final String fileName, final DmxScenery dmxScenery) {

        final DmxSceneriesType dmxSceneriesType = new DmxSceneriesType();
        final NodeInterface node = dmxSceneryModel.getNode();

        // dmx node
        final DmxNodeType dmxNode =
            new DmxNodeType()
                .withId(ByteUtils.formatHexUniqueId(node.getUniqueId()))
                .withNodeName(StringUtils.isNotBlank(node.getLabel()) ? node.getLabel()
                    : ByteUtils.formatHexUniqueId(node.getUniqueId()))
                .withDmxTimes(new DmxTimesType());
        dmxSceneriesType.getDmxNode().add(dmxNode);

        final Map configVariables = node.getConfigVariables();
        // add the time values
        for (int timeIndex = 0; timeIndex < DmxSceneryModel.TOTAL_AVAILABLE_TICKS; timeIndex++) {

            int cvNumLow = DmxSceneryModel.TICKS_CV_START + (timeIndex * 2);
            int cvNumHigh = DmxSceneryModel.TICKS_CV_START + 1 + (timeIndex * 2);
            Optional ticksLowValue = optFrom(configVariables, cvNumLow);
            Optional ticksHighValue = optFrom(configVariables, cvNumHigh);

            int time = calculateTime(ticksHighValue, ticksLowValue);

            DmxTimeType dmxTime =
                new DmxTimeType().withCvNumberLow(cvNumLow).withCvNumberHigh(cvNumHigh).withTimeOffset(time);
            dmxNode.getDmxTimes().getDmxTime().add(dmxTime);
        }

        for (DmxScenery currentDmxScenery : dmxSceneryModel.getSceneries()) {

            if (currentDmxScenery.getName().equals(dmxScenery.getName())) {

                LOGGER.info("Store points of scenery: {}", currentDmxScenery);

                final DmxSceneryType dmxSceneryType =
                    currentDmxScenery.fromDmxScenery(dmxDimmer -> getCvValuesForDimmer(dmxDimmer));
                dmxNode.getDmxScenery().add(dmxSceneryType);

                break;
            }
        }

        if (CollectionUtils.isNotEmpty(dmxSceneriesType.getDmxNode())) {

            DmxSceneriesExchange.builder().saveDmxSceneries(dmxSceneriesType, fileName);

            final WizardSettingsInterface wizardSettings = settingsService.getWizardSettings();
            wizardSettings.setWorkingDirectory(FILENAME_DMX_SCENERY_KEY, fileName);
        }
    }

    private void closeDmxDimmerConfigViews(final DmxSceneryModel dmxSceneryModel) {
        // check if DMX modeler views are opened
        for (DmxScenery dmxScenery : dmxSceneryModel.getSceneries()) {
            for (DmxDimmer dmxDimmer : dmxScenery.getDmxDimmers()) {
                String searchKey = DmxDimmerConfigView.prepareKey(dmxDimmer);
                LOGGER.info("Search for view with key: {}", searchKey);
                Dockable dmxDimmerConfigView = desktop.getContext().getDockableByKey(searchKey);
                if (dmxDimmerConfigView != null) {
                    LOGGER.info("Close the dmxDimmerConfigView: {}", dmxDimmerConfigView);

                    desktop.close(dmxDimmerConfigView);
                }
            }
        }
    }

    private void nodeNew(final NodeInterface node) {

    }

    private void cleanupSelectedNode(final NodeListListener nodeListListener) {

        for (DmxDimmer dmxDimmer : dmxSceneryModel.getDimmers()) {
            // check if the dimmer is opened
            String searchKey = DmxDimmerConfigView.prepareKey(dmxDimmer);
            LOGGER.info("Search for view with key: {}", searchKey);
            Dockable dmxDimmerConfigViewDockable = desktop.getContext().getDockableByKey(searchKey);
            if (dmxDimmerConfigViewDockable != null && dmxDimmerConfigViewDockable instanceof DmxDimmerConfigView) {
                LOGGER.info("Close the dmxDimmerConfigView: {}", dmxDimmerConfigViewDockable);
                desktop.close(dmxDimmerConfigViewDockable);
            }
        }

        // check if we must close the scenery panel
        Dockable dmxSceneryDockable = desktop.getContext().getDockableByKey(DockKeys.DMX_SCENERY_VIEW);
        if (dmxSceneryDockable != null) {
            LOGGER.info("Close the DMX scenery panel: {}", dmxSceneryDockable);

            desktop.close(dmxSceneryDockable);
        }
    }

    private void nodeRemoved(final NodeInterface node) {

        if (this.node != null && this.node.equals(node)) {
            LOGGER.info("The selected node was removed from the node list.");

            cleanupSelectedNode(this.nodeListListener);
        }
    }

    @Override
    public void openDmxDimmerConfigView(final DmxDimmer dmxDimmer) {

        // check if the channels of the dimmer are configured
        final List unmappedDmxChannels = new ArrayList<>();
        for (DmxChannel dmxChannel : dmxDimmer.getDmxChannels()) {

            Integer dmxChannelId = dmxChannel.getDmxChannelWrapper().getDmxChannelId();
            final Predicate missing =
                (channelId) -> this.dmxSceneryModel
                    .getConfiguredDmxChannelsMap().keySet().stream()
                    .filter(dcw -> Objects.equals(channelId, dcw.getDmxChannelId())).findFirst().isEmpty();
            if (dmxChannelId == null || missing.evaluate(dmxChannelId)) {
                LOGGER.info("Invalid DMX channel configuration detected for dmxChannelId: {}", dmxChannelId);
                unmappedDmxChannels.add(dmxChannelId);
            }
        }
        if (!unmappedDmxChannels.isEmpty()) {

            TaskDialogs
                .build(JOptionPane.getFrameForComponent(this.parent),
                    Resources.getString(DmxModelerController.class, "unmapped-dmx-channel.instruction"),
                    Resources
                        .getString(DmxModelerController.class, "unmapped-dmx-channel.text",
                            unmappedDmxChannels.toString()))
                .title(Resources.getString(DmxModelerController.class, "unmapped-dmx-channel.title")).inform();

            return;
        }

        // check if the dimmer config is already opened
        String searchKey = DmxDimmerConfigView.prepareKey(dmxDimmer);
        LOGGER.info("Search for view with key: {}", searchKey);
        Dockable dmxDimmerConfigView = desktop.getContext().getDockableByKey(searchKey);
        if (dmxDimmerConfigView == null) {
            LOGGER.info("Create new DmxDimmerConfigView.");

            // Create the DMX modeler view for the scenery
            final DmxDimmerConfigView view =
                new DmxDimmerConfigView(this.desktop, this, dmxDimmer, this.dmxSceneryModel, this.nodeProviderSupplier,
                    dmxToolBarProvider);

            Dockable tabPanel = desktop.getContext().getDockableByKey(DockKeys.TAB_PANEL);
            if (tabPanel != null) {
                desktop.createTab(tabPanel, view, 1, true);
            }
            else {
                desktop.addDockable(view, RelativeDockablePosition.RIGHT);
            }
        }
        else {
            LOGGER.info("Select the existing dmxDimmer config view.");
            DockUtils.selectWindow(dmxDimmerConfigView);
        }
    }

    @Override
    public void writeCvValues(final DmxDataRow dmxDataRow, final Runnable finishedCallback) {
        LOGGER.info("Write CV values of dmxDataRow: {}", dmxDataRow);

        final List configVariablesToWrite = new LinkedList<>();

        prepareBrightnessCvToWrite(configVariablesToWrite, dmxDataRow);
        writeCvValues(this.node, configVariablesToWrite);

        // reset the dirty flag
        dmxDataRow.setDirty(false);

        if (finishedCallback != null) {
            finishedCallback.run();
        }
    }

    @Override
    public void writeCvValues(final List dmxDataRows, final Runnable finishedCallback) {
        LOGGER.info("Write CV values of dmxDataRows: {}", dmxDataRows);

        final List configVariablesToWrite = new LinkedList<>();

        for (DmxDataRow dmxDataRow : dmxDataRows) {
            prepareBrightnessCvToWrite(configVariablesToWrite, dmxDataRow);
        }

        if (CollectionUtils.isNotEmpty(configVariablesToWrite)) {
            writeCvValues(this.node, configVariablesToWrite);
            for (DmxDataRow dmxDataRow : dmxDataRows) {
                // reset the dirty flag
                dmxDataRow.setDirty(false);
            }

            if (finishedCallback != null) {
                finishedCallback.run();
            }
        }
        else {
            LOGGER.info("No changed CV variables to write available.");
        }

    }

    @Override
    public void writeBrightnessCvValues(final NodeInterface node, final List changedDmxChannels) {

        final List configVariablesToWrite = new ArrayList<>();

        for (DmxChannelWrapper dmxChannelWrapper : changedDmxChannels) {

            final List dmxBrightnessValues =
                this.dmxSceneryModel.getConfiguredDmxChannelsMap().get(dmxChannelWrapper);
            for (DmxChannelValue dmxChannelValue : dmxBrightnessValues) {

                if (dmxChannelValue.getConfigVar().setValueIfDifferent(String.valueOf(dmxChannelValue.getNewValue()))) {
                    configVariablesToWrite.add(dmxChannelValue.getConfigVar());
                }
            }
        }

        LOGGER.info("Write CV values to node: {}", configVariablesToWrite);
        // store the changed values
        writeCvValues(this.dmxSceneryModel.getNode(), configVariablesToWrite);
    }

    private void writeCvValues(final NodeInterface node, final List configVariablesToWrite) {

        if (CollectionUtils.isNotEmpty(configVariablesToWrite)) {

            // write the CV to the node
            final List configVariablesWritten =
                this.nodeService
                    .setConfigVariables(ConnectionRegistry.CONNECTION_ID_MAIN, node, configVariablesToWrite);

            LOGGER.info("Update model with configuration variables: {}", configVariablesWritten);
            node.updateConfigVariableValues(configVariablesWritten, true);

        }
        else {
            LOGGER.warn("No configuration variables to write available.");

            throw new InvalidDataException("Action failed.");
        }
    }

    private void prepareBrightnessCvToWrite(
        final List configVariablesToWrite, final DmxDataRow dmxDataRow) {
        LOGGER.info("Prepare the CV write, dmxDataRow: {}", dmxDataRow);

        if (!dmxDataRow.isRowValid()) {
            LOGGER.warn("The dmxDataRow is not valid: {}", dmxDataRow);
            throw new InvalidDataException("The dmxDataRow is not valid.");
        }

        // DMX channel

        // brightness values
        if (dmxDataRow.getDataIndex() instanceof TimeDataIndex) {

            // prepare the CV values

            for (DmxChannelValue dmxChannelValue : dmxDataRow.getCvValues()) {
                Integer value = dmxChannelValue.getNewValue();
                if (value != null) {
                    String cvNum = dmxChannelValue.getConfigVar().getName();
                    configVariablesToWrite.add(ConfigurationVariable.from(cvNum, ByteUtils.getIntLowByteValue(value)));

                }
                else {
                    LOGGER.debug("No new value for dmxChannelValue: {}", dmxChannelValue);
                }
            }

            LOGGER.debug("Prepared CVs: {}", configVariablesToWrite);
        }

    }

    @Override
    public void activateTime(final LocalDateTime time) {
        // TODO make acceleration configurable
        int acceleration = 10;
        LOGGER.info("Activate time, time: {}, acceleration: {}", time, acceleration);

        try {
            DefaultBusyFrame.setWaitCursor(parent);

            nodeService.clock(ConnectionRegistry.CONNECTION_ID_MAIN, node, time, acceleration);
        }
        finally {
            DefaultBusyFrame.setDefaultCursor(parent);
        }
    }

    private void prepareReadTicksCVs(
        final Map configVariables,
        final List configVariablesToRead) {

        // prepare the config variables to read
        for (int timeIndex = 0; timeIndex < DmxSceneryModel.TOTAL_AVAILABLE_TICKS; timeIndex++) {

            // read the time ticks
            ConfigurationVariable cv = from(configVariables, DmxSceneryModel.TICKS_CV_START + (timeIndex * 2));
            if (cv.getValue() == null) {
                // set the timeout marker
                cv.setTimeout(true);

                configVariablesToRead.add(cv);
            }
            cv = from(configVariables, DmxSceneryModel.TICKS_CV_START + 1 + (timeIndex * 2));
            if (cv.getValue() == null) {
                // set the timeout marker
                cv.setTimeout(true);

                configVariablesToRead.add(cv);
            }
        }
    }

    private void prepareReadAllConfiguredBrightnessCVs(
        final Map configVariables,
        final List configVariablesToRead, boolean v2Firmware) {

        // check if the DMX channel is configured
        for (int dmxChannelIndex = 0; dmxChannelIndex < DmxSceneryModel.TOTAL_AVAILABLE_CHANNELS; dmxChannelIndex++) {

            int cvNumChannelId =
                (v2Firmware ? DmxSceneryModel.FIRST_DMX_CHANNEL_CV_START_V2
                    : DmxSceneryModel.FIRST_DMX_CHANNEL_CV_START)
                    + (dmxChannelIndex * DmxSceneryModel.DMX_CHANNEL_CV_GAP);
            ConfigurationVariable cv = from(configVariables, cvNumChannelId);
            if (StringUtils.isNotBlank(cv.getValue())) {

                try {
                    int dmxChannelId = Integer.parseInt(cv.getValue());
                    if (dmxChannelId > 0) {
                        LOGGER.info("Found configured dmx channel: {}", cv);

                        prepareReadBrightnessCVs(configVariables, configVariablesToRead, cvNumChannelId);
                    }
                }
                catch (Exception ex) {
                    LOGGER.warn("Check if dmx channel is configured failed for cv: {}", cv, ex);
                }
            }
        }
    }

    /**
     * Prepare all CV of the brightness values of the dmx channel.
     * 
     * @param configVariables
     *            the current config variables
     * @param configVariablesToRead
     *            the list of CV to read
     * @param cvNumChannelId
     *            the CV number of the DMX channel id (this is the first CV in the block)
     */
    private void prepareReadBrightnessCVs(
        final Map configVariables,
        final List configVariablesToRead, int cvNumChannelId) {

        // read all brightness values for this dmx channel
        for (int brightnessValueIndex =
            2; brightnessValueIndex < DmxSceneryModel.DMX_CHANNEL_CV_GAP; brightnessValueIndex++) {
            int cvNumBrightness = cvNumChannelId + brightnessValueIndex;
            LOGGER.debug("Current cvNumBrightness: {}", cvNumBrightness);
            ConfigurationVariable cvBrightness = from(configVariables, cvNumBrightness);
            if (cvBrightness.getValue() == null) {

                // set the timeout marker
                cvBrightness.setTimeout(true);

                configVariablesToRead.add(cvBrightness);
            }
        }
    }

    @Override
    public List getCvValuesForDimmer(final DmxDimmer dmxDimmer) {
        LOGGER.info("Get the CV values for dmxDimmer: {}", dmxDimmer.getName());

        // get the configured dmx channels of the dimmer
        int numDmxChannels = dmxDimmer.getDmxChannels().size();
        LOGGER.info("Configured DMX channels on this dimmer: {}", numDmxChannels);

        final Integer[] dmxChannelsOfDimmer = new Integer[numDmxChannels];
        for (int dmxChannelId = 0; dmxChannelId < numDmxChannels; dmxChannelId++) {
            dmxChannelsOfDimmer[dmxChannelId] =
                dmxDimmer.getDmxChannels().get(dmxChannelId).getDmxChannelWrapper().getDmxChannelId();
            LOGGER.debug("Set DMX channel Id at index: {}, value: {}", dmxChannelId, dmxChannelsOfDimmer[dmxChannelId]);
        }

        // Update the dmxDataRows with the values read

        final Map configVariables = node.getConfigVariables();

        // prepare the mapping for dmx channel id to the cv number where it is stored for the selected dimmer
        final Map dmxChannelToCvNumMap = new HashMap<>();
        for (int dmxChannelIndex = 0; dmxChannelIndex < numDmxChannels; dmxChannelIndex++) {
            Integer dmxChannelValue = dmxChannelsOfDimmer[dmxChannelIndex];
            LOGGER.debug("Current dmxChannelValue: {}", dmxChannelValue);

            // we must search the CV number of the dmxChannel

            Integer cvNumber =
                this.dmxSceneryModel
                    .getConfiguredDmxChannelsMap().keySet().stream()
                    .filter(dcw -> dcw.getDmxChannelId() == dmxChannelValue).map(dcw -> dcw.getCvNumber()).findFirst()
                    .orElse(null);
            if (cvNumber != null) {
                dmxChannelToCvNumMap.put(dmxChannelValue, cvNumber);
            }
            else {
                LOGGER.warn("No configured CV number available for dmxChannelValue: {}", dmxChannelValue);
            }
        }

        final List dmxDataRows =
            prepareDmxDataRowWithBrightnessValues(configVariables, dmxChannelsOfDimmer, dmxChannelToCvNumMap);

        // store the data rows
        this.dmxSceneryModel.addDmxDataRows(dmxDimmer, dmxDataRows);

        return dmxDataRows;
    }

    private int calculateTime(Optional ticksHighValue, Optional ticksLowValue) {
        int hours = ticksHighValue.map(val -> val * 100).orElse(0);
        int minutes = ticksLowValue.map(val -> val).orElse(0);
        int time = hours + minutes;

        return time;
    }

    private int[] calculateTimeTicks(int timeValue) {
        int hours = timeValue / 100;
        int minutes = timeValue % 100;

        return new int[] { hours, minutes };
    }

    /**
     * Prepare the brightness values of the provided dmx channels from the cached values.
     * 
     * @param updatedConfigVariables
     *            the configuration variables to read the values from
     * @param dmxChannelsOfDimmer
     *            the dmx channels
     * @param dmxChannelToCvNumMap
     *            the map of dmx channel id to cv number
     * @return the list of DmxDataRow instances
     */
    private List prepareDmxDataRowWithBrightnessValues(
        final Map updatedConfigVariables, final Integer[] dmxChannelsOfDimmer,
        final Map dmxChannelToCvNumMap) {

        // process the config variables
        // load the configured time stamps
        final List dmxDataRows = new ArrayList<>();

        for (int timeIndex = 0; timeIndex < DmxSceneryModel.TOTAL_AVAILABLE_TICKS; timeIndex++) {

            Optional ticksLowValue =
                optFrom(updatedConfigVariables, DmxSceneryModel.TICKS_CV_START + (timeIndex * 2));
            Optional ticksHighValue =
                optFrom(updatedConfigVariables, DmxSceneryModel.TICKS_CV_START + 1 + (timeIndex * 2));

            int time = calculateTime(ticksHighValue, ticksLowValue);

            final DmxDataRow dmxDataRow =
                new DmxDataRow(new TimeDataIndex(timeIndex, time, LocalTime.of(time / 60, time % 60)));

            int numDmxChannels = dmxChannelsOfDimmer.length;

            // get the brightness values for the configured channels
            for (int dmxChannelIndex = 0; dmxChannelIndex < numDmxChannels; dmxChannelIndex++) {

                // TODO if the DMX channel is not configured in the DMX channel brightness values we have a problem

                Integer dmxChannelValue = dmxChannelsOfDimmer[dmxChannelIndex];
                LOGGER.debug("Current dmxChannelValue: {}", dmxChannelValue);

                final List existingBrightnessValues =
                    this.dmxSceneryModel
                        .getConfiguredDmxChannelsMap().entrySet().stream()
                        .filter(entry -> Objects.equals(dmxChannelValue, entry.getKey().getDmxChannelId())).findFirst()
                        .map(entry -> entry.getValue()).orElse(null);

                if (existingBrightnessValues != null) {
                    // we must search the CV number of the dmxChannel because this is the first cv of this dmx channel
                    Integer cvNumber = dmxChannelToCvNumMap.get(dmxChannelValue);

                    // calculate the cv number of the brightness of the current time index of the dmx channel
                    int cvNum = timeIndex + cvNumber + 2;
                    final String cvName = Integer.toString(cvNum);
                    LOGGER.debug("Current cvNumber: {}, cvNum: {}, cvName: {}", cvNumber, cvNum, cvName);

                    DmxChannelValue existing =
                        IterableUtils
                            .find(existingBrightnessValues,
                                val -> Objects.equals(val.getConfigVar().getName(), cvName));
                    if (existing == null) {
                        final DmxChannelValue newValue = new DmxChannelValue(from(updatedConfigVariables, cvNum));
                        dmxDataRow.addCvValue(newValue);

                        LOGGER.info("Add new dmxChannelValue: {}", newValue);

                        existingBrightnessValues.add(newValue);
                    }
                    else {
                        LOGGER.info("Add existing dmxChannelValue: {}", existing);
                        dmxDataRow.addCvValue(existing);
                    }

                }
                else {
                    LOGGER.warn("No brightness values found for dmxChannelValue: {}", dmxChannelValue);
                }
            }

            dmxDataRows.add(dmxDataRow);
        }

        return dmxDataRows;
    }

    private static ConfigurationVariable from(final Map configVariables, int cvNumber) {
        return configVariables.get(Integer.toString(cvNumber));
    }

    private static Optional optFrom(final Map configVariables, int cvNumber) {
        return Optional
            .ofNullable(configVariables.get(Integer.toString(cvNumber))).filter(cv -> cv.getValue() != null)
            .map(cv -> (cv.getValue().isBlank() ? null : Integer.valueOf(cv.getValue())));
    }

    private boolean isV2Firmware() {
        boolean v2Firmware = node.getNode().getSoftwareVersion().isHigherOrEqualThan(SoftwareVersion.build(2, 0, 0));
        return v2Firmware;
    }

    protected void readToolsDataCvValues(final ProgressStatusCallback callback) {

        boolean v2Firmware = isV2Firmware();

        LOGGER
            .info("Read the toolsdata CV values. Current FIRST_DMX_CHANNEL_CV_START: {}, DMX_CHANNEL_CV_GAP: {}",
                v2Firmware ? DmxSceneryModel.FIRST_DMX_CHANNEL_CV_START_V2 : DmxSceneryModel.FIRST_DMX_CHANNEL_CV_START,
                DmxSceneryModel.DMX_CHANNEL_CV_GAP);

        final List configVariablesToRead = new ArrayList<>();

        final Map configVariables = node.getConfigVariables();

        // tools data starts from CV 1710 -> number of configured dimmers
        ConfigurationVariable cv = from(configVariables, DmxSceneryModel.CV_CONFIGURED_DIMMERS);
        if (cv != null && cv.getValue() == null) {
            configVariablesToRead.add(cv);
        }

        // read CVs of all possible configured DMX channels
        for (int index = 0; index < DmxSceneryModel.TOTAL_AVAILABLE_CHANNELS; index++) {
            cv =
                from(configVariables, (v2Firmware ? DmxSceneryModel.FIRST_DMX_CHANNEL_CV_START_V2
                    : DmxSceneryModel.FIRST_DMX_CHANNEL_CV_START) + (index * DmxSceneryModel.DMX_CHANNEL_CV_GAP));
            if (cv.getValue() == null) {
                configVariablesToRead.add(cv);
            }
        }

        // read the configured channels of the configured dimmers
        for (int index = 0; index < DmxSceneryModel.MAX_CONFIGURED_DIMMERS; index++) {
            cv =
                from(configVariables,
                    DmxSceneryModel.CV_CONFIGURED_DIMMER_0_CHANNELS + (index * DmxSceneryModel.DIMMER_GAP));
            if (cv.getValue() == null) {
                configVariablesToRead.add(cv);
            }
            for (int channel = 0; channel < DmxSceneryModel.MAX_CONFIGURED_CHANNELS; channel++) {

                // assigned dmx channel color
                int cvNum =
                    DmxSceneryModel.CV_CONFIGURED_DIMMER_0_CHANNEL_COLORS_START + (channel * 2)
                        + index * DmxSceneryModel.DIMMER_GAP;
                // LOGGER.info("Get CV with num: {}", cvNum);
                cv = from(configVariables, cvNum);
                if (cv != null) {
                    if (cv.getValue() == null) {
                        configVariablesToRead.add(cv);
                    }
                }
                else {
                    LOGGER.warn("No configured CV available to get the dmx channel color with cvNum: {}", cvNum);
                }

                // assigned dmx channel number
                cvNum =
                    DmxSceneryModel.CV_CONFIGURED_DIMMER_0_CHANNEL_COLORS_START + (channel * 2)
                        + index * DmxSceneryModel.DIMMER_GAP + 1;
                cv = from(configVariables, cvNum);
                if (cv != null) {
                    if (cv.getValue() == null) {
                        configVariablesToRead.add(cv);
                    }
                }
                else {
                    LOGGER.warn("No configured CV available to get the dmx channel number with cvNum: {}", cvNum);
                }
            }
        }

        // prepare the ticks CVs to read from the node
        prepareReadTicksCVs(configVariables, configVariablesToRead);

        LOGGER.debug("Prepared CVs: {}", configVariablesToRead);

        if (callback != null) {
            callback.messageChanged("Read CVs from node: " + configVariablesToRead.size());
        }

        // read configured dmx channels and ticks CVs from the node
        readCvFromNode(configVariablesToRead);

        // remove all already read data
        configVariablesToRead.clear();

        // read all brightness CVs for the configured DMX channels
        prepareReadAllConfiguredBrightnessCVs(configVariables, configVariablesToRead, v2Firmware);

        LOGGER.debug("Prepared brightness CVs to read: {}", configVariablesToRead);

        if (callback != null) {
            callback.messageChanged("Read CVs from node: " + configVariablesToRead.size());

            callback.statusChanged(30);
        }

        readCvFromNode(configVariablesToRead);

        // Update the dmxDataRows with the values read

        final Map updatedConfigVariables = node.getConfigVariables();

        // find all configured dmx channel of the node
        for (int dmxChannelIndex = 0; dmxChannelIndex < DmxSceneryModel.TOTAL_AVAILABLE_CHANNELS; dmxChannelIndex++) {

            int cvNum =
                (v2Firmware ? DmxSceneryModel.FIRST_DMX_CHANNEL_CV_START_V2
                    : DmxSceneryModel.FIRST_DMX_CHANNEL_CV_START)
                    + (dmxChannelIndex * DmxSceneryModel.DMX_CHANNEL_CV_GAP);
            Integer dmxChannelValue = optFrom(updatedConfigVariables, cvNum).orElse(null);

            // keep the configured channels in the map
            if (dmxChannelValue != null && dmxChannelValue.intValue() > 0) {

                Optional existing = getConfiguredDmxChannelWrapper(dmxChannelValue);
                if (existing.isEmpty()) {
                    LOGGER.info("Register the DMX channel: {}", dmxChannelValue);

                    this.dmxSceneryModel
                        .getConfiguredDmxChannelsMap()
                        .put(new DmxChannelWrapper(dmxChannelValue, cvNum), new ArrayList<>());
                }
            }
            else {
                LOGGER.warn("No dmxChannelValue for cvNum: {}", cvNum);
                this.dmxSceneryModel
                    .getConfiguredDmxChannelsMap()
                    .put(new DmxChannelWrapper(dmxChannelValue, cvNum), new ArrayList<>());
            }
        }

        // check if we have a stored scenery available and take the names from there
        Optional defaultScenery = Optional.empty();

        if (StringUtils.isBlank(this.sceneryFilename)) {

            final WizardSettingsInterface wizardSettings = settingsService.getWizardSettings();
            String storedDmxSceneryFileName = wizardSettings.getWorkingDirectory(FILENAME_DMX_SCENERY_KEY);
            if (StringUtils.isNotBlank(storedDmxSceneryFileName)) {
                LOGGER.info("Use stored location for DMX scenery files: {}", storedDmxSceneryFileName);
                this.sceneryFilename = storedDmxSceneryFileName;
            }
            else {
                LOGGER.info("No stored default scenery file available.");
                this.sceneryFilename = prepareSceneryFilename(this.node);
            }
        }

        try {

            final File sceneryFile = new File(this.sceneryFilename);
            if (sceneryFile.exists()) {
                LOGGER.info("Found scenery file: {}", this.sceneryFilename);

                final DmxSceneriesType dmxSceneriesType = DmxSceneriesExchange.builder().loadDmxSceneries(sceneryFile);
                if (dmxSceneriesType != null) {
                    if (CollectionUtils.isNotEmpty(dmxSceneriesType.getDmxNode())) {

                        // TODO support more than a single dmx node
                        if (CollectionUtils.isNotEmpty(dmxSceneriesType.getDmxNode().get(0).getDmxScenery())) {
                            final DmxSceneryType dmxSceneryType =
                                dmxSceneriesType.getDmxNode().get(0).getDmxScenery().get(0);
                            defaultScenery =
                                Optional
                                    .ofNullable(DmxScenery
                                        .fromDmxSceneryType(dmxSceneryType,
                                            dmxChannelNumber -> getConfiguredDmxChannelWrapper(dmxChannelNumber)
                                                .orElse(null)));

                            LOGGER.info("Found defaultScenery: {}", defaultScenery);
                        }
                    }
                }
            }

        }
        catch (Exception ex) {
            LOGGER.warn("Load stored default scenery failed.", ex);
        }

        // create the DMX scenery
        final DmxScenery dmxScenery = prepareScenery(updatedConfigVariables, defaultScenery, v2Firmware);

        final List configVariablesToWrite = new ArrayList<>();

        checkForUnmappedChannels(dmxScenery.getDmxDimmers(), configVariablesToWrite);

        if (!configVariablesToWrite.isEmpty()) {
            // Write the updated DMX-Channel CV on the node
            writeCvValues(this.node, configVariablesToWrite);
        }

        SwingUtils.executeInEDT(() -> this.dmxSceneryModel.addScenery(dmxScenery));
    }

    private void checkForUnmappedChannels(
        final List dmxDimmers, final List configVariablesToWrite) {

        LOGGER.info("Check for unmapped channels of dimmers: {}", dmxDimmers);

        // check if we have an unmapped DMX channel in the dimmers
        final List mappedChannels = new ArrayList<>();
        final List unmappedChannels = new ArrayList<>();
        for (DmxDimmer dmxDimmer : dmxDimmers) {

            for (DmxChannel dmxChannel : dmxDimmer.getDmxChannels()) {

                Integer channelId = dmxChannel.getDmxChannelWrapper().getDmxChannelId();
                Optional existing =
                    this.dmxSceneryModel
                        .getConfiguredDmxChannelsMap().keySet().stream()
                        .filter(dcw -> Objects.equals(channelId, dcw.getDmxChannelId())).findFirst();
                if (existing.isPresent()) {
                    LOGGER.info("Found mapped channel: {}, dimmer: {}", channelId, dmxDimmer.getName());
                    mappedChannels.add(DmxChannelWrapper.searchKey(channelId));
                }
                else {
                    LOGGER.info("Found unmapped channel: {}, dimmer: {}", channelId, dmxDimmer.getName());
                    unmappedChannels.add(DmxChannelWrapper.searchKey(channelId));
                }
            }
        }

        // check if we have unmapped DMX channels
        if (!unmappedChannels.isEmpty()) {

            for (DmxChannelWrapper unmappedChannelId : unmappedChannels) {
                // find 'free' mapping in CVs
                final List configuredDmxChannelIds =
                    new ArrayList<>(this.dmxSceneryModel.getConfiguredDmxChannelsMap().keySet());

                Collections
                    .sort(configuredDmxChannelIds,
                        (dcw0, dcw1) -> Integer.compare(dcw0.getCvNumber(), dcw1.getCvNumber()));

                for (DmxChannelWrapper currentChannelId : configuredDmxChannelIds) {
                    LOGGER
                        .info("Process currentChannelId: {}, unmappedChannelId: {}", currentChannelId,
                            unmappedChannelId);

                    // check if the current channel id is used already in a dimmer
                    if (mappedChannels
                        .stream().filter(wrapper -> wrapper.getDmxChannelId() == currentChannelId.getDmxChannelId())
                        .findFirst().isEmpty()) {

                        int cvNum = currentChannelId.getCvNumber();

                        LOGGER
                            .info(
                                "Found 'free' mapping to re-use with channelId: {} for unmappedChannelId: {}, cvNum: {}",
                                currentChannelId, unmappedChannelId, cvNum);

                        final List existingChannelValues =
                            this.dmxSceneryModel.getConfiguredDmxChannelsMap().remove(currentChannelId);

                        final DmxChannelWrapper newWrapper =
                            new DmxChannelWrapper(unmappedChannelId.getDmxChannelId(), cvNum);

                        this.dmxSceneryModel.getConfiguredDmxChannelsMap().put(newWrapper, existingChannelValues);

                        mappedChannels.add(newWrapper);

                        // prepare update the DMX-Channel CV on the node
                        configVariablesToWrite
                            .add(ConfigurationVariable
                                .from(cvNum, ByteUtils.getIntLowByteValue(unmappedChannelId.getDmxChannelId())));
                        break;
                    }
                    else {
                        LOGGER.info("Current channel is mapped: {}", currentChannelId);
                    }
                }
            }
        }

    }

    private void readCvFromNode(final List configVariablesToRead) {

        if (CollectionUtils.isNotEmpty(configVariablesToRead)) {

            final List configVariablesRead =
                this.nodeService
                    .queryConfigVariables(ConnectionRegistry.CONNECTION_ID_MAIN, node, configVariablesToRead);

            LOGGER.debug("Update model with configuration variables: {}", configVariablesRead);

            // check if timeout detected
            final List configVariablesTimeoutToRead = new ArrayList<>();
            for (ConfigurationVariable cv : configVariablesRead) {
                if (cv.isTimeout()) {
                    LOGGER.warn("Detected CV with timeout: {}", cv);

                    configVariablesTimeoutToRead.add(cv);
                }
            }

            if (!configVariablesTimeoutToRead.isEmpty()) {
                LOGGER.info("Fetch the timed out cv values once again.");
                final List configVariablesTimeoutRead =
                    this.nodeService
                        .queryConfigVariables(ConnectionRegistry.CONNECTION_ID_MAIN, node,
                            configVariablesTimeoutToRead);
                for (ConfigurationVariable cv : configVariablesTimeoutRead) {
                    if (!cv.isTimeout()) {
                        LOGGER.info("Update the re-read value from cv: {}", cv);
                        configVariablesRead
                            .stream().filter(cvFilter -> Objects.equals(cvFilter.getName(), cv.getName()))
                            .map(cvFilter -> {
                                cvFilter.setValue(cv.getValue());
                                cvFilter.setTimeout(false);
                                return cvFilter;
                            });
                    }
                }
            }

            //
            node.updateConfigVariableValues(configVariablesRead, true);
        }
        else {
            LOGGER.info("No unread CV detected.");
        }

    }

    private DmxScenery prepareScenery(
        final Map updatedConfigVariables, final Optional defaultScenery,
        boolean v2Firmware) {

        // create the DMX scenery
        final DmxScenery dmxScenery =
            new DmxScenery(defaultScenery.map(ds -> ds.getId()).orElse(UUID.randomUUID().toString()));
        // set the name of the scenery
        dmxScenery
            .setName(defaultScenery
                .map(ds -> ds.getName()).orElse(Resources.getString(DmxModelerController.class, "dmx-scenery")));

        Optional configuredDimmersValue =
            optFrom(updatedConfigVariables, DmxSceneryModel.CV_CONFIGURED_DIMMERS);
        int configuredDimmersCount = configuredDimmersValue.orElse(0);
        if (configuredDimmersCount > 0) {

            final Optional> defaultDimmers =
                defaultScenery.filter(ds -> !ds.getDmxDimmers().isEmpty()).map(ds -> ds.getDmxDimmers());

            for (int index = 0; index < configuredDimmersCount; index++) {

                final int listIndex = index;
                Optional defaultDimmer =
                    defaultDimmers.filter(dds -> dds.size() > listIndex).map(dds -> dds.get(listIndex));

                final DmxDimmer dmxDimmer =
                    new DmxDimmer(defaultDimmer.map(dd -> dd.getId()).orElse(UUID.randomUUID().toString()));
                int configuredDimmerChannels =
                    optFrom(updatedConfigVariables,
                        DmxSceneryModel.CV_CONFIGURED_DIMMER_0_CHANNELS + (index * DmxSceneryModel.DIMMER_GAP))
                            .orElse(0);
                // set the name of the dimmer
                dmxDimmer
                    .withName(defaultDimmer
                        .map(dd -> dd.getName())
                        .orElse(Resources.getString(DmxModelerController.class, "dmx-dimmer", index)));

                if (configuredDimmerChannels == 0 && index >= DmxSceneryModel.MAX_CONFIGURED_DIMMERS) {
                    // TODO more dimmers than storage in node -> check the default scenery
                    configuredDimmerChannels = defaultDimmer.map(dd -> dd.getDmxChannels().size()).orElse(0);
                }

                final List dmxChannels = new ArrayList<>();
                for (int channelColorIdx = 0; channelColorIdx < configuredDimmerChannels; channelColorIdx++) {
                    // assigned dmx channel color
                    int cvNum =
                        DmxSceneryModel.CV_CONFIGURED_DIMMER_0_CHANNEL_COLORS_START + (channelColorIdx * 2)
                            + index * DmxSceneryModel.DIMMER_GAP;

                    Optional opChannelColor = optFrom(updatedConfigVariables, cvNum);
                    if (opChannelColor.isEmpty()) {
                        opChannelColor =
                            Optional.ofNullable(channelColorSupplier(defaultDimmer, channelColorIdx).get());
                    }

                    int channelColor = opChannelColor.orElse(null);

                    // assigned dmx channel number
                    cvNum =
                        DmxSceneryModel.CV_CONFIGURED_DIMMER_0_CHANNEL_COLORS_START + (channelColorIdx * 2)
                            + index * DmxSceneryModel.DIMMER_GAP + 1;

                    final int channelColorIdxFin = channelColorIdx;

                    Integer dmxChannelNumber = optFrom(updatedConfigVariables, cvNum).orElseGet(() -> {
                        final DmxChannel dmxChannel = getDmxChannel(defaultDimmer, channelColorIdxFin);
                        if (dmxChannel != null) {
                            return dmxChannel.getDmxChannelWrapper().getDmxChannelId();
                        }
                        return null;
                    });

                    LOGGER.info("Current dmxChannelNumber: {}, channelColor: {}", dmxChannelNumber, channelColor);

                    Optional existing = getConfiguredDmxChannelWrapper(dmxChannelNumber);

                    if (existing.isPresent()) {
                        DmxChannel dmxChannel =
                            new DmxChannel(dmxDimmer, existing.get())
                                .withLineColor(LineColors.fromColorValue(channelColor));
                        dmxChannels.add(dmxChannel);
                    }
                    else {
                        LOGGER.warn("Skip unconfigured DMX channel number: {}", dmxChannelNumber);
                    }
                }
                dmxDimmer.setDmxChannels(dmxChannels);

                dmxScenery.addDmxDimmer(dmxDimmer);
            }
        }
        return dmxScenery;
    }

    private DmxChannel getDmxChannel(final Optional dmxDimmer, int channelColorIdx) {
        if (dmxDimmer.isEmpty() || !(dmxDimmer.get().getDmxChannels().size() > channelColorIdx)) {
            return null;
        }

        return dmxDimmer.get().getDmxChannels().get(channelColorIdx);
    }

    protected static Supplier channelColorSupplier(
        final Optional defaultDimmer, int channelColorIdx) {

        if (defaultDimmer.isPresent() && defaultDimmer.get().getDmxChannels().size() > channelColorIdx) {

            DmxChannel dc = defaultDimmer.get().getDmxChannels().get(channelColorIdx);

            return () -> Optional
                .of(dc).map(idc -> idc.getLineColor()).map(lc -> Integer.valueOf(LineColors.toColorValue(lc)))
                .orElse(Integer.valueOf(255));

        }

        return () -> Integer.valueOf(0);
    }

    private Optional getConfiguredDmxChannelWrapper(Integer dmxChannelValue) {
        return this.dmxSceneryModel
            .getConfiguredDmxChannelsMap().keySet().stream()
            .filter(dcw -> Objects.equals(dmxChannelValue, dcw.getDmxChannelId())).findFirst();
    }

    private void writeToolsDataCvValues(final List errorMessages, boolean v2Firmware) {
        LOGGER.info("Write the toolsdata CV values.");

        final List configVariablesToWrite = new ArrayList<>();

        if (CollectionUtils.isNotEmpty(this.dmxSceneryModel.getSceneries())) {
            final DmxScenery dmxScenery = this.dmxSceneryModel.getSceneries().get(0);

            final Map configVariables = node.getConfigVariables();

            // check if we have dimmers
            if (CollectionUtils.isNotEmpty(dmxScenery.getDmxDimmers())) {

                List dmxDimmers = dmxScenery.getDmxDimmers();
                int configuredDimmers = dmxDimmers.size();

                ConfigurationVariable configuredDimmersCv =
                    from(configVariables, DmxSceneryModel.CV_CONFIGURED_DIMMERS);
                if (configuredDimmersCv.setValueIfDifferent(String.valueOf(configuredDimmers))) {
                    configVariablesToWrite.add(configuredDimmersCv);
                }

                if (configuredDimmers > 0) {
                    int index = 0;
                    int maxDimmerIndex = configuredDimmers;
                    if (maxDimmerIndex > DmxSceneryModel.MAX_CONFIGURED_DIMMERS) {
                        maxDimmerIndex = DmxSceneryModel.MAX_CONFIGURED_DIMMERS;

                        LOGGER
                            .warn(
                                "More DMX dimmers configured than storage in CV space available. The stored configuration on the node will be truncated to match the available space.");

                        // notify user about truncated configuration
                        errorMessages.add(Resources.getString(DmxModelerController.class, "dimmer-space-exceeded"));
                    }

                    for (int dmxDimmerIndex = 0; dmxDimmerIndex < maxDimmerIndex; dmxDimmerIndex++) {
                        final DmxDimmer dmxDimmer = dmxDimmers.get(dmxDimmerIndex);

                        int configuredDimmerChannels = dmxDimmer.getDmxChannels().size();

                        LOGGER
                            .debug("Current dimmer: {}, the configured dimmer channels: {}", dmxDimmer,
                                configuredDimmerChannels);

                        ConfigurationVariable configuredDimmerChannelsCv =
                            from(configVariables,
                                DmxSceneryModel.CV_CONFIGURED_DIMMER_0_CHANNELS + (index * DmxSceneryModel.DIMMER_GAP));
                        if (configuredDimmerChannelsCv.setValueIfDifferent(String.valueOf(configuredDimmerChannels))) {
                            configVariablesToWrite.add(configuredDimmerChannelsCv);
                        }

                        int maxChannelIndex = configuredDimmerChannels;
                        if (maxChannelIndex > DmxSceneryModel.MAX_CONFIGURED_CHANNELS) {
                            maxChannelIndex = DmxSceneryModel.MAX_CONFIGURED_CHANNELS;

                            LOGGER
                                .warn(
                                    "More DMX channels configured than storage in CV space available. The stored configuration on the node will be truncated to match the available space.");

                            // notify user about truncated configuration
                            errorMessages
                                .add(Resources.getString(DmxModelerController.class, "channel-space-exceeded"));
                        }

                        for (int channelColorIdx = 0; channelColorIdx < maxChannelIndex; channelColorIdx++) {
                            int cvNum =
                                DmxSceneryModel.CV_CONFIGURED_DIMMER_0_CHANNEL_COLORS_START + (channelColorIdx * 2)
                                    + index * DmxSceneryModel.DIMMER_GAP;
                            int channelColor =
                                LineColors.toColorValue(dmxDimmer.getDmxChannels().get(channelColorIdx).getLineColor());

                            LOGGER.debug("Current color CV: {}, channelColor: {}", cvNum, channelColor);
                            ConfigurationVariable configuredChannelColorCv = from(configVariables, cvNum);
                            if (configuredChannelColorCv.setValueIfDifferent(String.valueOf(channelColor))) {
                                configVariablesToWrite.add(configuredChannelColorCv);
                            }

                            // next cv is the dmx channel
                            cvNum++;
                            int dmxChannel =
                                dmxDimmer
                                    .getDmxChannels().get(channelColorIdx).getDmxChannelWrapper().getDmxChannelId();

                            LOGGER.debug("Current channel CV: {}, dmx channel: {}", cvNum, dmxChannel);
                            ConfigurationVariable configuredDmxChannelCv = from(configVariables, cvNum);
                            if (configuredDmxChannelCv.setValueIfDifferent(String.valueOf(dmxChannel))) {
                                configVariablesToWrite.add(configuredDmxChannelCv);
                            }
                        }

                        index++;
                    }
                }

            }
            else {
                ConfigurationVariable configuredDimmersCv =
                    from(configVariables, DmxSceneryModel.CV_CONFIGURED_DIMMERS);
                if (configuredDimmersCv.setValueIfDifferent(String.valueOf(0))) {
                    configVariablesToWrite.add(configuredDimmersCv);
                }
            }

            if (CollectionUtils.isNotEmpty(configVariablesToWrite)) {
                LOGGER.info("Write toolsdata variables to node.");
                this.nodeService
                    .setConfigVariables(ConnectionRegistry.CONNECTION_ID_MAIN, node, configVariablesToWrite);
            }
            else {
                LOGGER.info("No changed CV variables found to write on node.");
            }

            // reset the dirty flag
            this.dmxSceneryModel.setHasPendingChanges(false);
        }
    }

    @Override
    public void selectedDmxChannelChanged(DmxDimmer dmxDimmer, int dmxChannelId) {

        // Search if the dimmer config is already opened
        String searchKey = DmxDimmerConfigView.prepareKey(dmxDimmer);
        LOGGER.info("Search for view with key: {}", searchKey);
        Dockable dmxDimmerConfigViewDockable = desktop.getContext().getDockableByKey(searchKey);
        if (dmxDimmerConfigViewDockable != null) {

            try {
                final DmxDimmerConfigView view = (DmxDimmerConfigView) dmxDimmerConfigViewDockable;
                view.selectedDmxChannelChanged(dmxDimmer, dmxChannelId);
            }
            catch (Exception ex) {
                LOGGER.warn("Process selected DMX channel changed in dimmer config view failed.", ex);
            }

        }
    }

    @Override
    public Map> getConfiguredDmxChannelsMap() {
        return MapUtils.unmodifiableMap(this.dmxSceneryModel.getConfiguredDmxChannelsMap());
    }

    @Override
    public void setChangedDmxChannelConfiguration(List changedDmxChannels) {

        final Map configVariables = node.getConfigVariables();
        final List configVariablesToWrite = new ArrayList<>();

        final List configVariablesToRead = new ArrayList<>();

        // the changed dmx channels contains the new dmx channel id and the cv number.
        // we must check if the previous configured channel id was 0 for the cv number to detect if we must load the cv
        // values with the brightness for this channel
        for (DmxChannelWrapper wrapper : changedDmxChannels) {
            int channelCv = wrapper.getCvNumber();

            // get the configuration variable for the current channel cv
            final ConfigurationVariable configuredChannelCv = from(configVariables, channelCv);
            // check if the previous channel id is 0
            int prevChannelId = Integer.parseInt(configuredChannelCv.getValue());
            if (prevChannelId == 0) {
                LOGGER
                    .info(
                        "The previous channel id was not assigned. We have to load the cv values of the brightness for this channel. Use the channelCv: {}",
                        channelCv);

                // we must prepare to load the cv values of the brightness for this channel
                prepareReadBrightnessCVs(configVariables, configVariablesToRead, channelCv);
            }

            // set the new channel id value
            if (configuredChannelCv.setValueIfDifferent(String.valueOf(wrapper.getDmxChannelId().intValue()))) {
                configVariablesToWrite.add(configuredChannelCv);
            }
        }

        if (CollectionUtils.isNotEmpty(configVariablesToWrite)) {
            LOGGER.info("Write variables to node.");
            // write the CV to the node
            final List configVariablesWritten =
                this.nodeService
                    .setConfigVariables(ConnectionRegistry.CONNECTION_ID_MAIN, node, configVariablesToWrite);

            LOGGER.info("Update model with configuration variables: {}", configVariablesWritten);
            node.updateConfigVariableValues(configVariablesWritten, true);

            // update the mappings
            for (DmxChannelWrapper wrapper : changedDmxChannels) {
                int channelCv = wrapper.getCvNumber();
                this.dmxSceneryModel
                    .getConfiguredDmxChannelsMap().keySet().stream().filter(dcw -> dcw.getCvNumber() == channelCv)
                    .findFirst().ifPresent(dcw -> dcw.setDmxChannelId(wrapper.getDmxChannelId()));
            }
        }
        else {
            LOGGER.info("No changed CV variables found to write on node.");
        }

        // read the cv brightness values of the new configured DMX channels from the node
        LOGGER.info("Read the brightness cv values from the node.");
        readCvFromNode(configVariablesToRead);

        // notify the listener of changes
        for (DmxChannelWrapper wrapper : changedDmxChannels) {
            this.dmxSceneryModel.publishDmxChannelEvent(new DmxChannelWrapperEvent(wrapper, false));
        }
    }

    @Override
    public Set getConfiguredDmxTimelineSet() {

        final Map updatedConfigVariables = node.getConfigVariables();
        final Set configuredDmxTimelineSet = new HashSet<>();

        // find all configured dmx times of the node
        for (int timeIndex = 0; timeIndex < DmxSceneryModel.TOTAL_AVAILABLE_TICKS; timeIndex++) {

            int cvNumLow = DmxSceneryModel.TICKS_CV_START + (timeIndex * 2);
            int cvNumHigh = DmxSceneryModel.TICKS_CV_START + 1 + (timeIndex * 2);
            Optional ticksLowValue = optFrom(updatedConfigVariables, cvNumLow);
            Optional ticksHighValue = optFrom(updatedConfigVariables, cvNumHigh);

            int dmxTime = calculateTime(ticksHighValue, ticksLowValue);

            LOGGER.info("Register the DMX time: {}, cvNumLow: {}, cvNumHigh: {}", dmxTime, cvNumLow, cvNumHigh);

            configuredDmxTimelineSet.add(new DmxTimelineWrapper(dmxTime, cvNumHigh, cvNumLow));
        }

        return SetUtils.unmodifiableSet(configuredDmxTimelineSet);
    }

    @Override
    public void setChangedDmxTimelineConfiguration(List changedDmxTimeline) {

        final Map configVariables = node.getConfigVariables();
        final List configVariablesToWrite = new ArrayList<>();

        // final List configVariablesToRead = new ArrayList<>();

        for (DmxTimelineWrapper wrapper : changedDmxTimeline) {
            int cvNumHigh = wrapper.getCvNumberHigh();
            int cvNumLow = wrapper.getCvNumberLow();

            int dmxTime = wrapper.getDmxTime();
            LOGGER
                .info("Current dmxTime: {}, cvNumHigh: {}, cvNumLow: {}, timeHigh: {}, timeLow: {}", dmxTime, cvNumHigh,
                    cvNumLow, dmxTime / 60, dmxTime % 60);

            // get the configuration variable for the current channel cv
            final ConfigurationVariable configuredCvTimeHigh = from(configVariables, cvNumHigh);
            final ConfigurationVariable configuredCvTimeLow = from(configVariables, cvNumLow);

            // set the new time value
            if (configuredCvTimeHigh.setValueIfDifferent(String.valueOf(dmxTime / 100))) {
                configVariablesToWrite.add(configuredCvTimeHigh);
            }
            if (configuredCvTimeLow.setValueIfDifferent(String.valueOf(dmxTime % 100))) {
                configVariablesToWrite.add(configuredCvTimeLow);
            }
        }

        if (CollectionUtils.isNotEmpty(configVariablesToWrite)) {
            LOGGER.info("Write variables to node.");
            // write the CV to the node
            final List configVariablesWritten =
                this.nodeService
                    .setConfigVariables(ConnectionRegistry.CONNECTION_ID_MAIN, node, configVariablesToWrite);

            LOGGER.info("Update model with configuration variables: {}", configVariablesWritten);
            node.updateConfigVariableValues(configVariablesWritten, true);

            // notify the listener of changes
            this.dmxSceneryModel.publishDmxTimelineEvent(new DmxTimelineWrapperEvent());

        }
        else {
            LOGGER.info("No changed CV variables found to write on node.");
        }
    }

    @Override
    public boolean hasPendingChanges(final DmxDimmer dmxDimmer) {

        String searchKey = DmxDimmerConfigView.prepareKey(dmxDimmer);
        LOGGER.info("Search for view with key: {}", searchKey);
        Dockable dmxDimmerConfigView = desktop.getContext().getDockableByKey(searchKey);
        if (dmxDimmerConfigView instanceof DmxDimmerConfigView) {
            LOGGER.info("Check for pending changes, dmxDimmerConfigView: {}", dmxDimmerConfigView);
            return ((DmxDimmerConfigView) dmxDimmerConfigView).hasPendingChanges();
        }

        return false;
    }

    @Override
    public void writeChangedCvValuesToNode() {
        LOGGER.info("Write all changed CV values to node.");

        boolean v2Firmware = isV2Firmware();

        final List errorMessages = new ArrayList<>();

        // check if we have to write the tools data values
        if (this.dmxSceneryModel.isHasPendingChanges()) {
            LOGGER.info("The DMX scenery model has pending changes.");
            writeToolsDataCvValues(errorMessages, v2Firmware);
        }

        // check if DMX modeler views are opened
        final List dmxDimmerConfigViews = new ArrayList<>();
        for (DmxScenery dmxScenery : dmxSceneryModel.getSceneries()) {
            for (DmxDimmer dmxDimmer : dmxScenery.getDmxDimmers()) {
                String searchKey = DmxDimmerConfigView.prepareKey(dmxDimmer);
                LOGGER.info("Search for view with key: {}", searchKey);
                Dockable dmxDimmerConfigView = desktop.getContext().getDockableByKey(searchKey);
                if (dmxDimmerConfigView != null) {
                    LOGGER.info("Add the dmxDimmerConfigView: {}", dmxDimmerConfigView);

                    dmxDimmerConfigViews.add(dmxDimmerConfigView);
                }
            }
        }

        for (Dockable dockable : dmxDimmerConfigViews) {
            if (dockable instanceof DmxDimmerConfigView) {
                DmxDimmerConfigView dmxDimmerConfigView = (DmxDimmerConfigView) dockable;

                try {
                    dmxDimmerConfigView.writeChangedCvValues();
                }
                catch (InvalidDataException ex) {
                    LOGGER.warn("The CV data to write is not valid.", ex);

                    TaskDialogs
                        .build(JOptionPane.getFrameForComponent(this.parent),
                            Resources.getString(DmxModelerController.class, "invalid-cv-data.instruction"),
                            Resources.getString(DmxModelerController.class, "invalid-cv-data.text"))
                        .title(Resources.getString(DmxModelerController.class, "invalid-cv-data.title"))
                        .showException(ex);

                }
            }
        }

        if (!errorMessages.isEmpty()) {
            // show warning dialog

            // prepare message text
            StringBuilder sb =
                new StringBuilder(Resources.getString(DmxModelerController.class, "storage-cv-data-exceeded.text"));
            for (String errorMessage : errorMessages) {
                sb.append(errorMessage).append("\n");
            }

            TaskDialogs
                .build(JOptionPane.getFrameForComponent(this.parent),
                    Resources.getString(DmxModelerController.class, "storage-cv-data-exceeded.instruction"),
                    sb.toString())
                .title(Resources.getString(DmxModelerController.class, "storage-cv-data-exceeded.title")).inform();
        }
    }

    @Override
    public void readAllCvValuesFromNode() {
        LOGGER.info("Read all relevant CV values from the node.");

        SwingUtilities
            .invokeLater(
                () -> new CvTransferProgressDialog(dmxSceneryView.getComponent(), true, DmxModelerController.this));

    }

    @Override
    public void getFixedPatternCvValues(final List items, final ProgressStatusCallback callback) {
        LOGGER.info("Read the fixed pattern cv values.");

        final List configVariablesToRead = new ArrayList<>();

        final Map configVariables = node.getConfigVariables();

        for (FixedPatternItem item : items) {
            int cvNumber = item.getCvNumber();
            ConfigurationVariable cvDmxChannelId = from(configVariables, cvNumber);
            if (cvDmxChannelId.getValue() == null) {
                configVariablesToRead.add(cvDmxChannelId);
            }
            ConfigurationVariable cvBrightness = from(configVariables, cvNumber + 1);
            if (cvBrightness.getValue() == null) {
                configVariablesToRead.add(cvBrightness);
            }
        }

        LOGGER.debug("Prepared CVs: {}", configVariablesToRead);

        if (callback != null) {
            callback.messageChanged("Read CVs from node: " + configVariablesToRead.size());
        }

        // read dmx channels and brightness CVs from the node
        readCvFromNode(configVariablesToRead);

        final Map updatedConfigVariables = node.getConfigVariables();
        for (FixedPatternItem item : items) {
            int cvNumber = item.getCvNumber();

            Integer dmxChannelValue = optFrom(updatedConfigVariables, cvNumber).orElse(null);
            item.setDmxChannelId(dmxChannelValue);

            Integer brightnessValue = optFrom(updatedConfigVariables, cvNumber + 1).orElse(null);
            item.setBrightnessValue(brightnessValue);
        }
    }

    @Override
    public void writeFixedPatternCvValues(List fixedPatternItems, Runnable finishedCallback) {
        LOGGER.info("Write CV values of fixedPatternItems: {}", fixedPatternItems);

        final List configVariablesToWrite = new LinkedList<>();

        final Map configVariables = node.getConfigVariables();

        for (FixedPatternItem item : fixedPatternItems) {

            int cvNumber = item.getCvNumber();
            if (item.getDmxChannelId() != null && item.getBrightnessValue() != null) {

                Integer channelId = item.getDmxChannelId();
                Integer brightness = item.getBrightnessValue();

                addIfDifferent(configVariables, configVariablesToWrite, cvNumber, channelId);
                addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 1, brightness);
            }
            else {
                LOGGER.debug("No new value for item: {}", item);

                addIfDifferent(configVariables, configVariablesToWrite, cvNumber, 0);
                addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 1, 0);
            }

        }

        if (CollectionUtils.isNotEmpty(configVariablesToWrite)) {
            writeCvValues(this.node, configVariablesToWrite);

            if (finishedCallback != null) {
                finishedCallback.run();
            }
        }
        else {
            LOGGER.info("No changed CV variables to write available.");
        }
    }

    @Override
    public void getOverlayCvValues(List items, ProgressStatusCallback callback) {
        LOGGER.info("Read the overlay cv values.");

        final List configVariablesToRead = new ArrayList<>();

        boolean v2Firmware = node.getNode().getSoftwareVersion().isHigherOrEqualThan(SoftwareVersion.build(2, 0, 0));
        final Map configVariables = node.getConfigVariables();

        for (OverlayItem item : items) {
            int cvNumber = item.getCvNumber();
            ConfigurationVariable cvDmxChannelIdA = from(configVariables, cvNumber);
            if (cvDmxChannelIdA.getValue() == null) {
                configVariablesToRead.add(cvDmxChannelIdA);
            }
            ConfigurationVariable cvBrightnessA = from(configVariables, cvNumber + 1);
            if (cvBrightnessA.getValue() == null) {
                configVariablesToRead.add(cvBrightnessA);
            }
            if (v2Firmware) {
                ConfigurationVariable cvDmxChannelIdB = from(configVariables, cvNumber + 2);
                if (cvDmxChannelIdB.getValue() == null) {
                    configVariablesToRead.add(cvDmxChannelIdB);
                }
                ConfigurationVariable cvBrightnessB = from(configVariables, cvNumber + 3);
                if (cvBrightnessB.getValue() == null) {
                    configVariablesToRead.add(cvBrightnessB);
                }
                ConfigurationVariable cvTransitionTime = from(configVariables, cvNumber + 4);
                if (cvTransitionTime.getValue() == null) {
                    configVariablesToRead.add(cvTransitionTime);
                }
            }
            else {
                ConfigurationVariable cvTransitionTime = from(configVariables, cvNumber + 2);
                if (cvTransitionTime.getValue() == null) {
                    configVariablesToRead.add(cvTransitionTime);
                }
            }
        }

        LOGGER.debug("Prepared CVs: {}", configVariablesToRead);

        if (callback != null) {
            callback.messageChanged("Read CVs from node: " + configVariablesToRead.size());
        }

        // read dmx channels and brightness CVs from the node
        readCvFromNode(configVariablesToRead);

        final Map updatedConfigVariables = node.getConfigVariables();
        for (OverlayItem item : items) {
            int cvNumber = item.getCvNumber();

            Integer dmxChannelValueA = optFrom(updatedConfigVariables, cvNumber).orElse(null);
            item.setDmxChannelIdA(dmxChannelValueA);

            Integer brightnessValueA = optFrom(updatedConfigVariables, cvNumber + 1).orElse(null);
            item.setBrightnessValueA(brightnessValueA);

            if (v2Firmware) {
                Integer dmxChannelValueB = optFrom(updatedConfigVariables, cvNumber + 2).orElse(null);
                item.setDmxChannelIdB(dmxChannelValueB);

                Integer brightnessValueB = optFrom(updatedConfigVariables, cvNumber + 3).orElse(null);
                item.setBrightnessValueB(brightnessValueB);

                Integer transitionTime = optFrom(updatedConfigVariables, cvNumber + 4).orElse(null);
                item.setTransitionTime(transitionTime);
            }
            else {
                Integer transitionTime = optFrom(updatedConfigVariables, cvNumber + 2).orElse(null);
                item.setTransitionTime(transitionTime);
            }
        }
    }

    private static void add(final List configVariablesToWrite, int cvNumber, Integer value) {
        configVariablesToWrite.add(ConfigurationVariable.from(cvNumber, ByteUtils.getIntLowByteValue(value)));
    }

    private static void none() {

    }

    private static void addIfDifferent(
        final Map configVariables,
        final List configVariablesToWrite, int cvNumber, Integer value) {
        optFrom(configVariables, cvNumber).ifPresent(val -> {
            if (!Objects.equals(val, value)) {
                add(configVariablesToWrite, cvNumber, value);
            }
            else {
                none();
            }
        });
    }

    @Override
    public void writeOverlayCvValues(List items, Runnable finishedCallback) {
        LOGGER.info("Write CV values of overlayItems: {}", items);

        final List configVariablesToWrite = new LinkedList<>();

        boolean v2Firmware = node.getNode().getSoftwareVersion().isHigherOrEqualThan(SoftwareVersion.build(2, 0, 0));
        final Map configVariables = node.getConfigVariables();

        for (OverlayItem item : items) {

            int cvNumber = item.getCvNumber();
            if (item.getDmxChannelIdA() != null && item.getBrightnessValueA() != null
                && item.getTransitionTime() != null) {

                final Integer channelIdA = item.getDmxChannelIdA();
                final Integer brightnessA = item.getBrightnessValueA();
                final Integer transitionTime = item.getTransitionTime();

                addIfDifferent(configVariables, configVariablesToWrite, cvNumber, channelIdA);
                addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 1, brightnessA);
                if (v2Firmware) {

                    if (item.getDmxChannelIdB() != null && item.getBrightnessValueB() != null) {
                        final Integer channelIdB = item.getDmxChannelIdB();
                        final Integer brightnessB = item.getBrightnessValueB();
                        addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 2, channelIdB);
                        addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 3, brightnessB);
                    }
                    else {
                        addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 2, 0);
                        addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 3, 0);
                    }
                    addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 4, transitionTime);
                }
                else {
                    addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 2, transitionTime);
                }
            }
            else if (v2Firmware && item.getDmxChannelIdB() != null && item.getBrightnessValueB() != null
                && item.getTransitionTime() != null) {

                final Integer channelIdB = item.getDmxChannelIdB();
                final Integer brightnessB = item.getBrightnessValueB();
                final Integer transitionTime = item.getTransitionTime();

                addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 2, channelIdB);
                addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 3, brightnessB);

                // channel A is 0
                addIfDifferent(configVariables, configVariablesToWrite, cvNumber, 0);
                addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 1, 0);
                addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 4, transitionTime);
            }
            else {
                LOGGER.debug("No new value for item: {}", item);

                addIfDifferent(configVariables, configVariablesToWrite, cvNumber, 0);
                addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 1, 0);
                if (v2Firmware) {
                    addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 2, 0);
                    addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 3, 0);
                    addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 4, 0);
                }
                else {
                    addIfDifferent(configVariables, configVariablesToWrite, cvNumber + 2, 0);
                }
            }

        }

        if (CollectionUtils.isNotEmpty(configVariablesToWrite))

        {
            writeCvValues(this.node, configVariablesToWrite);

            if (finishedCallback != null) {
                finishedCallback.run();
            }
        }
        else {
            LOGGER.info("No changed CV variables to write available.");
        }
    }

    @Override
    public void setPendingChanges(boolean hasPendingChanges) {
        LOGGER.info("Set the pending changes flag: {}", hasPendingChanges);
        // set the dirty flag
        this.dmxSceneryModel.setHasPendingChanges(hasPendingChanges);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy