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

org.bidib.wizard.localhost.LocalHostBidibDistributedMessageHandler Maven / Gradle / Ivy

package org.bidib.wizard.localhost;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.bidib.jbidibc.core.BidibDistributedMessageListener;
import org.bidib.jbidibc.core.node.BidibNode;
import org.bidib.jbidibc.messages.AccessoryState;
import org.bidib.jbidibc.messages.AddressData;
import org.bidib.jbidibc.messages.BidibLibrary;
import org.bidib.jbidibc.messages.DriveState;
import org.bidib.jbidibc.messages.Feature;
import org.bidib.jbidibc.messages.Node;
import org.bidib.jbidibc.messages.enums.BoosterState;
import org.bidib.jbidibc.messages.enums.ClassIdEnum;
import org.bidib.jbidibc.messages.enums.CommandStationState;
import org.bidib.jbidibc.messages.enums.DriveAcknowledge;
import org.bidib.jbidibc.messages.enums.IdentifyState;
import org.bidib.jbidibc.messages.enums.MessageClassEnum;
import org.bidib.jbidibc.messages.enums.TargetModeEnum;
import org.bidib.jbidibc.messages.exception.ProtocolException;
import org.bidib.jbidibc.messages.message.BidibGuestMessage;
import org.bidib.jbidibc.messages.message.BidibMessageInterface;
import org.bidib.jbidibc.messages.message.BidibRequestFactory;
import org.bidib.jbidibc.messages.message.GuestRequestSendMessage;
import org.bidib.jbidibc.messages.message.GuestRequestSubscribeMessage;
import org.bidib.jbidibc.messages.message.GuestRequestUnsubscribeMessage;
import org.bidib.jbidibc.messages.message.GuestResponseNotifyMessage;
import org.bidib.jbidibc.messages.message.GuestResponseSentMessage;
import org.bidib.jbidibc.messages.message.GuestResponseSubscriptionCountMessage;
import org.bidib.jbidibc.messages.message.GuestResponseSubscriptionMessage;
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.model.NodeInterface;
import org.bidib.wizard.api.model.NodeProvider;
import org.bidib.wizard.api.model.connection.AbstractMessageEvent;
import org.bidib.wizard.api.model.connection.BidibConnection;
import org.bidib.wizard.api.model.connection.GatewayConnection;
import org.bidib.wizard.api.model.connection.event.AccessoryStateMessageEvent;
import org.bidib.wizard.api.model.connection.event.BoosterDiagMessageEvent;
import org.bidib.wizard.api.model.connection.event.BoosterStateMessageEvent;
import org.bidib.wizard.api.model.connection.event.CommandStationDriveAcknowledgeMessageEvent;
import org.bidib.wizard.api.model.connection.event.CommandStationDriveManualMessageEvent;
import org.bidib.wizard.api.model.connection.event.CommandStationDriveStateMessageEvent;
import org.bidib.wizard.api.model.connection.event.CommandStationStateMessageEvent;
import org.bidib.wizard.api.model.connection.event.FeatureMessageEvent;
import org.bidib.wizard.api.model.connection.event.NodeLostMessageEvent;
import org.bidib.wizard.api.model.connection.event.SysIdentifyStateMessageEvent;
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.localhost.event.SubscriberEvent;
import org.bidib.wizard.localhost.event.SubscriberEvent.SubscribeAction;
import org.bidib.wizard.localhost.exception.InvalidSubscriptionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.reactivex.rxjava3.core.Scheduler;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;

/**
 * The {@code LocalHostBidibDistributedMessageHandler} provides the handling of guest messages.
 */
public class LocalHostBidibDistributedMessageHandler
    implements BidibDistributedMessageListener, BidibDistributedMessagePublisher {

    private static final Logger LOGGER = LoggerFactory.getLogger(LocalHostBidibDistributedMessageHandler.class);

    private final BidibConnection connection;

    private final NodeProvider nodeProvider;

    private final BidibRequestFactory requestFactory;

    private final CommandStationService commandStationService;

    private final SwitchingNodeService switchingNodeService;

    private CompositeDisposable compDispMessages = new CompositeDisposable();

    protected final Map> subscriptions = new HashMap<>();

    private final BlockingQueue receiveQueue = new LinkedBlockingQueue<>();

    private ScheduledExecutorService receiveQueueWorker;

    private AtomicBoolean processReceiveQueue = new AtomicBoolean();

    private final Scheduler computationScheduler;

    private PublishSubject subjectSubscriberEvents;

    private final BidibResponseSupplier bidibResponseSupplier;

    private static final class ReceiveMessageContainer {
        private final BidibNode bidibNode;

        private final BidibMessageInterface message;

        /**
         * Create a new container to transport the node and the message that is related to the node.
         * 
         * @param bidibNode
         *            the node that sent the message (origin of message)
         * @param message
         *            the message
         */
        public ReceiveMessageContainer(final BidibNode bidibNode, final BidibMessageInterface message) {
            this.bidibNode = bidibNode;
            this.message = message;
        }

        /**
         * @return the bidib node that sent the message
         */
        public BidibNode getBidibNode() {
            return bidibNode;
        }

        public BidibMessageInterface getMessage() {
            return message;
        }
    }

    /**
     * Create a new instance of the LocalHostBidibDistributedMessageHandler.
     * 
     * @param connection
     *            the connection
     * @param nodeProvider
     *            the node provider
     */
    public LocalHostBidibDistributedMessageHandler(final BidibConnection connection, final NodeProvider nodeProvider,
        final BidibRequestFactory requestFactory, final NodeService nodeService,
        final CommandStationService commandStationService, final SwitchingNodeService switchingNodeService) {
        LOGGER.info("Create new instance of LocalHostBidibDistributedMessageHandler.");

        this.connection = connection;
        this.nodeProvider = nodeProvider;
        this.requestFactory = requestFactory;
        this.commandStationService = commandStationService;
        this.switchingNodeService = switchingNodeService;

        this.subjectSubscriberEvents = PublishSubject.create();

        this.bidibResponseSupplier =
            new BidibResponseSupplier(this.connection, this.nodeProvider, this.requestFactory, nodeService,
                this.commandStationService, this.switchingNodeService);

        // subscribe to messages of the connection
        this.computationScheduler =
            RxJavaPlugins
                .createComputationScheduler(
                    new ThreadFactoryBuilder().setNameFormat("localHostSubjectMessagesWorkers-thread-%d").build());

        Disposable dispMessages =
            this.connection.getSubjectMessages().observeOn(this.computationScheduler).subscribe(me -> {
                LOGGER.debug("Received message event: {}", me);

                processMessageEvent(me);
            }, err -> {
                LOGGER.warn("The message subject has signalled an error.", err);
            });

        this.compDispMessages.add(dispMessages);

        final ThreadFactory namedThreadFactory =
            new ThreadFactoryBuilder().setNameFormat("localHostRcvQueueWorkers-thread-%d").build();
        receiveQueueWorker = Executors.newScheduledThreadPool(2, namedThreadFactory);

        final int receiveTimeout = 1000;

        processReceiveQueue.set(true);

        receiveQueueWorker.submit(() -> {

            LOGGER.info("Start the receiver queue.");

            while (processReceiveQueue.get()) {
                try {
                    final ReceiveMessageContainer messageContainer =
                        this.receiveQueue.poll(receiveTimeout, TimeUnit.MILLISECONDS);
                    if (messageContainer != null) {
                        handleDistributedMessage(messageContainer);
                    }
                }
                catch (InterruptedException ex) {
                    LOGGER.info("Process receive queue was interrupted and the receiver queue will terminate.");
                    break;
                }
                catch (Exception ex) {
                    LOGGER.warn("Process receive queue failed. Ignore and continue.");
                }
            }

            LOGGER.info("The receiver queue has finished.");
        });

        this.connection.setLocalHostEnabled(true);
    }

    @Override
    public void shutdown() {
        LOGGER.info("Shutdown the LocalHostBidibDistributedMessageHandler.");

        this.compDispMessages.dispose();

        LOGGER.info("Stop the receiver queue.");
        processReceiveQueue.set(false);

        receiveQueueWorker.shutdownNow();

        this.computationScheduler.shutdown();
    }

    @Override
    public void handleDistributedMessage(final BidibNode bidibNode, final BidibMessageInterface message) {

        LOGGER.info("handleDistributedMessage, node: {}, message: {}", bidibNode, message);

        // add the message to the receive queue
        final ReceiveMessageContainer messgeContainer = new ReceiveMessageContainer(bidibNode, message);
        this.receiveQueue.add(messgeContainer);
    }

    /**
     * Process the distributed message.
     * 
     * @param messgeContainer
     *            the container with the node and the message
     */
    private void handleDistributedMessage(final ReceiveMessageContainer messgeContainer) {

        final BidibNode bidibNode = messgeContainer.getBidibNode();
        final BidibMessageInterface message = messgeContainer.getMessage();

        int type = ByteUtils.getInt(message.getType());
        switch (type) {
            case BidibLibrary.MSG_GUEST_RESP_SUBSCRIPTION_COUNT:
            case BidibLibrary.MSG_GUEST_RESP_SUBSCRIPTION:
            case BidibLibrary.MSG_GUEST_RESP_SENT:
            case BidibLibrary.MSG_GUEST_RESP_NOTIFY:
                break;
            case BidibLibrary.MSG_GUEST_REQ_SUBSCRIBE:
                handleGuestReqSubscribe((GuestRequestSubscribeMessage) message, this.connection, bidibNode);
                break;
            case BidibLibrary.MSG_GUEST_REQ_UNSUBSCRIBE:
                handleGuestReqUnsubscribe((GuestRequestUnsubscribeMessage) message, this.connection, bidibNode);
                break;
            case BidibLibrary.MSG_GUEST_REQ_SEND:
                handleGuestReqSend((GuestRequestSendMessage) message, this.connection, bidibNode);
                break;
            default:
                break;
        }
    }

    /**
     *
     * @param guestRequestSubscribeMessage
     *            the message
     * @param connection
     *            the connection
     * @param bidibNode
     *            the bidib node that sent the message
     */
    protected void handleGuestReqSubscribe(
        final GuestRequestSubscribeMessage guestRequestSubscribeMessage, final BidibConnection connection,
        final BidibNode bidibNode) {
        LOGGER.info("Handle the GuestRequestSubscribeMessage: {}", guestRequestSubscribeMessage);

        // the address of the node that sent the message
        final byte[] originNodeAddress = guestRequestSubscribeMessage.getAddr();

        final TargetModeEnum targetMode = guestRequestSubscribeMessage.getTargetMode();
        int subscription = guestRequestSubscribeMessage.getSubscription();

        final NodeSubscriptionData nodeSubscriptionData =
            new NodeSubscriptionData(originNodeAddress, targetMode, bidibNode.getCachedUniqueId(), subscription);

        final int subscriptionUpstream = guestRequestSubscribeMessage.getSubscriptionUpstream();
        final int subscriptionDownstream = guestRequestSubscribeMessage.getSubscriptionDownstream();

        LOGGER
            .info(
                "Received subscribe request from node with address: {} ({}) for targetMode: {}, subscriptionUpstream: {}, subscriptionUpstream: {}, subscription: {}",
                NodeUtils.formatAddress(nodeSubscriptionData.getSubscriberAddress()),
                ByteUtils.getUniqueIdAsString(bidibNode.getCachedUniqueId()), targetMode, subscriptionUpstream,
                subscriptionDownstream, StringUtils.leftPad(Integer.toString(subscription, 2), 16, "0"));

        // int acknowledge = 0;

        // keep track of the nodes and messageClass that are registered

        prepareSubscription(subscription, nodeSubscriptionData);

        try {
            byte[] uniqueId = guestRequestSubscribeMessage.getTargetUniqueId();
            int ackSequence = guestRequestSubscribeMessage.getNum();
            publishResponseSubscription(targetMode, uniqueId, ackSequence, subscription, nodeSubscriptionData);
        }
        catch (ProtocolException ex) {
            LOGGER.warn("Publish guestResponseSubscriptionMessage failed.", ex);
        }
    }

    /**
     *
     * @param guestRequestSubscribeMessage
     *            the message
     * @param connection
     *            the connection
     * @param bidibNode
     *            the bidib node that sent the message
     */
    protected void handleGuestReqUnsubscribe(
        final GuestRequestUnsubscribeMessage guestRequestUnsubscribeMessage, final BidibConnection connection,
        final BidibNode bidibNode) {
        LOGGER.info("Handle the GuestRequestUnsubscribeMessage: {}", guestRequestUnsubscribeMessage);

        // the address of the node that sent the message
        final byte[] originNodeAddress = guestRequestUnsubscribeMessage.getAddr();

        final TargetModeEnum targetMode = guestRequestUnsubscribeMessage.getTargetMode();

        int subscription = guestRequestUnsubscribeMessage.getSubscription();

        final NodeSubscriptionData nodeSubscriptionData =
            new NodeSubscriptionData(originNodeAddress, targetMode, bidibNode.getCachedUniqueId(), subscription);

        final int subscriptionUpstream = guestRequestUnsubscribeMessage.getSubscriptionUpstream();
        final int subscriptionDownstream = guestRequestUnsubscribeMessage.getSubscriptionDownstream();

        LOGGER
            .info(
                "Received unsubscribe request from node with address: {} ({}) for targetMode: {}, subscriptionUpstream: {}, subscriptionUpstream: {}, subscription: {}",
                NodeUtils.formatAddress(nodeSubscriptionData.getSubscriberAddress()),
                ByteUtils.getUniqueIdAsString(bidibNode.getCachedUniqueId()), targetMode, subscriptionUpstream,
                subscriptionDownstream, StringUtils.leftPad(Integer.toString(subscription, 2), 16, "0"));

        // keep track of the nodes and messageClass that are registered

        // TODO handle unsubscribe

        prepareUnsubscribe(subscription, nodeSubscriptionData);

        try {
            byte[] uniqueId = guestRequestUnsubscribeMessage.getTargetUniqueId();
            int ackSequence = guestRequestUnsubscribeMessage.getNum();
            publishResponseSubscription(targetMode, uniqueId, ackSequence, subscription, nodeSubscriptionData);
        }
        catch (ProtocolException ex) {
            LOGGER.warn("Publish gateResponseSubscriptionMessage failed.", ex);
        }
    }

    protected void publishResponseSubscription(
        final TargetModeEnum targetMode, byte[] targetModeUniqueId, int ackSequence, int subscription,
        final NodeSubscriptionData nodeSubscriptionData) {

        int result = 0;

        try {
            // we must provide the response for all targetModes

            final List nodes = new ArrayList<>();

            switch (targetMode) {
                case BIDIB_TARGET_MODE_UID:
                    LOGGER
                        .info("Publish gateway response, uniqueId: {}",
                            ByteUtils.convertUniqueIdToString(targetModeUniqueId));

                    NodeInterface node = nodeProvider.findNodeByUniqueIdWithoutClass(targetModeUniqueId);
                    if (node == null) {
                        LOGGER
                            .warn("No node found with uniqueId: {}",
                                ByteUtils.convertUniqueIdToString(targetModeUniqueId));
                        result = 0xFF;
                    }
                    else {
                        nodes.add(node);
                    }
                    break;

                case BIDIB_TARGET_MODE_DCCGEN:
                    // check if DCC generator nodes are available
                    List dccGenNodes =
                        nodeProvider.findNodesByClass(ClassIdEnum.BIT_DCC_GEN_DRIVE_AND_SWITCH);
                    if (CollectionUtils.isEmpty(dccGenNodes)) {
                        LOGGER.warn("No DCCGEN node found.");
                        result = 0xFF;
                    }
                    else {
                        nodes.addAll(dccGenNodes);
                    }
                    break;
                case BIDIB_TARGET_MODE_BOOSTER:
                    // check if booster nodes are available
                    List boosterNodes = nodeProvider.findNodesByClass(ClassIdEnum.BIT_BOOSTER);
                    if (CollectionUtils.isEmpty(boosterNodes)) {
                        LOGGER.warn("No BOOSTER node found.");
                        result = 0xFF;
                    }
                    else {
                        nodes.addAll(boosterNodes);
                    }
                    break;
                case BIDIB_TARGET_MODE_ACCESSORY:
                    // check if accessory nodes are available
                    List accessoryNodes = nodeProvider.findNodesByClass(ClassIdEnum.BIT_ACCESSORY);
                    if (CollectionUtils.isEmpty(accessoryNodes)) {
                        LOGGER.warn("No ACCESSORY node found.");
                        result = 0xFF;
                    }
                    else {
                        nodes.addAll(accessoryNodes);
                    }
                    break;
                case BIDIB_TARGET_MODE_SWITCH:
                    // check if switching nodes are available
                    List switchNodes = nodeProvider.findNodesByClass(ClassIdEnum.BIT_SWITCHING);
                    if (CollectionUtils.isEmpty(switchNodes)) {
                        LOGGER.warn("No SWITCHING node found.");
                        result = 0xFF;
                    }
                    else {
                        nodes.addAll(switchNodes);
                    }
                    break;
                case BIDIB_TARGET_MODE_TOP:
                    node = nodeProvider.findNodeByAddress(Node.ROOTNODE_ADDR);
                    if (node == null) {
                        LOGGER.warn("No root node found!");
                        result = 0xFF;
                    }
                    else {
                        nodes.add(node);
                    }
                    break;
                default:
                    LOGGER.warn("Unsupported targetMode detected: {}", targetMode);

                    throw new InvalidSubscriptionException(0xFF, "Unsupported targetMode detected: " + targetMode);
            }

            if (!nodes.isEmpty()) {

                int nodeCount = nodes.size();

                try {
                    // byte[] uniqueId = guestRequestUnsubscribeMessage.getUniqueId();

                    // send the RESP_SUBSCRIPTION_COUNT
                    final GuestResponseSubscriptionCountMessage guestResponseSubscriptionCountMessage =
                        requestFactory
                            .createGuestResponseSubscriptionCountMessage(nodeSubscriptionData.getSubscriberAddress(), 0,
                                targetMode, targetModeUniqueId, ackSequence, result, nodeCount);

                    publishMessage(nodeSubscriptionData, guestResponseSubscriptionCountMessage);
                }
                catch (ProtocolException ex1) {
                    LOGGER.warn("Publish guestResponseSubscriptionCountMessage failed.", ex1);
                }

                // send RESP_SUBSCRIPTION
                for (NodeInterface node : nodes) {

                    try {
                        byte[] nodeUniqueId = NodeUtils.getTargetModeUniqueId(node.getUniqueId());

                        // send the MSG_GUEST_RESP_SUBSCRIPTION to the node
                        final GuestResponseSubscriptionMessage guestResponseSubscriptionMessage =
                            requestFactory
                                .createGuestResponseSubscriptionMessage(nodeSubscriptionData.getSubscriberAddress(), 0,
                                    targetMode, nodeUniqueId, ackSequence, result, subscription);

                        publishMessage(nodeSubscriptionData, guestResponseSubscriptionMessage);
                    }
                    catch (ProtocolException ex1) {
                        LOGGER.warn("Publish guestResponseSubscriptionMessage failed.", ex1);
                    }
                }
            }
        }
        catch (InvalidSubscriptionException ex) {
            LOGGER.warn("The subscription is not supported.", ex);

            int nodeCount = 1;
            result = ex.getResult();

            try {
                // send the RESP_SUBSCRIPTION_COUNT
                final GuestResponseSubscriptionCountMessage guestResponseSubscriptionCountMessage =
                    requestFactory
                        .createGuestResponseSubscriptionCountMessage(nodeSubscriptionData.getSubscriberAddress(), 0,
                            targetMode, targetModeUniqueId, ackSequence, result, nodeCount);

                publishMessage(nodeSubscriptionData, guestResponseSubscriptionCountMessage);
            }
            catch (ProtocolException ex1) {
                LOGGER.warn("Publish guestResponseSubscriptionCountMessage failed.", ex1);
            }

            // send the MSG_GUEST_RESP_SUBSCRIPTION to the node
            try {
                final GuestResponseSubscriptionMessage guestResponseSubscriptionMessage =
                    requestFactory
                        .createGuestResponseSubscriptionMessage(nodeSubscriptionData.getSubscriberAddress(), 0,
                            targetMode, targetModeUniqueId, ackSequence, result, subscription);

                publishMessage(nodeSubscriptionData, guestResponseSubscriptionMessage);
            }
            catch (ProtocolException ex1) {
                LOGGER.warn("Publish guestResponseSubscriptionMessage failed.", ex1);
            }
        }
    }

    protected void prepareSubscription(int subscriptionClasses, final NodeSubscriptionData nodeSubscriptionData) {

        for (MessageClassEnum messageClass : MessageClassEnum.values()) {

            if (messageClass == MessageClassEnum.BIDIB_MESSAGE_CLASS_NOTHING) {
                continue;
            }

            boolean isActive = MessageClassEnum.isAnyActive(messageClass, subscriptionClasses);
            LOGGER.info("Current subscription message class: {}, isActive: {}", messageClass, isActive);

            try {

                List subscriptionDataList = this.subscriptions.get(messageClass);
                if (subscriptionDataList == null) {
                    LOGGER
                        .info("No existing subscriptions for message class found: {}, current nodeSubscriptionData: {}",
                            messageClass, nodeSubscriptionData);

                    if (!isActive) {
                        LOGGER.info("The subscription is not active.");
                        continue;
                    }

                    // no existing subscriptions for message class found
                    subscriptionDataList = new ArrayList<>();
                    this.subscriptions.put(messageClass, subscriptionDataList);
                    subscriptionDataList.add(nodeSubscriptionData);

                    subjectSubscriberEvents
                        .onNext(new SubscriberEvent(SubscribeAction.add, nodeSubscriptionData.getSubscriberAddress(),
                            subscriptionClasses).withUniqueId(nodeSubscriptionData.getSubscriberUniqueId()));
                }
                else {
                    LOGGER.info("Found existing subscriptions for message class: {}", messageClass);

                    // check if the subscription for the node is available already
                    NodeSubscriptionData existingSubscriptionData =
                        IterableUtils
                            .find(subscriptionDataList, data -> Objects
                                .equals(data.getSubscriberUniqueId(), nodeSubscriptionData.getSubscriberUniqueId()));

                    if (existingSubscriptionData != null) {
                        LOGGER
                            .info("The original uniqueId is registered already: {}",
                                ByteUtils.formatHexUniqueId(nodeSubscriptionData.getSubscriberUniqueId()));
                        if (!isActive) {
                            LOGGER.info("Remove subscription for messageClass: {}", messageClass);
                            subscriptionDataList.remove(nodeSubscriptionData);

                            subjectSubscriberEvents
                                .onNext(new SubscriberEvent(SubscribeAction.add,
                                    nodeSubscriptionData.getSubscriberAddress(), subscriptionClasses)
                                        .withUniqueId(nodeSubscriptionData.getSubscriberUniqueId()));
                        }
                        else {
                            LOGGER.info("The subscription is still active: {}", messageClass);
                        }
                    }
                    else {
                        LOGGER
                            .info("Register uniqueId: {}, messageClass: {}",
                                ByteUtils.formatHexUniqueId(nodeSubscriptionData.getSubscriberUniqueId()),
                                messageClass);
                        subscriptionDataList.add(nodeSubscriptionData);

                        subjectSubscriberEvents
                            .onNext(
                                new SubscriberEvent(SubscribeAction.add, nodeSubscriptionData.getSubscriberAddress(),
                                    subscriptionClasses).withUniqueId(nodeSubscriptionData.getSubscriberUniqueId()));
                    }

                }
            }
            catch (Exception ex) {
                LOGGER.warn("Prepare subscriptions failed.", ex);
                // TODO: handle exception
            }
        }
    }

    protected void prepareUnsubscribe(int subscriptionClasses, final NodeSubscriptionData nodeSubscriptionData) {

        // handle the unsubscribe request
        for (MessageClassEnum messageClass : MessageClassEnum.values()) {

            if (messageClass == MessageClassEnum.BIDIB_MESSAGE_CLASS_NOTHING) {
                continue;
            }

            boolean isActive = MessageClassEnum.isAnyActive(messageClass, subscriptionClasses);
            LOGGER.info("Current subscription message class: {}, isActive: {}", messageClass, isActive);

            try {

                List subscriptionDataList = this.subscriptions.get(messageClass);
                if (subscriptionDataList == null) {
                    LOGGER
                        .info("No existing subscriptions for message class found: {}, current nodeSubscriptionData: {}",
                            messageClass, nodeSubscriptionData);

                    // if (!isActive) {
                    // LOGGER.info("The subscription is not active.");
                    // continue;
                    // }
                    //
                    // // no existing subscriptions for message class found
                    // subscriptionDataList = new ArrayList<>();
                    // this.subscriptions.put(messageClass, subscriptionDataList);
                    // subscriptionDataList.add(nodeSubscriptionData);
                    //
                    // subjectSubscriberEvents
                    // .onNext(new SubscriberEvent(SubscribeAction.add, nodeSubscriptionData.getSubscriberAddress(),
                    // subscriptionClasses).withUniqueId(nodeSubscriptionData.getSubscriberUniqueId()));
                }
                else {
                    LOGGER.info("Found existing subscriptions for message class: {}", messageClass);

                    // check if the subscription for the node is available already
                    NodeSubscriptionData existingSubscriptionData =
                        IterableUtils
                            .find(subscriptionDataList, data -> Objects
                                .equals(data.getSubscriberUniqueId(), nodeSubscriptionData.getSubscriberUniqueId()));

                    if (existingSubscriptionData != null) {
                        LOGGER
                            .info("The original uniqueId is registered already: {}",
                                ByteUtils.formatHexUniqueId(nodeSubscriptionData.getSubscriberUniqueId()));
                        if (isActive) {
                            LOGGER.info("Remove subscription for messageClass: {}", messageClass);
                            subscriptionDataList.remove(nodeSubscriptionData);

                            subjectSubscriberEvents
                                .onNext(new SubscriberEvent(SubscribeAction.remove,
                                    nodeSubscriptionData.getSubscriberAddress(), subscriptionClasses)
                                        .withUniqueId(nodeSubscriptionData.getSubscriberUniqueId()));
                        }
                        else {
                            LOGGER.info("The subscription is still active: {}", messageClass);
                        }
                    }
                    else {
                        LOGGER
                            .info("Register uniqueId: {}, messageClass: {}",
                                ByteUtils.formatHexUniqueId(nodeSubscriptionData.getSubscriberUniqueId()),
                                messageClass);
                        subscriptionDataList.add(nodeSubscriptionData);

                        subjectSubscriberEvents
                            .onNext(
                                new SubscriberEvent(SubscribeAction.remove, nodeSubscriptionData.getSubscriberAddress(),
                                    subscriptionClasses).withUniqueId(nodeSubscriptionData.getSubscriberUniqueId()));
                    }

                }
            }
            catch (Exception ex) {
                LOGGER.warn("Prepare subscriptions failed.", ex);
                // TODO: handle exception
            }
        }
    }

    private final Map nodeContainerMap =
        Collections.synchronizedMap(new LinkedHashMap());

    private BidibNodeContainer getBidibNodeContainer(Long uniqueId) {
        BidibNodeContainer bidibNodeContainer = null;
        synchronized (nodeContainerMap) {
            bidibNodeContainer = nodeContainerMap.get(uniqueId);
            if (bidibNodeContainer == null) {
                bidibNodeContainer = new BidibNodeContainer(uniqueId);
                nodeContainerMap.put(uniqueId, bidibNodeContainer);
            }
        }
        return bidibNodeContainer;
    }

    protected void handleGuestReqSend(
        final GuestRequestSendMessage guestRequestSendMessage, final BidibConnection connection,
        final BidibNode bidibNode) {
        LOGGER.info("Handle the GuestRequestSendMessage: {}", guestRequestSendMessage);

        final byte[] originNodeAddress = guestRequestSendMessage.getAddr();
        final TargetModeEnum targetMode = guestRequestSendMessage.getTargetMode();

        final NodeInterface targetNode;
        try {
            targetNode = findNodeByTargetMode(guestRequestSendMessage, targetMode);
            LOGGER.info("Resolved the target node: {}, targetMode: {}", targetNode, targetMode);
        }
        catch (ProtocolException ex) {
            LOGGER.warn("Find node by targetMode failed. Discard handle message: {}", guestRequestSendMessage, ex);

            // send error
            int result = 0xFF; // no existing node
            int ackSequence = guestRequestSendMessage.getNum();

            try {
                // send the MSG_GUEST_RESP_SENT to the node
                final GuestResponseSentMessage guestResponseSentMessage =
                    this.requestFactory
                        .createGuestResponseSentMessage(originNodeAddress, 0, targetMode, new byte[] { 0, 0, 0, 0, 0 },
                            ackSequence, result);

                final NodeSubscriptionData nodeSubscriptionData =
                    new NodeSubscriptionData(originNodeAddress, targetMode, 0L, null);

                publishMessage(nodeSubscriptionData, guestResponseSentMessage);
            }
            catch (ProtocolException ex1) {
                LOGGER.info("Process command from wrapped message failed.", ex1);
            }
            return;
        }

        // get the origin node by it's address
        final NodeInterface originNode = nodeProvider.findNodeByAddress(originNodeAddress);
        LOGGER.info("Resolved the origin node: {}", originNode);

        // get the node container for the target node
        final BidibNodeContainer bidibNodeContainer = getBidibNodeContainer(targetNode.getUniqueId());

        // force subscription on origin node

        final NodeSubscriptionData nodeSubscriptionData =
            new NodeSubscriptionData(originNodeAddress, targetMode, originNode.getUniqueId(), null);
        LOGGER.info("Force subscription, nodeSubscriptionData: {}", nodeSubscriptionData);

        int subscription = 0xFFFF;
        prepareSubscription(subscription, nodeSubscriptionData);

        LOGGER.info("Current originNodeAddress: {}, targetMode: {}", originNodeAddress, targetMode);

        this.bidibResponseSupplier
            .processMessages(this.requestFactory, guestRequestSendMessage, targetNode, targetMode, nodeSubscriptionData,
                bidibNodeContainer, this);
    }

    private NodeInterface findNodeByTargetMode(final BidibGuestMessage guestMessage, final TargetModeEnum targetMode)
        throws ProtocolException {
        NodeInterface node = null;

        LOGGER.info("Find node by targetMode: {}", targetMode);

        switch (targetMode) {
            case BIDIB_TARGET_MODE_UID:
                node = nodeProvider.findNodeByUniqueIdWithoutClass(guestMessage.getTargetUniqueId());
                break;

            case BIDIB_TARGET_MODE_DCCGEN:
                // get the first DCC generator
                List dccGenNodes =
                    nodeProvider.findNodesByClass(ClassIdEnum.BIT_DCC_GEN_DRIVE_AND_SWITCH);
                if (CollectionUtils.isNotEmpty(dccGenNodes)) {
                    node = dccGenNodes.get(0);
                }
                break;
            case BIDIB_TARGET_MODE_BOOSTER:
                // get the first booster
                List boosterNodes = nodeProvider.findNodesByClass(ClassIdEnum.BIT_BOOSTER);
                if (CollectionUtils.isNotEmpty(boosterNodes)) {
                    node = boosterNodes.get(0);
                }
                break;
            case BIDIB_TARGET_MODE_ACCESSORY:
                // get the first accessory node
                List accessoryNodes = nodeProvider.findNodesByClass(ClassIdEnum.BIT_ACCESSORY);
                if (CollectionUtils.isNotEmpty(accessoryNodes)) {
                    node = accessoryNodes.get(0);
                }
                break;
            case BIDIB_TARGET_MODE_TOP:
                node = nodeProvider.findNodeByAddress(Node.ROOTNODE_ADDR);
                break;

            default:
                LOGGER.warn("Unsupported target mode detected: {}", targetMode);
                break;
        }

        LOGGER.info("The found node: {}", node);

        if (node == null) {
            throw new ProtocolException("Requested node not found.");
        }

        return node;
    }

    /**
     * Process the message event that is received from the interface.
     * 
     * @param me
     *            the message event
     */
    private void processMessageEvent(final AbstractMessageEvent me) {

        switch (me.getMessageType()) {

            case BidibLibrary.MSG_CS_STATE:
                notifyCommandStationSubscribers((CommandStationStateMessageEvent) me);
                break;
            case BidibLibrary.MSG_CS_DRIVE_ACK:
                notifyCommandStationSubscribers((CommandStationDriveAcknowledgeMessageEvent) me);
                break;
            case BidibLibrary.MSG_CS_DRIVE_MANUAL:
                notifyCommandStationSubscribers((CommandStationDriveManualMessageEvent) me);
                break;
            case BidibLibrary.MSG_CS_DRIVE_STATE:
                notifyCommandStationSubscribers((CommandStationDriveStateMessageEvent) me);
                break;
            case BidibLibrary.MSG_ACCESSORY_STATE:
                notifyAccessorySubscribers((AccessoryStateMessageEvent) me);
                break;
            case BidibLibrary.MSG_BOOST_STAT:
                notifyBoosterSubscribers((BoosterStateMessageEvent) me);
                break;
            case BidibLibrary.MSG_BOOST_DIAGNOSTIC:
                notifyBoosterSubscribers((BoosterDiagMessageEvent) me);
                break;
            case BidibLibrary.MSG_NODE_LOST:
                notifyNodeLost((NodeLostMessageEvent) me);
                break;
            case BidibLibrary.MSG_SYS_IDENTIFY_STATE:
                notifySystemSubscribers((SysIdentifyStateMessageEvent) me);
                break;
            case BidibLibrary.MSG_FEATURE:
                notifyFeatureAndUserSubscribers((FeatureMessageEvent) me);
                break;
            default:
                break;
        }

    }

    private void notifyNodeLost(NodeLostMessageEvent me) {
        LOGGER.info("Node lost: {}", me.getNode());

        // the node address of the lost node
        byte[] nodeAddress = me.getNode().getAddr();

        for (Entry> entry : this.subscriptions.entrySet()) {

            // create a copy of the addresses
            final List subscriptionDataList = new ArrayList<>(entry.getValue());
            for (NodeSubscriptionData subscriptionData : subscriptionDataList) {

                // check if the node or a subnode of the node is registered
                if (ByteUtils.arrayEquals(subscriptionData.getSubscriberAddress(), nodeAddress)
                    || NodeUtils.isSubNode(nodeAddress, subscriptionData.getSubscriberAddress())) {
                    LOGGER.info("Remove address: {}", NodeUtils.formatAddress(subscriptionData.getSubscriberAddress()));
                    entry.getValue().remove(subscriptionData);
                }
            }
        }
    }

    private List getSubscriptionDataList(final MessageClassEnum messageClass) {
        final List subscriptionDataList = subscriptions.get(messageClass);
        return subscriptionDataList != null ? subscriptionDataList : Collections.emptyList();
    }

    private void notifyFeatureAndUserSubscribers(final FeatureMessageEvent me) {
        LOGGER.info("Publish the feature message event to the guests: {}", me);

        try {
            // fetch all address node data that are registered for the message class
            final List subscriptionDataList =
                getSubscriptionDataList(MessageClassEnum.BIDIB_MESSAGE_CLASS_FEATURE_AND_USER);
            LOGGER.info("Fetched subscriptions for class FEATURE_AND_USER: {}", subscriptionDataList);

            if (!subscriptionDataList.isEmpty()) {

                final Feature feature = me.getFeature();
                byte[] nodeAddress = me.getAddress(); // the address of the node
                int messageNum = me.getMessageNum();
                final NodeInterface originNode = nodeProvider.findNodeByAddress(me.getAddress());

                final BidibMessageInterface featureResponse =
                    requestFactory.createFeatureResponse(nodeAddress, messageNum, feature);

                publishAsGuestResponseNotifyMessage(originNode, featureResponse, subscriptionDataList);
            }
        }
        catch (ProtocolException ex) {
            LOGGER.warn("Publish guestResponseNotifyMessage failed.", ex);
        }
    }

    private void notifySystemSubscribers(final SysIdentifyStateMessageEvent me) {
        try {
            // fetch all address node data that are registered for the message class
            final List subscriptionDataList =
                getSubscriptionDataList(MessageClassEnum.BIDIB_MESSAGE_CLASS_SYSTEM);
            LOGGER.info("Fetched subscriptions for class SYSTEM: {}", subscriptionDataList);

            if (!subscriptionDataList.isEmpty()) {

                final IdentifyState identifyState = me.getIdentifyState();
                byte[] nodeAddress = me.getAddress(); // the address of the node
                int messageNum = me.getMessageNum();
                final NodeInterface originNode = nodeProvider.findNodeByAddress(me.getAddress());

                final BidibMessageInterface csStateResponse =
                    requestFactory.createSysIdentifyStateResponse(nodeAddress, messageNum, identifyState);

                publishAsGuestResponseNotifyMessage(originNode, csStateResponse, subscriptionDataList);
            }
        }
        catch (ProtocolException ex) {
            LOGGER.warn("Publish guestResponseNotifyMessage failed.", ex);
        }
    }

    private void notifyCommandStationSubscribers(final CommandStationStateMessageEvent me) {

        try {
            // fetch all address node data that are registered for the message class
            final List subscriptionDataList =
                getSubscriptionDataList(MessageClassEnum.BIDIB_MESSAGE_CLASS_COMMANDSTATION);
            LOGGER.info("Fetched subscriptions for class COMMANDSTATION: {}", subscriptionDataList);

            if (!subscriptionDataList.isEmpty()) {

                final CommandStationState csState = me.getCommandStationState();
                byte[] nodeAddress = me.getAddress(); // the address of the command station node
                int messageNum = me.getMessageNum();
                final NodeInterface originNode = nodeProvider.findNodeByAddress(me.getAddress());

                final BidibMessageInterface csStateResponse =
                    requestFactory.createCommandStationStateResponse(nodeAddress, messageNum, csState);

                publishAsGuestResponseNotifyMessage(originNode, csStateResponse, subscriptionDataList);
            }
        }
        catch (ProtocolException ex) {
            LOGGER.warn("Publish guestResponseNotifyMessage failed.", ex);
        }
    }

    private void notifyCommandStationSubscribers(final CommandStationDriveAcknowledgeMessageEvent me) {

        try {
            final List subscriptionDataList =
                getSubscriptionDataList(MessageClassEnum.BIDIB_MESSAGE_CLASS_COMMANDSTATION);
            LOGGER.info("Fetch subscriptions for class COMMANDSTATION: {}", subscriptionDataList);

            if (!subscriptionDataList.isEmpty()) {

                byte[] nodeAddress = me.getAddress();
                int messageNum = me.getMessageNum();
                final NodeInterface originNode = nodeProvider.findNodeByAddress(me.getAddress());

                final DriveAcknowledge driveAckn = me.getDriveAcknowledge();
                final int dccAddress = me.getDccAddress();
                final Integer acknowledgedMessageNumber = me.getAcknowledgedMessageNumber();

                final BidibMessageInterface csStateResponse =
                    requestFactory
                        .createCommandStationDriveAckResponse(nodeAddress, messageNum,
                            new AddressData(dccAddress, null), driveAckn, acknowledgedMessageNumber);

                publishAsGuestResponseNotifyMessage(originNode, csStateResponse, subscriptionDataList);
            }
        }
        catch (ProtocolException ex) {
            LOGGER.warn("Publish guestResponseNotifyMessage failed.", ex);
        }
    }

    private void notifyCommandStationSubscribers(final CommandStationDriveManualMessageEvent me) {

        try {
            byte[] nodeAddress = me.getAddress();
            int messageNum = me.getMessageNum();
            final NodeInterface originNode = nodeProvider.findNodeByAddress(me.getAddress());

            final DriveState driveState = me.getDriveState();

            final BidibMessageInterface csStateResponse =
                requestFactory.createCommandStationDriveManualResponse(nodeAddress, messageNum, driveState);

            final List subscriptionDataList =
                getSubscriptionDataList(MessageClassEnum.BIDIB_MESSAGE_CLASS_COMMANDSTATION);
            LOGGER.info("Fetch subscriptions for class COMMANDSTATION: {}", subscriptionDataList);
            publishAsGuestResponseNotifyMessage(originNode, csStateResponse, subscriptionDataList);
        }
        catch (ProtocolException ex) {
            LOGGER.warn("Publish guestResponseNotifyMessage failed.", ex);
        }
    }

    private void notifyCommandStationSubscribers(final CommandStationDriveStateMessageEvent me) {

        try {
            byte[] nodeAddress = me.getAddress();
            int messageNum = me.getMessageNum();
            final NodeInterface originNode = nodeProvider.findNodeByAddress(me.getAddress());

            final int opCode = me.getOpCode();
            final DriveState driveState = me.getDriveState();

            final BidibMessageInterface csStateResponse =
                requestFactory.createCommandStationDriveStateResponse(nodeAddress, messageNum, opCode, driveState);

            final List subscriptionDataList =
                getSubscriptionDataList(MessageClassEnum.BIDIB_MESSAGE_CLASS_COMMANDSTATION);
            LOGGER.info("Fetch subscriptions for class COMMANDSTATION: {}", subscriptionDataList);
            publishAsGuestResponseNotifyMessage(originNode, csStateResponse, subscriptionDataList);
        }
        catch (ProtocolException ex) {
            LOGGER.warn("Publish guestResponseNotifyMessage failed.", ex);
        }
    }

    private void notifyAccessorySubscribers(final AccessoryStateMessageEvent me) {

        try {
            final List subscriptionDataList =
                getSubscriptionDataList(MessageClassEnum.BIDIB_MESSAGE_CLASS_ACCESSORY);
            LOGGER.info("Fetch subscriptions for class ACCESSORY: {}", subscriptionDataList);

            if (!subscriptionDataList.isEmpty()) {

                final AccessoryState accessoryState = me.getAccessoryState();
                byte[] nodeAddress = me.getAddress();
                int messageNum = me.getMessageNum();
                final NodeInterface originNode = nodeProvider.findNodeByAddress(me.getAddress());

                int accessoryNumber = me.getAccessoryNumber();
                Integer aspect = accessoryState.getActiveAspect();
                byte[] value =
                    new byte[] { accessoryState.getTotal(), accessoryState.getExecute(),
                        ByteUtils.getLowByte(accessoryState.getWait()) };

                final BidibMessageInterface accessoryStateResponse =
                    requestFactory
                        .createAccessoryStateResponse(nodeAddress, messageNum, accessoryNumber, aspect, value);

                publishAsGuestResponseNotifyMessage(originNode, accessoryStateResponse, subscriptionDataList);
            }
        }
        catch (ProtocolException ex) {
            LOGGER.warn("Publish guestResponseNotifyMessage failed.", ex);
        }
    }

    private void notifyBoosterSubscribers(final BoosterStateMessageEvent me) {

        try {
            final BoosterState boosterState = me.getBoosterState();
            byte[] nodeAddress = me.getAddress();
            int messageNum = me.getMessageNum();
            final NodeInterface originNode = nodeProvider.findNodeByAddress(me.getAddress());

            final BidibMessageInterface boostStateResponse =
                requestFactory.createBoosterStateResponse(nodeAddress, messageNum, boosterState);

            final List subscriptionDataList =
                getSubscriptionDataList(MessageClassEnum.BIDIB_MESSAGE_CLASS_BOOSTER);
            LOGGER.info("Fetch subscriptions for class BOOSTER: {}", subscriptionDataList);
            publishAsGuestResponseNotifyMessage(originNode, boostStateResponse, subscriptionDataList);
        }
        catch (ProtocolException ex) {
            LOGGER.warn("Publish guestResponseNotifyMessage failed.", ex);
        }
    }

    private void notifyBoosterSubscribers(final BoosterDiagMessageEvent me) {

        try {
            byte[] nodeAddress = me.getAddress();
            int messageNum = me.getMessageNum();
            final NodeInterface originNode = nodeProvider.findNodeByAddress(me.getAddress());

            int current = me.getCurrent();
            int voltage = me.getVoltage();
            int temperature = me.getTemperature();

            final BidibMessageInterface boostDiagResponse =
                requestFactory.createBoosterDiagnosticResponse(nodeAddress, messageNum, current, voltage, temperature);

            final List subscriptionDataList =
                getSubscriptionDataList(MessageClassEnum.BIDIB_MESSAGE_CLASS_BOOSTER);
            publishAsGuestResponseNotifyMessage(originNode, boostDiagResponse, subscriptionDataList);
        }
        catch (ProtocolException ex) {
            LOGGER.warn("Publish guestResponseNotifyMessage failed.", ex);
        }
    }

    /**
     * Publish the message as guest response notify message.
     * 
     * @param bidibMessage
     *            the original bidib message
     * @param nodeAddress
     *            the node address of the sender node
     * @param subscriptionDataList
     *            the subscription data list
     * @throws ProtocolException
     *             thrown if create {@code GuestResponseNotifyMessage} failed
     */
    @Override
    public void publishAsGuestResponseNotifyMessage(
        final NodeInterface originNode, final BidibMessageInterface bidibMessage,
        final List subscriptionDataList) throws ProtocolException {

        if (CollectionUtils.isNotEmpty(subscriptionDataList)) {

            for (NodeSubscriptionData nodeSubscriptionData : subscriptionDataList) {

                byte[] nodeAddress = nodeSubscriptionData.getSubscriberAddress();
                TargetModeEnum targetMode = nodeSubscriptionData.getTargetMode();
                LOGGER
                    .info("Prepared nodeAddress: {}, targetMode: {}, bidibMessage: {}",
                        NodeUtils.formatAddress(nodeAddress), targetMode, bidibMessage);

                // send the MSG_GUEST_RESP_NOTIFY to the node
                final GuestResponseNotifyMessage gateResponseNotifyMessage =
                    new GuestResponseNotifyMessage(nodeAddress, 0, TargetModeEnum.BIDIB_TARGET_MODE_UID,
                        NodeUtils.getTargetModeUniqueId(TargetModeEnum.BIDIB_TARGET_MODE_UID, originNode.getUniqueId()),
                        bidibMessage.getNum(), bidibMessage.getMessageContent());

                publishMessage(nodeSubscriptionData, gateResponseNotifyMessage);
            }
        }
        else {
            LOGGER.info("No subscribers for bidibMessage: {}", bidibMessage);
        }
    }

    /**
     * Publish the message to the node with the provided nodeAddress.
     * 
     * @param nodeSubscriptionData
     *            the node subscription data
     * @param bidibMessage
     *            the message
     */
    @Override
    public void publishMessage(
        final NodeSubscriptionData nodeSubscriptionData, final BidibMessageInterface bidibMessage) {
        LOGGER
            .info("Publish message, current subscriber address: {}, subscriber uniqueId: {}",
                NodeUtils.formatAddress(nodeSubscriptionData.getSubscriberAddress()),
                ByteUtils.getUniqueIdAsString(nodeSubscriptionData.getSubscriberUniqueId()));

        // TODO must evaluate if the addresses are subscribed to the current node address and filter out the not
        // matching addresses

        // send the message to the connection
        try {
            final NodeInterface node = nodeProvider.findNodeByAddress(nodeSubscriptionData.getSubscriberAddress());
            final GatewayConnection gatewayConnection = (GatewayConnection) this.connection;

            if (node != null) {
                bidibMessage.setAddr(nodeSubscriptionData.getSubscriberAddress());
                gatewayConnection.publishBidibMessage(node, bidibMessage);
            }
            else {
                LOGGER.warn("The node is not available, node address: {}", nodeSubscriptionData.getSubscriberAddress());
            }
        }
        catch (Exception ex) {
            LOGGER.warn("Publish bidib message to connection failed: {}", bidibMessage, ex);
        }
    }

    public Disposable subscribeSubscriberEvents(Consumer onNext, Consumer onError) {

        return subjectSubscriberEvents
            .subscribeOn(Schedulers.single()).observeOn(Schedulers.computation()).subscribe(onNext, onError);
    }

    public Map> getSubscriptions() {
        return MapUtils.unmodifiableMap(subscriptions);
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy