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

org.bidib.wizard.mvc.main.controller.MainController Maven / Gradle / Ivy

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

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;

import javax.annotation.PostConstruct;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.bidib.api.json.types.ConnectionPhase;
import org.bidib.jbidibc.exchange.vendorcv.VendorCvData;
import org.bidib.jbidibc.exchange.vendorcv.VendorCvError;
import org.bidib.jbidibc.exchange.vendorcv.VendorCvFactory;
import org.bidib.jbidibc.messages.enums.LcOutputType;
import org.bidib.jbidibc.messages.enums.NetBidibSocketType;
import org.bidib.jbidibc.messages.enums.SysErrorEnum;
import org.bidib.jbidibc.messages.exception.InvalidConfigurationException;
import org.bidib.jbidibc.messages.helpers.Context;
import org.bidib.jbidibc.messages.helpers.DefaultContext;
import org.bidib.jbidibc.messages.port.PortConfigValue;
import org.bidib.jbidibc.messages.utils.ByteUtils;
import org.bidib.jbidibc.messages.utils.NodeUtils;
import org.bidib.jbidibc.messages.utils.ThreadFactoryBuilder;
import org.bidib.wizard.api.event.ErrorEvent;
import org.bidib.wizard.api.locale.Resources;
import org.bidib.wizard.api.model.Accessory;
import org.bidib.wizard.api.model.NodeInterface;
import org.bidib.wizard.api.model.connection.BidibConnection;
import org.bidib.wizard.api.model.event.NodeStatusEvent;
import org.bidib.wizard.api.model.event.NodeStatusEvent.StatusIdentifier;
import org.bidib.wizard.api.model.listener.NodeErrorListener;
import org.bidib.wizard.api.model.listener.NodeListListener;
import org.bidib.wizard.api.model.listener.NodeSelectionListener;
import org.bidib.wizard.api.notification.NodeStatusInfo.LoadStatus;
import org.bidib.wizard.api.service.console.ConsoleColor;
import org.bidib.wizard.api.service.console.ConsoleService;
import org.bidib.wizard.api.service.node.BoosterService;
import org.bidib.wizard.api.service.node.CommandStationService;
import org.bidib.wizard.api.service.node.NodeService;
import org.bidib.wizard.api.service.node.SwitchingNodeService;
import org.bidib.wizard.api.utils.PortListUtils;
import org.bidib.wizard.client.common.alert.BidibAlert;
import org.bidib.wizard.client.common.component.LabeledDisplayItems;
import org.bidib.wizard.client.common.controller.CvDefinitionPanelControllerInterface;
import org.bidib.wizard.client.common.event.BidibConnectionEvent;
import org.bidib.wizard.client.common.event.MainControllerBoosterOnEvent;
import org.bidib.wizard.client.common.event.MainControllerEvent;
import org.bidib.wizard.client.common.event.MenuEvent;
import org.bidib.wizard.client.common.view.DockUtils;
import org.bidib.wizard.client.common.view.statusbar.StatusBar;
import org.bidib.wizard.common.context.DefaultApplicationContext;
import org.bidib.wizard.common.exception.ConnectionException;
import org.bidib.wizard.common.exception.InternalStartupException;
import org.bidib.wizard.common.labels.WizardLabelWrapper;
import org.bidib.wizard.common.model.settings.MiscSettingsInterface;
import org.bidib.wizard.common.model.settings.WizardSettingsInterface;
import org.bidib.wizard.common.script.node.types.TargetType;
import org.bidib.wizard.common.service.SettingsService;
import org.bidib.wizard.config.MainViewFactory;
import org.bidib.wizard.config.PairingControllerFactory;
import org.bidib.wizard.core.model.connection.ConnectionRegistry;
import org.bidib.wizard.core.service.ConnectionService;
import org.bidib.wizard.model.ports.BacklightPort;
import org.bidib.wizard.model.ports.LightPort;
import org.bidib.wizard.model.ports.Port;
import org.bidib.wizard.model.ports.ServoPort;
import org.bidib.wizard.model.ports.SwitchPairPort;
import org.bidib.wizard.model.ports.SwitchPort;
import org.bidib.wizard.model.status.BoosterStatus;
import org.bidib.wizard.model.status.CommandStationStatus;
import org.bidib.wizard.mvc.common.exception.NodeChangeVetoException;
import org.bidib.wizard.mvc.console.controller.ConsoleController;
import org.bidib.wizard.mvc.main.controller.exception.CloseAbortedException;
import org.bidib.wizard.mvc.main.controller.listener.AlertListener;
import org.bidib.wizard.mvc.main.controller.listener.AlertListener.AlertAction;
import org.bidib.wizard.mvc.main.model.ConnectionPhaseModel;
import org.bidib.wizard.mvc.main.model.MainModel;
import org.bidib.wizard.mvc.main.view.MainNodeListActionListener;
import org.bidib.wizard.mvc.main.view.MainView;
import org.bidib.wizard.mvc.main.view.exchange.NodeExchangeHelper;
import org.bidib.wizard.mvc.tips.controller.TipOfDayClosedListener;
import org.bidib.wizard.mvc.tips.controller.TipOfDayController;
import org.bidib.wizard.startup.WizardStartupParams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.xml.sax.SAXException;

import com.jidesoft.swing.FolderChooser;
import com.vlsolutions.swing.docking.Dockable;
import com.vlsolutions.swing.docking.DockingContext;

import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;

public class MainController implements MainControllerInterface {
    private static final Logger LOGGER = LoggerFactory.getLogger(MainController.class);

    private final MainModel mainModel;

    @Autowired
    private MainViewFactory mainViewFactory;

    private MainView mainView;

    @Autowired
    private MainNodeListActionListener mainNodeListActionListener;

    private final ScheduledExecutorService selectedNodeChangeWorker =
        Executors
            .newScheduledThreadPool(1,
                new ThreadFactoryBuilder().setNameFormat("selectedNodeChangeWorkers-thread-%d").build());

    @Autowired
    private CommandStationService commandStationService;

    @Autowired
    private AlertController alertController;

    @Autowired
    private TipOfDayController tipOfDayController;

    @Autowired
    private MiscSettingsInterface miscSettings;

    @Autowired
    private WizardSettingsInterface wizardSettings;

    @Autowired
    private SettingsService settingsService;

    @Autowired
    private ConnectionService connectionService;

    @Autowired
    private NodeService nodeService;

    @Autowired
    private BoosterService boosterService;

    @Autowired
    private SwitchingNodeService switchingNodeService;

    @Autowired
    private StatusBar statusBar;

    @Autowired
    private WizardLabelWrapper wizardLabelWrapper;

    @Autowired
    private ApplicationContext context;

    @Autowired
    private CvDefinitionPanelControllerInterface cvDefinitionPanelController;

    @Autowired
    private WizardStartupParams startupParams;

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    private ConsoleService consoleService;

    private final ConnectionPhaseModel connectionPhaseModel;

    private CompositeDisposable compDisposable = new CompositeDisposable();

    private CompositeDisposable compDispConnectionStatusChanges = new CompositeDisposable();

    @Autowired
    private PairingControllerFactory pairingControllerFactory;

    private PairingController pairingController;

    private final String connectionId;

    @Autowired
    private DockingContext dockingContext;

    private AtomicBoolean startupPassed = new AtomicBoolean();

    private final ScheduledExecutorService openConnectionWorker;

    /**
     * the startup workers
     */
    private final ScheduledExecutorService startupWorkers =
        Executors
            .newScheduledThreadPool(1, new ThreadFactoryBuilder().setNameFormat("startupWorkers-thread-%d").build());

    public MainController(final MainModel model, final ConnectionPhaseModel connectionPhaseModel, String connectionId) {
        LOGGER.info("Created MainController, provided connectionId: {}", connectionId);

        this.connectionId = connectionId;

        this.connectionPhaseModel = connectionPhaseModel;
        connectionPhaseModel.setConnectionId(this.connectionId);

        this.mainModel = model;

        final ThreadFactory namedThreadFactory =
            new ThreadFactoryBuilder().setNameFormat("openConnectionWorkers-thread-%d").build();
        this.openConnectionWorker = Executors.newScheduledThreadPool(1, namedThreadFactory);
    }

    @PostConstruct
    public void init() {
        LOGGER.info("Initialize the main controller.");
        final NodeErrorListener nodeErrorListener = new NodeErrorListener() {

            @Override
            public void nodeErrorChanged(
                final NodeInterface node, final SysErrorEnum sysError, final String reasonData) {

                // notify the console
                SwingUtilities.invokeLater(() -> {
                    try {
                        // ensure console is visible
                        ConsoleController.ensureConsoleVisible();

                        LOGGER
                            .info("The error state has changed for node: {}, sysError: {}, reasonData: {}", node,
                                sysError, reasonData);

                        String reason = null;
                        if (sysError != null) {
                            reason = reasonData;
                            // add line
                            String consoleLineText =
                                String.format("The node %s is set to error state: %s", node, reason);
                            LOGGER.warn(consoleLineText);

                            consoleService.addConsoleLine(ConsoleColor.red, consoleLineText);
                        }
                        else {
                            reason = reasonData;
                            if (StringUtils.isNotBlank(reason)) {
                                // add line
                                String consoleLineText =
                                    String.format("The node %s is set to error state: %s", node, reason);
                                LOGGER.warn(consoleLineText);

                                consoleService.addConsoleLine(ConsoleColor.red, consoleLineText);
                            }
                            else {
                                // add line
                                consoleService
                                    .addConsoleLine(ConsoleColor.black,
                                        String.format("The error state on node %s is cleared.", node));
                            }
                        }
                    }
                    catch (Exception ex) {
                        LOGGER
                            .warn("Add new error line to console failed, sysError: {}, reasonData: {}", sysError,
                                node.getReasonData(), ex);
                    }
                });
            }

            @Override
            public void nodeStallChanged(final NodeInterface node, final Boolean nodeStall) {
                // notify the console
                SwingUtilities.invokeLater(() -> {
                    try {
                        // ensure console is visible
                        ConsoleController.ensureConsoleVisible();

                        // add line
                        consoleService
                            .addConsoleLine(ConsoleColor.black,
                                String
                                    .format("The node %s is %s", node,
                                        (nodeStall != null && nodeStall.booleanValue()) ? "stall" : "not stall"));
                    }
                    catch (Exception ex) {
                        LOGGER.warn("Add new error line to console failed", ex);
                    }
                });
            }
        };
        mainModel.setNodeErrorListener(nodeErrorListener);
    }

    public boolean hasStartupPassed() {
        return startupPassed.get();
    }

    @Override
    public void clearNodes() {
        LOGGER.info("Clear the nodes from the model.");

        mainModel.getStatusModel().setCd(false);
        mainModel.getStatusModel().setRx(false);
        mainModel.getStatusModel().setTx(false);
        mainModel.getStatusModel().setCd(false);

        mainModel.clearNodes();
    }

    private void loadLabels() {

        // make sure the labels directory is available and write enabled
        String labelPath = miscSettings.getBidibConfigDir();
        LOGGER.info("Check if the directory for labels exists: {}", labelPath);
        boolean labelPathIsValid = false;

        do {
            try {
                File dir = FileUtils.getFile(labelPath);
                // Path dir = Paths.get(labelPath);

                if (!dir.exists()) {
                    // if (!Files.exists(dir)) {
                    LOGGER.info("Try to create the directory for labels: {}", labelPath);

                    try {
                        // dir = Files.createDirectories(dir);
                        FileUtils.forceMkdir(dir);

                        LOGGER.info("Created new directory for labels: {}", dir);
                    }
                    catch (IOException ioex) {
                        LOGGER.warn("Create new directory for labels failed.", ioex);

                        if (SystemUtils.IS_OS_LINUX && labelPath.startsWith("/home/")) {
                            String userName = System.getProperty("user.name");
                            // check if the name is correct
                            int beginIndex = 6;
                            int endIndex = labelPath.indexOf("/", beginIndex);
                            String configuredUserName = labelPath.substring(beginIndex, endIndex);
                            if (!userName.equals(configuredUserName)) {

                                LOGGER
                                    .warn(
                                        "The current username '{}' does not match the configured username '{}' for the label path.",
                                        userName, configuredUserName);

                                Object[] options =
                                    { Resources.getString(NodeExchangeHelper.class, "labeldirerror.correct"),
                                        Resources.getString(NodeExchangeHelper.class, "labeldirerror.ignore") };

                                int answer =
                                    JOptionPane
                                        .showOptionDialog(JOptionPane.getFrameForComponent(null),
                                            Resources
                                                .getString(NodeExchangeHelper.class,
                                                    "labeldirerror.message_username_mismatch",
                                                    new Object[] { userName, configuredUserName, labelPath }),
                                            Resources.getString(NodeExchangeHelper.class, "labeldirerror.title"),
                                            JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null, options,
                                            options[0]);

                                if (answer == JOptionPane.YES_OPTION) {
                                    labelPath = "/home/" + userName + labelPath.substring(endIndex);

                                    LOGGER.info("The user selected to correct the label path: {}", labelPath);
                                    miscSettings.setBidibConfigDir(labelPath);
                                    settingsService.storeSettings();

                                    // dir = Paths.get(labelPath);
                                    dir = FileUtils.getFile(labelPath);
                                    // if (!Files.exists(dir)) {
                                    if (!dir.exists()) {
                                        LOGGER.info("Try to create the directory for labels: {}", labelPath);
                                        try {
                                            // dir = Files.createDirectories(dir);
                                            FileUtils.forceMkdir(dir);
                                            LOGGER.info("Created new directory for labels: {}", dir);
                                        }
                                        catch (IOException ioex2) {
                                            LOGGER.warn("Create new directory for labels failed.", ioex);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                if (!dir.exists()) {
                    LOGGER.warn("The directory for labels is not available. Check permission on path: {}", labelPath);

                    throw new IllegalStateException(
                        "The directory for labels is not available. Check permission on path: " + labelPath);
                }

                if (!dir.canWrite()) {
                    LOGGER
                        .warn("The directory for labels is not write enabled. Check permission on path: {}", labelPath);

                    throw new IllegalStateException(
                        "The directory for labels is not write enabled. Check permission on path: " + labelPath);
                }

                LOGGER.info("The label directory is write enabled: {}", labelPath);

                labelPathIsValid = true;
            }
            catch (Exception ex) {
                LOGGER
                    .warn(
                        "The directory for labels is not available or is not write enabled. Check permission on path: {}",
                        labelPath, ex);

                int choice =
                    JOptionPane
                        .showOptionDialog(JOptionPane.getFrameForComponent(null), Resources
                            .getString(NodeExchangeHelper.class, "labeldirerror.message", new Object[] { labelPath }),
                            Resources.getString(NodeExchangeHelper.class, "labeldirerror.title"),
                            JOptionPane.OK_CANCEL_OPTION, JOptionPane.ERROR_MESSAGE, null, null, null);

                // interpret the user's choice
                if (choice == JOptionPane.CANCEL_OPTION) {
                    System.exit(0);
                }
                else {
                    // let the user change the label directory
                    FolderChooser chooser = new FolderChooser();
                    String userHome = System.getProperty("user.home");
                    final File bidibDefaultDir = new File(userHome, ".bidib");
                    chooser.setSelectedFolder(bidibDefaultDir);
                    int returnVal = chooser.showOpenDialog(JOptionPane.getFrameForComponent(null));
                    if (returnVal == FolderChooser.APPROVE_OPTION) {
                        LOGGER.info("You chose to open this folder: {}", chooser.getSelectedFile().getPath());

                        labelPath = chooser.getSelectedFile().getPath();
                    }
                    else {
                        System.exit(0);
                    }
                }
            }
        }
        while (!labelPathIsValid);

        // check if the label path was changed
        if (!labelPath.equals(miscSettings.getBidibConfigDir())) {
            LOGGER.info("Set the new label path: {}", labelPath);
            miscSettings.setBidibConfigDir(labelPath);
            settingsService.storeSettings();
        }

        startupWorkers.submit(() -> {
            LOGGER.info("Start load and register label factories.");
            final AtomicBoolean startupPassed = new AtomicBoolean(false);
            try {
                reloadLabels(null);

                startupPassed.set(true);
            }
            catch (Exception ex) {
                LOGGER.warn("Load labels failed.", ex);
            }

            LOGGER.info("Load and register labels passed. Invoke open UI.");

            SwingUtilities.invokeLater(() -> {
                LOGGER.info("Open the UI from AWT-thread.");
                if (startupPassed.get()) {
                    prepareAndOpenUI();
                }
                else {
                    JOptionPane
                        .showMessageDialog(null,
                            Resources.getString(MainController.class, "mandatory-directory-missing.text"),
                            Resources.getString(MainController.class, "mandatory-directory-missing.title"),
                            JOptionPane.ERROR_MESSAGE);
                    System.exit(1);

                }

                LOGGER.info("Open the UI has finished.");
            });
        });
    }

    /**
     * Reload the labels of the node.
     * 
     * @param uniqueId
     *            the unique id of the node
     */
    private void reloadLabels(Long uniqueId) {
        LOGGER.info("Reload labels for uniqueId: {}", uniqueId);

        if (uniqueId != null) {
            wizardLabelWrapper.loadLabels(uniqueId);
        }
    }

    private boolean setDefaultCursor() {
        LOGGER.info("Set default cursor");
        return mainView.setBusy(false);
    }

    private boolean setWaitCursor() {
        LOGGER.info("Set wait cursor");
        return mainView.setBusy(true);
    }

    private PairingController getPairingController() {
        if (this.pairingController == null) {
            LOGGER.info("Create the pairing controller.");
            this.pairingController = pairingControllerFactory.create(statusBar, connectionService);
        }
        return this.pairingController;
    }

    /**
     * Start the main controller.
     */
    public void start() {

        LOGGER.info("Start the main controller");

        DefaultApplicationContext.getInstance().register(DefaultApplicationContext.KEY_MAIN_CONTROLLER, this);

        loadLabels();
    }

    private void prepareAndOpenUI() {
        LOGGER.info("Open the UI.");

        this.mainView = mainViewFactory.create(settingsService);

        // subscribe to information of initial load of node tree data
        Disposable dispNodeStatusInfoChanges = connectionService.subscribeNodeStatusInfoChanges(nsi -> {
            LOGGER.info("The node status info has changed: {}", nsi);

            if (this.connectionId.equals(nsi.getConnectionId())) {
                if (nsi.getLoadStatus() == LoadStatus.FINISHED) {
                    LOGGER
                        .info("The initial load of the node tree data has finished. Total number of nodes: {}",
                            nsi.getArgs());
                    mainModel.signalInitialLoadFinished();

                    String statusText = null;
                    if (nsi.getArgs() != null && nsi.getArgs().length > 0) {
                        statusText = Resources.formatString(nsi.getResourceKey(), nsi.getArgs());
                    }
                    else {
                        statusText = "The initial load of the node tree data has finished.";
                    }

                    statusBar.setStatusText(statusText);
                }
                else if (nsi.getLoadStatus() == LoadStatus.UNKNOWN) {

                    try {
                        String statusText = Resources.formatString(nsi.getResourceKey(), nsi.getArgs());
                        statusBar.setStatusText(statusText);
                    }
                    catch (Exception ex) {
                        LOGGER.warn("Format and display the status bar message failed: {}", nsi, ex);
                    }
                }
            }
        }, error -> {
            LOGGER.warn("The initial load of the node tree data has failed.", error);
        });
        compDisposable.add(dispNodeStatusInfoChanges);

        // subscribe to changes of connection status
        Disposable dispConnectionStatus = connectionService.subscribeConnectionStatusChanges(ci -> {
            String connectionId = ci.getConnectionId();
            if (this.connectionId.equals(connectionId)) {

                // publish the change of the connection status
                publishStatus(connectionId, ci.getConnectionState().getActualPhase(), null);

            }
        }, error -> {
            LOGGER.warn("Subscription to connection status signalled an error: ", error);
            publishStatus(error);
        });
        compDisposable.add(dispConnectionStatus);

        // subscribe to connection actions
        Disposable dispConnectionAction = connectionService.subscribeConnectionActions(ci -> {
            String connectionId = ci.getConnectionId();
            if (this.connectionId.equals(connectionId)) {

                final PairingController pairingController = getPairingController();

                // publish the change of the connection status
                pairingController.publishAction(connectionId, ci.getMessageKey(), ci.getContext());

            }
        }, error -> {
            LOGGER.warn("Subscription to connection action signalled an error: ", error);
            publishAction(error);
        });
        compDisposable.add(dispConnectionAction);

        mainView.createComponents();

        // layout the frame content
        mainView.prepareFrame();

        // configure the main view
        DefaultApplicationContext.getInstance().register(DefaultApplicationContext.KEY_MAIN_FRAME, mainView.getFrame());

        LOGGER.info("Fetched startup params: {}", startupParams);

        // check the startup mode
        if (startupParams != null) {
            String startupMode = startupParams.getStartupMode();
            if (WizardStartupParams.MODE_ICONIFIED.equalsIgnoreCase(startupMode)) {
                mainView.getFrame().setExtendedState(JFrame.ICONIFIED);
            }
        }

        // show the frame
        mainView.setVisible(true);

        mainView.bringWindowToFront();

        // display the connect hint in the status bar
        mainView.setStatusText(Resources.getString(getClass(), "connectHint"), StatusBar.DISPLAY_NORMAL);

        // add the node list listener
        mainView.addNodeListListener(mainNodeListActionListener);

        DefaultApplicationContext
            .getInstance()
            .register(DefaultApplicationContext.KEY_MAIN_NODELIST_ACTION_LISTENER, mainNodeListActionListener);

        // add the node selection listener
        mainView.addNodeListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(final ListSelectionEvent e) {
                if (!e.getValueIsAdjusting()) {
                    LOGGER.info("The list selection has changed on the node list: {}", e.getSource());

                    final LabeledDisplayItems nodeList =
                        (LabeledDisplayItems) e.getSource();

                    // get the new selected node
                    final NodeInterface node = nodeList.getSelectedItem();

                    LOGGER
                        .info("Value changed, selected node in nodeList: {}, selectedNode in mainModel: {}", node,
                            mainModel.getSelectedNode());

                    // do not load the node if it is already selected
                    if (node == null || node.equals(mainModel.getSelectedNode())) {
                        LOGGER.info("The node is not available or already selected!");
                        return;
                    }

                    // TODO if the node is partially loaded already we must trigger the load of the configuration and
                    // show the details after the load has finished
                    if (StatusIdentifier.InitialLoadPartiallyFinished == node.getNodeLoadStatusIdentifier()) {

                        // we must subscribe to node events to detect when the load has finished and all nodes have
                        // finished loading the configuration and port status

                        // TODO maybe register the disposable with the node to allow dispose when node is loaded twice
                        // bu has not finished before
                        final CompositeDisposable disposable = new CompositeDisposable();
                        final BidibConnection connection =
                            MainController.this.connectionService.find(ConnectionRegistry.CONNECTION_ID_MAIN);

                        LOGGER
                            .info(
                                "Found partially loaded node that is selected to display the details. Subscribe to nodeEvents on connection: {}",
                                connection);
                        try {
                            final Disposable dispNodeEventSubscription = connection.subscribeNodeEvents(nodeEvent -> {
                                LOGGER.info("Received nodeEvent: {}", nodeEvent);

                                if (nodeEvent instanceof NodeStatusEvent) {
                                    final NodeStatusEvent nse = (NodeStatusEvent) nodeEvent;

                                    handleNodeStatusEvent(connection, nse, disposable, node,
                                        loadedNode -> selectNode(loadedNode));
                                }
                            }, err -> {
                                LOGGER.warn("Received error from nodeEvent subject.", err);
                            }, () -> {
                                LOGGER.info("NodeEventSubject has completed.");
                            });

                            disposable.add(dispNodeEventSubscription);
                            LOGGER.info("Finished subscribe to nodeEvents on connection: {}", connection);

                            MainController.this.nodeService
                                .loadNodeConfiguration(ConnectionRegistry.CONNECTION_ID_MAIN, node);
                        }
                        finally {
                            LOGGER.info("Load the partially loaded node is initiated, node: {}", node);
                        }

                        return;
                    }

                    // make sure the initial loading of the tree structure has finished
                    if (StatusIdentifier.InitialLoadFinished != node.getNodeLoadStatusIdentifier()) {
                        LOGGER
                            .warn(
                                "The initial load of the nodeTab has not finished yet. Cancel selection and load of node details: {}",
                                node.toNodeAddressAndUniqueString());

                        statusBar
                            .setStatusText(String
                                .format(Resources.getString(MainController.class, "load-config-not-finished"),
                                    node.toNodeAddressAndUniqueString()));
                        return;
                    }

                    selectNode(node);
                }
            }
        });

        // Add the CV definition request listener
        mainView
            .addCvDefinitionRequestListener(new MainCvDefinitionRequestListener(this.mainModel, nodeService,
                connectionId, statusBar, mainView.getFrame()));

        // add the window listener
        mainView.setWindowListener(new WindowAdapter() {

            @Override
            public void windowClosing(WindowEvent e) {
                LOGGER.info("The main window will be closed.");

                // ask the user to save all nodes if running simulation
                boolean isSimulation = connectionService.isSimulation(MainController.this.connectionId);
                LOGGER.info("The current connection is a simulation: {}", isSimulation);

                if (isSimulation) {
                    boolean continueClose =
                        showConfirmDialog(
                            Resources.getString(MainController.class, "close-simulation-connection.message"),
                            Resources.getString(MainController.class, "close-simulation-connection.title"));
                    if (!continueClose) {
                        LOGGER.info("The user cancelled the close main window operation.");
                        return;
                    }
                }
                // }

                LOGGER.info("Close main window operation was not aborted.");

                stop();
            }
        });

        this.startupPassed.set(true);

        ConsoleController.ensureConsoleVisible();

        // show the booster table
        if (wizardSettings.isShowBoosterTable()) {
            LOGGER.info("Show the booster table is selected in preferences.");

            // open the booster table ...
            try {

                this.applicationEventPublisher.publishEvent(new MenuEvent(MenuEvent.Action.boosterTable));
            }
            catch (Exception ex) {
                LOGGER.warn("Open booster table failed.", ex);
            }
        }

        // show the RXTX log view
        if (wizardSettings.isShowRxTxView()) {
            LOGGER.info("Show the RXTX logview is selected in preferences.");

            // open the booster table ...
            try {

                this.applicationEventPublisher.publishEvent(new MenuEvent(MenuEvent.Action.rxtxView));
            }
            catch (Exception ex) {
                LOGGER.warn("Open RXTX logview failed.", ex);
            }
        }

        if (miscSettings.isLoadWorkspaceAtStartup()) {
            String workspacePath = this.miscSettings.getWorkspacePath();

            File loadFile = null;
            try {
                loadFile = new File(workspacePath, "workspace.xml");
                LOGGER.info("Load workspace from file: {}", loadFile);

                if (!loadFile.getParentFile().exists()) {
                    throw new IllegalArgumentException("The requested file is not available: " + loadFile);
                }

                try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(loadFile))) {

                    dockingContext.readXML(in);

                    LOGGER.info("Load workspace passed.");
                }
                catch (IOException | ParserConfigurationException | SAXException ioe) {
                    LOGGER.warn("Load workspace failed.", ioe);
                }

            }
            catch (Exception ex) {
                LOGGER.warn("Load workspace from file failed.", ex);
            }
        }

        subscribeConnectionStatusChanges();

        // evaluate the auto connect startup parameters
        boolean isAutoConnect = false;
        if (startupParams != null) {
            isAutoConnect = startupParams.isAutoConnect();
        }

        if (isAutoConnect) {
            LOGGER.info("Queue the autoConnect task.");
            SwingUtilities.invokeLater(() -> {
                // trigger the autoConnect
                try {
                    LOGGER.info("Try to autoconnect.");
                    MainController.this.applicationEventPublisher
                        .publishEvent(new BidibConnectionEvent(connectionId, BidibConnectionEvent.Action.connect));
                }
                catch (Exception ex) {
                    LOGGER.warn("Autoconnect failed: {}", ex);
                }
            });
        }
        else {
            if (wizardSettings.isUseHotPlugController()) {
                try {
                    LOGGER.info("Create and start AlertController.");
                    alertController.start();

                    LOGGER.info("Create and start AlertController has finished.");
                }
                catch (Exception ex) {
                    LOGGER.warn("Start AlertController for USB devices failed.", ex);
                }
                catch (Error err) {
                    LOGGER.warn("Start AlertController for USB devices failed.", err);
                }
            }
            else {
                LOGGER.info("The usage of hotplug controller is disabled in preferences.");
            }
        }

        showTipOfDay();
    }

    private void selectNode(final NodeInterface node) {

        // select the node details tab
        try {
            String searchKey = "tabPanel";
            LOGGER.info("Search for view with key: {}", searchKey);
            Dockable tabPanel = mainView.getDesktop().getContext().getDockableByKey(searchKey);
            DockUtils.selectWindow(tabPanel);
        }
        catch (Exception ex) {
            LOGGER.warn("Select tabPanel failed.", ex);
        }

        LOGGER.info("Set the new selected node in the model: {}", node);
        try {
            mainModel.setSelectedNode(node, false);
        }
        catch (NodeChangeVetoException ex) {
            LOGGER.warn("Change the selected node was vetoed: {}", ex.getMessage());

            mainView
                .setStatusText(String
                    .format(Resources.getString(MainController.class, "change-node-vetoed"),
                        mainModel.getSelectedNode()),
                    StatusBar.DISPLAY_NORMAL);

            // ask the user if he wants to discard the pending changes
            int result =
                JOptionPane
                    .showConfirmDialog(mainView.getFrame(),
                        Resources.getString(MainController.class, "discardPendingChanges.text"),
                        Resources.getString(MainController.class, "discardPendingChanges.title"),
                        JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);

            if (result != JOptionPane.OK_OPTION) {
                return;
            }

            // force the node change
            mainModel.setSelectedNode(node, true);
        }

        // use an executor
        selectedNodeChangeWorker.schedule(() -> {

            LOGGER.info("Process node changed starting, Node-changed-thread-{}", System.currentTimeMillis());

            try {
                SwingUtilities.invokeLater(() -> setWaitCursor());

                StopWatch sw = new StopWatch();
                sw.start();

                mainView
                    .setStatusText(String.format(Resources.getString(MainController.class, "load-config"), node),
                        StatusBar.DISPLAY_NORMAL);

                final Context context = new DefaultContext();

                // fetch the cv definition
                LOGGER.info("Load the CV definition because no vendorCV is stored in node: {}", node);
                VendorCvData vendorCV = cvDefinitionPanelController.loadCvDefinition(context, node);

                boolean nodeHasLimitations = true;
                if (node != null) {
                    // ignore sys errors
                    nodeHasLimitations = node.isBootloaderNode() || node.isNodeHasError(true);

                    LOGGER.info("The node was checked for limitiations: {}", nodeHasLimitations);
                }

                sw.stop();
                LOGGER.info("Process node changed took: {}", sw);
                if (vendorCV != null) {
                    mainView
                        .setStatusText(
                            String.format(Resources.getString(MainController.class, "load-config-finished"), node, sw),
                            StatusBar.DISPLAY_NORMAL);
                }
                else {
                    List vendorCVErrors =
                        context.get(VendorCvFactory.VENDORCV_ERRORS, List.class, Collections.emptyList());

                    if (CollectionUtils.isNotEmpty(vendorCVErrors)) {
                        LOGGER.warn("Load vendor CV data from files failed: {}", vendorCVErrors);
                        mainView
                            .setStatusText(String
                                .format(
                                    Resources
                                        .getString(MainController.class, "load-config-finished-load-vendorcv-failed"),
                                    node, sw),
                                StatusBar.DISPLAY_ERROR);
                    }
                    else {
                        mainView
                            .setStatusText(String
                                .format(Resources.getString(MainController.class, "load-config-finished-no-vendorcv"),
                                    node, sw),
                                StatusBar.DISPLAY_NORMAL);
                    }
                }
            }
            catch (Exception ex) {
                LOGGER.warn("Process node change caused an error.", ex);
            }
            finally {
                SwingUtilities.invokeLater(() -> setDefaultCursor());
                LOGGER.info("Process node changed finished.");
            }
        }, 0, TimeUnit.MILLISECONDS);
    }

    private void subscribeConnectionStatusChanges() {
        LOGGER.info("Subscribe to connection status changes. Currently configured connectionId: {}", this.connectionId);

        // register for connection events on connection service because we want to know if the main connection is
        // opened
        Disposable dispConnectionStatusChanges = this.connectionService.subscribeConnectionStatusChanges(ci -> {
            LOGGER.info("The connection status has changed: {}", ci);

            if (this.connectionId.equals(ci.getConnectionId())) {
                LOGGER.info("The status of the configured connection has changed.");

                if (ci.getConnectionState().getActualPhase() == ConnectionPhase.CONNECTING) {
                    LOGGER.info("Connection is now connecting, connectionId: {}", this.connectionId);

                    try {
                        final BidibConnection conn = MainController.this.connectionService.find(this.connectionId);
                        if (conn != null) {
                            LOGGER.info("Set the node provider to the model.");
                            MainController.this.mainModel.setNodeProvider(conn.getNodeProvider());
                        }
                        else {
                            LOGGER
                                .warn(
                                    "Cannot set the node provider in the model because no connection found with id: {}",
                                    this.connectionId);
                        }
                    }
                    catch (Exception ex) {
                        LOGGER
                            .warn(
                                "Find the connection and set the node provider in the model failed. Current connectionId: {}",
                                this.connectionId, ex);
                    }

                    compDispConnectionStatusChanges.dispose();
                    compDispConnectionStatusChanges.clear();
                }
            }

        }, err -> {
            LOGGER.warn("Subscription to connection status changes caused an error.", err);
            if (compDispConnectionStatusChanges != null) {
                compDispConnectionStatusChanges.dispose();
                compDispConnectionStatusChanges.clear();
            }
        }, () -> {
            LOGGER.info("The subscription to connection status changes has completed.");
        });

        compDispConnectionStatusChanges.add(dispConnectionStatusChanges);

    }

    private void showTipOfDay() {

        if (wizardSettings.isShowTipOfDay()) {
            LOGGER.info("Show tip of days.");

            final List> alertList = new LinkedList<>();

            final AlertListener alertListener = new AlertListener() {

                @Override
                public void alertAdded(final BidibAlert alert, AlertAction alertAction) {
                    LOGGER.info("Alert added: {}", alert);

                    switch (alertAction) {
                        case DEVICE_ADDED:
                            alertList.add(alert);
                            break;
                        default:
                            alertList.remove(alert);
                            break;
                    }
                }

                @Override
                public void alertRemoved(final BidibAlert alert) {
                    LOGGER.info("Alert removed: {}", alert);
                }

            };

            if (alertController != null) {
                alertController.addAlertListener(alertListener);
            }

            final TipOfDayClosedListener listener = new TipOfDayClosedListener() {

                @Override
                public void closed() {
                    LOGGER.info("The tips of day dialog was closed. Show an alert if available.");

                    for (BidibAlert alert : alertList) {
                        LOGGER.info("Show alert: {}", alert);
                        alertController.showAlert(alert, AlertAction.DEVICE_ADDED, mainView.getFrame());
                    }

                    // remove the alerts from the list
                    alertList.clear();

                    if (alertController != null) {
                        alertController.removeAlertListener(alertListener);
                    }
                }
            };

            try {
                tipOfDayController.start(listener);
            }
            catch (Exception ex) {
                LOGGER.warn("Start the tip of day controller failed.", ex);
            }
        }
        else {
            LOGGER.info("Show tip of days is disabled in settings.");
        }

    }

    @Override
    public void stop() {
        LOGGER.info("Stop the main controller.");

        selectedNodeChangeWorker.shutdownNow();

        compDispConnectionStatusChanges.dispose();

        mainView.saveWindowPosition();

        try {
            LOGGER.info("Dispose the compDisposable: {}", compDisposable);
            compDisposable.dispose();
        }
        catch (Exception ex) {
            LOGGER.warn("Dispose the compDisposable failed.", ex);
        }

        if (alertController != null) {
            try {
                alertController.stopWatcher();
            }
            catch (Exception ex) {
                LOGGER.warn("Stop usbHotPlugController failed.", ex);
            }
        }

        mainView.performShutdown();

        mainView.setVisible(false);
        mainView.getFrame().dispose();

        int exitCode = SpringApplication.exit(this.context, () -> 0);
        LOGGER.info("Exit with exitCode: {}", exitCode);
        System.exit(exitCode);
    }

    @Override
    public void savePendingChanges() {
        LOGGER.info("Save the pending changes of the current selected node.");

        mainView.savePendingChanges();

    }

    @Override
    public void addNodeListListener(NodeListListener nodeListListener) {
        LOGGER.info("Add new nodeListListener: {}", nodeListListener);
        mainModel.addNodeListListener(nodeListListener);
    }

    @Override
    public void removeNodeListListener(NodeListListener nodeListListener) {
        LOGGER.info("Remove nodeListListener: {}", nodeListListener);
        mainModel.removeNodeListListener(nodeListListener);
    }

    @Override
    public void addNodeSelectionListListener(NodeSelectionListener l) {
        mainModel.addNodeSelectionListener(l);
    }

    @Override
    public void removeNodeSelectionListener(NodeSelectionListener l) {
        mainModel.removeNodeSelectionListener(l);
    }

    @EventListener
    public void handleConnectionEvent(final BidibConnectionEvent event) {
        LOGGER.info("Received connection event: {}", event);

        SwingUtilities.invokeLater(() -> {

            if (this.connectionId.equals(event.getConnectionId())) {

                switch (event.getAction()) {
                    case connect:
                        openConnection();
                        break;
                    case disconnect:
                        try {
                            closeConnection();
                        }
                        catch (CloseAbortedException ex) {
                            LOGGER.warn("Close connection was aborted, reason: {}", ex.getMessage());
                        }
                        break;
                    default:
                        break;
                }
            }
        });

    }

    private final Object openThreadLock = new Object();

    private Future openConnectionFuture;

    @Override
    public void openConnection() {
        LOGGER.info("### Open the connection to BiDiB.");

        // create the context
        final Context context = new DefaultContext();

        doOpenConnection(context);
    }

    private void doOpenConnection(final Context context) {
        // Use the connection service to connect and disconnect

        if (connectionService.isConnected(this.connectionId)) {
            LOGGER.warn("The is opened already. Skip open connection.");
            return;
        }

        synchronized (openThreadLock) {

            // use a thread to open the port
            LOGGER.info("Start a thread to open the port.");

            if (openConnectionFuture != null) {

                LOGGER.warn("An openThread is already running!!!!");
                return;
            }

            final String openConnectionId = this.connectionId;

            // keep the openThread as an instance variable
            final Runnable openThread = () -> {
                LOGGER.info("The open thread is starting.");

                try {
                    final Function afterCreateOrFind = conn -> {
                        LOGGER.info("Open connection has passed, connectionId: {}", this.connectionId);

                        // if (localHostHandlerFactory != null) {
                        // LOGGER.info("Create the localHost handler.");
                        //
                        // // create the localhost handler
                        // BidibDistributedMessageListener localHostHandler =
                        // localHostHandlerFactory.createLocalHostHandler(conn);
                        // context.register(BidibDistributedMessageListener.class.getSimpleName(), localHostHandler);
                        // }

                        return conn;
                    };

                    connectionService.connect(openConnectionId, afterCreateOrFind, afterCreateOrFind, context);
                }
                catch (ConnectionException | InternalStartupException ex) {
                    LOGGER.warn("Connect failed.", ex);

                    String message = ex.getMessage();
                    if (StringUtils.isNotBlank(ex.getUri())) {
                        StringBuilder sb = new StringBuilder(ex.getUri());
                        sb.append("\r\n").append(ex.getMessage());
                        message = sb.toString();
                    }
                    showErrorDialog(Resources.getString(MainController.class, "open-connection.failed", message),
                        Resources.getString(MainController.class, "open-connection.title"));
                }

                LOGGER.info("+++ The openThread has finished.");
                openConnectionFuture = null;
            };

            LOGGER.info("Start the open thread.");
            openConnectionFuture = this.openConnectionWorker.submit(openThread);
        }

    }

    @Override
    public void listenNetBidib() {
        LOGGER.info("### Listen for incomng netBiDiB connections.");

        final Context context = new DefaultContext();

        // TODO make the constants for the keys
        context.register(NetBidibSocketType.class.getSimpleName(), NetBidibSocketType.serverSocket);

        doOpenConnection(context);
    }

    @Override
    public void closeConnection() {
        LOGGER.info("### Close the connection to BiDiB.");

        // ask the user to save all nodes if running simulation
        boolean isSimulation = connectionService.isSimulation(this.connectionId);
        LOGGER.info("The current connection is a simulation: {}", isSimulation);

        if (isSimulation) {
            boolean continueClose =
                showConfirmDialog(Resources.getString(MainController.class, "close-simulation-connection.message"),
                    Resources.getString(MainController.class, "close-simulation-connection.title"));
            if (!continueClose) {
                LOGGER.info("The user cancelled the disconnect.");
                throw new CloseAbortedException("The user cancelled the disconnect.");
            }
        }

        // check if we have pending changes
        try {
            this.mainModel.setSelectedNode(null, false);
        }
        catch (NodeChangeVetoException ex) {
            LOGGER.warn("The currently selected node has pending changes.", ex);

            boolean continueClose =
                showConfirmDialog(Resources.getString(MainController.class, "close-connection-pending-changes.message"),
                    Resources.getString(MainController.class, "close-connection-pending-changes.title"));
            if (!continueClose) {
                LOGGER.info("The user cancelled the disconnect.");
                throw new CloseAbortedException("The user cancelled the disconnect.");
            }

            // save the pending changes
            this.mainView.savePendingChanges();

            // clear selected node
            this.mainModel.setSelectedNode(null, true);
        }

        // check if a thread is running that reads the structure
        if (openConnectionFuture != null && !openConnectionFuture.isDone()) {
            LOGGER.warn("The openThread is running!!!! Wait for termination.");

            try {
                LOGGER.info("### Interrupt the open thread.");
                boolean cancelled = openConnectionFuture.cancel(true);
                LOGGER.info("### The open thread was cancelled: {}", cancelled);

                LOGGER.info("### The open thread has finished.");
            }
            catch (Exception ex) {
                LOGGER.warn("### Wait for termination of openThread failed.", ex);
            }
        }

        // disconnect from bidib
        try {
            connectionService.disconnect(this.connectionId);
        }
        catch (Exception ex) {
            LOGGER.warn("Disconnect connection failed.", ex);
        }

        LOGGER.info("The connection service has closed the connection.");

        if (!this.compDispConnectionStatusChanges.isDisposed()) {

            LOGGER
                .warn(
                    "The compDispConnectionStatusChanges is not disposed! Dispose now to add new subscription to connection status changes.");

            this.compDispConnectionStatusChanges.dispose();
            this.compDispConnectionStatusChanges.clear();
        }

        LOGGER
            .info(
                "Create new compositeDisposable for connection status changes and subscribe to connection status changes.");
        this.compDispConnectionStatusChanges = new CompositeDisposable();
        subscribeConnectionStatusChanges();
    }

    private void showErrorDialog(final String message, final String title) {
        if (SwingUtilities.isEventDispatchThread()) {
            JOptionPane.showMessageDialog(mainView.getFrame(), message, title, JOptionPane.ERROR_MESSAGE);
        }
        else {
            SwingUtilities
                .invokeLater(() -> JOptionPane
                    .showMessageDialog(mainView.getFrame(), message, title, JOptionPane.ERROR_MESSAGE));
        }
    }

    private boolean showConfirmDialog(final String message, final String title) {
        int result = JOptionPane.showConfirmDialog(mainView.getFrame(), message, title, JOptionPane.OK_CANCEL_OPTION);
        if (JOptionPane.OK_OPTION == result) {

            return true;
        }
        return false;
    }

    @Override
    public void allBoosterOff() {

        for (NodeInterface node : mainModel.getNodeProvider().getNodes()) {
            if (NodeUtils.hasBoosterFunctions(node.getUniqueId())) {
                LOGGER.info("Switch booster off: {}", node);
                try {
                    boosterService.setBoosterState(this.connectionId, node.getBoosterNode(), BoosterStatus.OFF);
                }
                catch (Exception ex) {
                    LOGGER.warn("Switch booster off failed for node: {}", node, ex);
                }
            }
        }
    }

    @Override
    public void allBoosterOn(boolean boosterAndCommandStation, CommandStationStatus requestedCommandStationState) {
        LOGGER
            .info("Switch all boosters on, boosterAndCommandStation: {}, requestedCommandStationState: {}",
                boosterAndCommandStation, requestedCommandStationState);

        if (boosterAndCommandStation) {
            LOGGER.info("Switch the command stations on before the boosters will be switched on.");

            // use the command station service to witch the command station on

            for (NodeInterface node : mainModel.getNodeProvider().getNodes()) {
                if (node.getCommandStationNode() != null && NodeUtils.hasCommandStationFunctions(node.getUniqueId())) {

                    try {
                        LOGGER.info("Send soft-stop to command station to stop all locos: {}", node);
                        commandStationService
                            .setCommandStationState(this.connectionId, node.getCommandStationNode(),
                                CommandStationStatus.SOFTSTOP);

                        Thread.sleep(300);

                        // LOGGER.info("Clear the loco buffer before switch on the command station, node: {}", node);
                        // commandStationService.clearLocoBuffer(node);
                        //
                        // Thread.sleep(300);
                    }
                    catch (Exception ex) {
                        LOGGER.warn("Switch command station to SOFTSTOP failed for node: {}", node, ex);
                    }

                    try {
                        LOGGER.info("Switch command station on: {}", node);
                        commandStationService
                            .setCommandStationState(this.connectionId, node.getCommandStationNode(),
                                requestedCommandStationState);
                    }
                    catch (Exception ex) {
                        LOGGER.warn("Switch command station on failed for node: {}", node, ex);
                    }
                }
            }

            LOGGER.info("Wait a little bit to let the command station send the DCC signal.");

            try {
                Thread.sleep(300);
            }
            catch (Exception ex) {
                LOGGER.warn("Wait a little bit to let the command station send the DCC signal was interrupted.", ex);
            }
        }

        LOGGER.info("Switch all boosters on.");

        for (NodeInterface node : mainModel.getNodeProvider().getNodes()) {
            if (NodeUtils.hasBoosterFunctions(node.getUniqueId())) {
                LOGGER.info("Switch booster on: {}", node);
                try {
                    boosterService.setBoosterState(this.connectionId, node.getBoosterNode(), BoosterStatus.ON);
                }
                catch (Exception ex) {
                    LOGGER.warn("Switch booster on failed for node: {}", node, ex);
                }
            }
        }
    }

    @Override
    public void resetNode(final NodeInterface node) {
        LOGGER.info("Reset the current node: {}", node);

        nodeService.reset(this.connectionId, node);
    }

    @Override
    public void transferAccessoryToNode(final NodeInterface node, final Accessory accessory) {
        LOGGER
            .info("Transfer the accessory to the node and save permanently: {}",
                (accessory != null ? accessory.getDebugString() : null));

        try {
            switchingNodeService.saveAccessory(this.connectionId, node.getSwitchingNode(), accessory);

            LOGGER.info("Transfer and save accessory passed.");
        }
        catch (InvalidConfigurationException ex) {
            LOGGER.warn("Transfer accessory or save accessory failed.", ex);

            mainModel.setNodeHasError(mainModel.getSelectedNode(), true);

            throw ex;
        }
    }

    @Override
    public void replacePortConfig(TargetType portType, final Map> portConfig) {
        LOGGER.info("Replace the port config, portType: {}, portConfig: {}", portType, portConfig);
        try {
            NodeInterface node = mainModel.getSelectedNode();

            switch (portType.getScriptingTargetType()) {
                case BACKLIGHTPORT:
                    BacklightPort backlightPort =
                        PortListUtils.findPortByPortNumber(node.getBacklightPorts(), portType.getPortNum().intValue());

                    if (backlightPort != null) {
                        // update the port config
                        backlightPort.setPortConfigX(portConfig);

                        // TODO make this wait for the response from the node
                        setPortParameters(node, backlightPort, portConfig);
                    }
                    else {
                        LOGGER.warn("No backlightPort available with port number: {}", portType.getPortNum());
                    }
                    break;
                case LIGHTPORT:
                    LightPort lightPort =
                        PortListUtils.findPortByPortNumber(node.getLightPorts(), portType.getPortNum().intValue());

                    if (lightPort != null) {
                        // update the port config
                        lightPort.setPortConfigX(portConfig);

                        // TODO make this wait for the response from the node
                        setPortParameters(node, lightPort, portConfig);
                    }
                    else {
                        LOGGER.warn("No lightPort available with port number: {}", portType.getPortNum());
                    }
                    break;
                case SERVOPORT:
                    ServoPort servoPort =
                        PortListUtils.findPortByPortNumber(node.getServoPorts(), portType.getPortNum());

                    if (servoPort != null) {
                        // update the port config
                        servoPort.setPortConfigX(portConfig);
                        // write to node

                        // TODO make this wait for the response from the node
                        setPortParameters(node, servoPort, portConfig);
                    }
                    else {
                        LOGGER.warn("No servoPort available with port number: {}", portType.getPortNum());
                    }
                    break;
                case SWITCHPORT:
                    SwitchPort switchPort =
                        PortListUtils.findPortByPortNumber(node.getEnabledSwitchPorts(), portType.getPortNum());

                    if (switchPort != null) {
                        // update the port config
                        switchPort.setPortConfigX(portConfig);

                        // update the port config
                        setPortParameters(node, switchPort, portConfig);

                    }
                    else {
                        LOGGER.warn("No switchPort available with port number: {}", portType.getPortNum());
                    }
                    break;
                case SWITCHPAIRPORT:
                    SwitchPairPort switchPairPort =
                        PortListUtils.findPortByPortNumber(node.getEnabledSwitchPairPorts(), portType.getPortNum());

                    if (switchPairPort != null) {
                        // update the port config
                        switchPairPort.setPortConfigX(portConfig);

                        // update the port config
                        setPortParameters(node, switchPairPort, portConfig);

                    }
                    else {
                        LOGGER.warn("No switchPairPort available with port number: {}", portType.getPortNum());
                    }
                    break;
                default:
                    LOGGER.warn("Unsupported port type detected: {}", portType);
                    break;
            }

        }
        catch (Exception ex) {
            LOGGER.warn("Replace port config on node failed.", ex);
        }
    }

    @Override
    public void mapPortType(TargetType portType) {
        LOGGER.info("Map the port to the new type, portType: {}", portType);

        NodeInterface node = mainModel.getSelectedNode();

        Port port = null;
        LcOutputType lcOutputType = null;
        switch (portType.getScriptingTargetType()) {
            case BACKLIGHTPORT:
                port = PortListUtils.findPortByPortNumber(node.getBacklightPorts(), portType.getPortNum().intValue());
                lcOutputType = LcOutputType.BACKLIGHTPORT;
                break;
            case INPUTPORT:
                port = PortListUtils.findPortByPortNumber(node.getInputPorts(), portType.getPortNum().intValue());
                lcOutputType = LcOutputType.INPUTPORT;
                break;
            case LIGHTPORT:
                port = PortListUtils.findPortByPortNumber(node.getLightPorts(), portType.getPortNum().intValue());
                lcOutputType = LcOutputType.LIGHTPORT;
                break;
            case MOTORPORT:
                port = PortListUtils.findPortByPortNumber(node.getMotorPorts(), portType.getPortNum().intValue());
                lcOutputType = LcOutputType.MOTORPORT;
                break;
            case SERVOPORT:
                port = PortListUtils.findPortByPortNumber(node.getServoPorts(), portType.getPortNum().intValue());
                lcOutputType = LcOutputType.SERVOPORT;
                break;
            case SOUNDPORT:
                port = PortListUtils.findPortByPortNumber(node.getSoundPorts(), portType.getPortNum().intValue());
                lcOutputType = LcOutputType.SOUNDPORT;
                break;
            case SWITCHPORT:
                port = PortListUtils.findPortByPortNumber(node.getSwitchPorts(), portType.getPortNum().intValue());
                lcOutputType = LcOutputType.SWITCHPORT;
                break;
            case SWITCHPAIRPORT:
                port = PortListUtils.findPortByPortNumber(node.getSwitchPairPorts(), portType.getPortNum().intValue());
                lcOutputType = LcOutputType.SWITCHPAIRPORT;
                break;
            default:
                LOGGER.warn("Unsupported port type detected: {}", portType);
                break;
        }

        if (port == null) {
            LOGGER.warn("No port found to map for portType: {}", portType);

            throw new RuntimeException("No port found to map for portType:" + portType);
        }

        Map> portConfig = new LinkedHashMap<>();

        switchingNodeService.setPortConfig(this.connectionId, node.getSwitchingNode(), port, lcOutputType, portConfig);
    }

    private void setPortParameters(
        final NodeInterface node, final Port port, final Map> portConfig) {

        switchingNodeService.setPortConfig(this.connectionId, node.getSwitchingNode(), port);
    }

    /**
     * Send notification to users subscribed on channel "/topic/connectionState".
     * 
     * @param error
     *            the error.
     */
    private void publishStatus(Throwable error) {
        LOGGER.error("Notify error on connectionState: {}", error);
    }

    /**
     * Send notification to users subscribed on channel "/topic/system/connectionState".
     * 
     * @param connectionPhase
     *            the connectionPhase.
     */
    private void publishStatus(String connectionId, final ConnectionPhase connectionPhase, String error) {
        LOGGER
            .info("Notify new connectionState, connectionId: {}, connectionPhase: {}, error: {}", connectionId,
                connectionPhase, error);

        if (connectionId.equals(this.connectionId)) {
            final ConnectionPhase prevConnectionPhase = connectionPhaseModel.getConnectionPhase();

            if (SwingUtilities.isEventDispatchThread()) {
                connectionPhaseModel.setConnectionPhase(connectionPhase);
            }
            else {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        connectionPhaseModel.setConnectionPhase(connectionPhase);
                    }
                });
            }

            switch (connectionPhase) {
                case CONNECTED:
                    LOGGER
                        .info("Port was opened is signalled, connectionId: {}, prevConnectionPhase: {}", connectionId,
                            prevConnectionPhase);

                    if (SwingUtilities.isEventDispatchThread()) {
                        mainModel.getStatusModel().setCd(true);
                        if (ConnectionPhase.STALL == prevConnectionPhase) {
                            showConsoleStall(connectionId, false);
                        }
                    }
                    else {
                        SwingUtilities.invokeLater(() -> {
                            mainModel.getStatusModel().setCd(true);
                            if (ConnectionPhase.STALL == prevConnectionPhase) {
                                showConsoleStall(connectionId, false);
                            }
                        });
                    }
                    break;
                case DISCONNECTED:
                    LOGGER.info("Port was closed is signalled, connectionId: {}", connectionId);
                    // clear all nodes
                    if (SwingUtilities.isEventDispatchThread()) {

                        handleClosed(connectionId);
                    }
                    else {
                        SwingUtilities.invokeLater(() -> handleClosed(connectionId));
                    }
                    break;
                case STALL:
                    SwingUtilities.invokeLater(() -> showConsoleStall(connectionId, true));
                    break;
                default:
                    break;

            }
        }
        else {
            LOGGER.info("The connectionId is not matching, stored: {}, provided: {}", this.connectionId, connectionId);
        }
    }

    private void showConsoleStall(String connectionId, boolean stall) {

        // notify the console
        // SwingUtilities.invokeLater(() -> {
        try {
            // ensure console is visible
            ConsoleController.ensureConsoleVisible();
            String consoleLineText = null;
            if (stall) {
                LOGGER.info("The stall state of the connection has been set to true, connectionId: {}", connectionId);
                // add line
                consoleLineText =
                    String
                        .format("The stall state of the connection has been set to true, connectionId: %s",
                            connectionId);
            }
            else {
                LOGGER.info("The stall state of the connection has been set to false., connectionId: {}", connectionId);
                // add line
                consoleLineText =
                    String
                        .format("The stall state of the connection has been set to false, connectionId: %s",
                            connectionId);
            }

            LOGGER.warn(consoleLineText);

            consoleService.addConsoleLine(ConsoleColor.black, consoleLineText);
        }
        catch (Exception ex) {
            LOGGER
                .warn("Add change of stall state of connection to console failed, connectionId: {}", connectionId, ex);
        }
        // });
    }

    private void handleClosed(final String connectionId) {
        LOGGER.info("Port was closed, set the status text and clear the nodes, connectionId: {}", connectionId);

        // stop the model clock
        mainModel.getStatusModel().setModelClockStartEnabled(false);

        clearNodes();

        mainModel.getStatusModel().setCd(false);

        mainModel.signalResetInitialLoadFinished();

        // release the cv definition
        // model.setCvDefinition(null);

        // release the pairing dialog
        if (pairingController != null) {
            LOGGER.info("Close the pairing dialog on disconnect.");
            pairingController.closeDialogs();
        }

        if (compDispConnectionStatusChanges.isDisposed()) {

            this.compDispConnectionStatusChanges = new CompositeDisposable();
            subscribeConnectionStatusChanges();
        }

        if (openConnectionFuture != null) {
            LOGGER.info("+++ The connection was closed. Free the openConnectionFuture.");
            openConnectionFuture = null;
        }
    }

    /**
     * Publish the error.
     * 
     * @param error
     *            the error.
     */
    private void publishAction(Throwable error) {
        LOGGER.error("Notify error on action: {}", error);
    }

    @EventListener
    public void handleMenuEvent(final MainControllerEvent event) {
        LOGGER.info("Handle the menu event: {}", event);

        SwingUtilities.invokeLater(() -> {
            switch (event.getAction()) {
                case allBoosterOff:
                    allBoosterOff();
                    break;
                case allBoosterOn:
                    final MainControllerBoosterOnEvent boosterOnEvent = (MainControllerBoosterOnEvent) event;
                    allBoosterOn(boosterOnEvent.isBoosterAndCommandStation(),
                        boosterOnEvent.getRequestedCommandStationState());
                    break;
                case stop:
                    stop();
                    break;
                case listenNetBidib:
                    listenNetBidib();
                    break;
                case savePendingChanges:
                    savePendingChanges();
                    break;
                default:
                    break;
            }
        });
    }

    private void handleNodeStatusEvent(
        final BidibConnection connection, final NodeStatusEvent nse, final CompositeDisposable disposable,
        final NodeInterface node, final Consumer nodeSelector) {

        LOGGER.info("Received nodeStatusEvent after node was loaded: {}", nse);

        if (nse.getConnectionId().equals(connection.getConnectionId())
            && StatusIdentifier.InitialLoadFinished == nse.getStatusIdentifier()
            && node.getUniqueId() == nse.getUniqueId()) {
            long uniqueId = nse.getUniqueId();
            LOGGER
                .info("Received InitialLoadFinished for node with uniqueId: {}", ByteUtils.formatHexUniqueId(uniqueId));

            disposable.dispose();

            try {
                nodeSelector.accept(node);
            }
            catch (Exception ex) {
                LOGGER.warn("Select fully loaded node failed.", ex);
            }
        }
    }

    @EventListener
    public void handleErrorEvent(final ErrorEvent event) {
        LOGGER.info("Handle the error event: {}", event);

        if (Objects.equals(this.connectionId, event.getConnectionId())
            && StringUtils.isNotBlank(event.getResourceKey())) {

            String message = Resources.getString(ErrorEvent.class, event.getResourceKey() + ".message");
            String title = Resources.getString(ErrorEvent.class, event.getResourceKey() + ".title");

            showErrorDialog(message, title);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy