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

org.bidib.wizard.discovery.service.SimpleNetBidibDeviceSearchService Maven / Gradle / Ivy

package org.bidib.wizard.discovery.service;

import java.io.ByteArrayOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.apache.commons.collections4.CollectionUtils;
import org.bidib.jbidibc.core.MessageParser;
import org.bidib.jbidibc.core.PlainMessageParser;
import org.bidib.jbidibc.messages.BidibLibrary;
import org.bidib.jbidibc.messages.MessageProcessor;
import org.bidib.jbidibc.messages.exception.ProtocolException;
import org.bidib.jbidibc.messages.message.BidibMessageInterface;
import org.bidib.jbidibc.messages.message.BidibRequestFactory;
import org.bidib.jbidibc.messages.message.LocalAnnounceMessage;
import org.bidib.jbidibc.messages.message.LocalDiscoverMessage;
import org.bidib.jbidibc.messages.message.RequestFactory;
import org.bidib.jbidibc.messages.message.netbidib.LocalLinkMessage;
import org.bidib.jbidibc.messages.message.netbidib.LocalProtocolSignatureMessage;
import org.bidib.jbidibc.messages.utils.ByteUtils;
import org.bidib.jbidibc.messages.utils.MessageUtils;
import org.bidib.jbidibc.messages.utils.ThreadFactoryBuilder;
import org.bidib.wizard.common.event.WizardApplicationReadyEvent;
import org.bidib.wizard.common.model.settings.NetBidibSettingsInterface;
import org.bidib.wizard.common.utils.NetworkAddressHelper;
import org.bidib.wizard.core.model.settings.NetBidibSettings;
import org.bidib.wizard.core.utils.AopUtils;
import org.bidib.wizard.discovery.listener.SimpleNetBidibServiceListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;

public class SimpleNetBidibDeviceSearchService implements NetBidibDeviceSearchService {

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

    private static final Logger MSG_RX_LOGGER = LoggerFactory.getLogger("RX");

    public static final String EMITTER_PREFIX_BIDIB = "BiDiB";

    protected static final int NETBIDIB_PORT = 62875;

    private DatagramSocket socket;

    private final SimpleNetBidibServiceListener netBidibServiceListener;

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

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

    private final org.bidib.jbidibc.messages.logger.Logger splitMessageLogger;

    private final RequestFactory requestFactory;

    private final NetBidibSettingsInterface netBidibSettings;

    private final Semaphore semaphore = new Semaphore(1);

    private ScheduledFuture registerFuture;

    public SimpleNetBidibDeviceSearchService(final SimpleNetBidibServiceListener netBidibServiceListener,
        final NetBidibSettingsInterface netBidibSettings) {

        this.netBidibServiceListener = netBidibServiceListener;
        this.netBidibSettings = netBidibSettings;

        // create local request factory
        this.requestFactory = new BidibRequestFactory();
        this.requestFactory.initialize();

        splitMessageLogger = new org.bidib.jbidibc.messages.logger.Logger() {

            @Override
            public void debug(String format, Object... arguments) {
                LOGGER.debug(format, arguments);
            }

            @Override
            public void info(String format, Object... arguments) {
                LOGGER.info(format, arguments);
            }

            @Override
            public void warn(String format, Object... arguments) {
                LOGGER.warn(format, arguments);
            }

            @Override
            public void error(String format, Object... arguments) {
                LOGGER.error(format, arguments);
            }
        };
    }

    @EventListener
    public void handleContextStart(final WizardApplicationReadyEvent ase) {
        LOGGER.info("The application is ready.");

        addSettingsListener();

        if (this.netBidibSettings.isDiscoveryUdpEnabled()) {
            triggerStartSearch();
        }
        else {
            LOGGER.info("Start search the netBiDiB devices with UDP is disabled in settings.");
        }
    }

    private void addSettingsListener() {

        try {
            final NetBidibSettings nbs = AopUtils.getTargetObject(netBidibSettings);

            nbs.addPropertyChangeListener(NetBidibSettings.PROPERTY_DISCOVERY_UDP_ENABLED, evt -> {

                boolean isDiscoveryUdpEnabled = netBidibSettings.isDiscoveryUdpEnabled();
                LOGGER
                    .info("The discovery UDP enabled flag has changed, isDiscoveryUdpEnabled: {}",
                        isDiscoveryUdpEnabled);

                if (isDiscoveryUdpEnabled) {
                    triggerStartSearch();
                }
                else {
                    // do noting here
                }

            });
        }
        catch (Exception ex) {
            LOGGER.warn("Register for changes of netBidib settings failed.", ex);
        }

    }

    private synchronized void triggerStartSearch() {

        if (this.registerFuture != null) {
            LOGGER.info("Register is in progress already.");
            return;
        }

        LOGGER.info("Schedule the search for UDP netBiDiB devices after 5s.");

        this.registerFuture = serviceWorker.schedule(() -> {

            startSearch();

            this.registerFuture = null;
        }, 5, TimeUnit.SECONDS);
    }

    protected void startSearch() {
        LOGGER
            .info("Start the search for a simple netBiDiB device. Current simpleNetBidibServiceListener: {}",
                netBidibServiceListener);

        MulticastReceiver receiver = null;
        Future future = null;

        try {

            if (!semaphore.tryAcquire()) {
                LOGGER.warn("The discovery search for UDP netBiDiB devices is already running.");
                return;
            }

            final InetAddress sourceAddress = NetworkAddressHelper.getLocalAddressOfType(Inet4Address.class);
            LOGGER.info("Current source address: {}", sourceAddress);

            final List broadcastAddresses = getBroadcastAddresses();

            if (CollectionUtils.isNotEmpty(broadcastAddresses)) {

                try {

                    final byte[] broadcastMessage = getBroadcastMessge();

                    // must initialize the DatagramSocket with null, otherwise the later bind will fail
                    this.socket = new DatagramSocket(null);
                    this.socket.setBroadcast(true);
                    this.socket.setReuseAddress(true);

                    this.socket
                        .bind(new InetSocketAddress(sourceAddress, SimpleNetBidibDeviceSearchService.NETBIDIB_PORT));

                    receiver = new MulticastReceiver(this.socket, broadcastMessage, packet -> {

                        byte[] receivedData = ByteUtils.subArray(packet.getData(), 0, packet.getLength());

                        final Supplier contextKeyProvider = () -> {
                            return packet.getAddress().getHostAddress() + ":" + packet.getPort();
                        };

                        LOGGER.info("Received data: {}", ByteUtils.bytesToHex(receivedData));

                        // parse the discovery packet

                        final MessageParser messageParser = new PlainMessageParser();
                        final MessageProcessor messageProcessor = new MessageProcessor() {

                            private boolean checkCRC = false;

                            private AtomicBoolean isFirstPacket = new AtomicBoolean();

                            @Override
                            public void processMessages(ByteArrayOutputStream messageData) throws ProtocolException {

                                if (messageData.size() < 1) {
                                    LOGGER.info("No data in provided buffer, skip process messages.");
                                    return;
                                }

                                final String newContextKey = contextKeyProvider.get();

                                // if a CRC error is detected in splitMessages the reading loop will terminate ...
                                MessageUtils
                                    .splitBidibMessages(splitMessageLogger, messageData, checkCRC, isFirstPacket,
                                        message -> processMessage(message, newContextKey));
                            }

                            protected void processMessage(byte[] messageArray, final String contextKey)
                                throws ProtocolException {

                                BidibMessageInterface message = null;

                                try {
                                    // create the bidib message from the provided message data
                                    message = requestFactory.createConcreteMessage(messageArray);
                                    if (MSG_RX_LOGGER.isInfoEnabled()) {

                                        StringBuilder sb = new StringBuilder("<< ");
                                        sb.append(message);
                                        sb.append(" : ");
                                        sb.append(ByteUtils.bytesToHex(messageArray));

                                        MSG_RX_LOGGER.info(sb.toString());
                                    }

                                    // process the message
                                    processMessage(message, contextKey);
                                }
                                catch (ProtocolException ex) {
                                    LOGGER
                                        .warn("Process received messages failed: {}",
                                            ByteUtils.bytesToHex(messageArray), ex);

                                    StringBuilder sb = new StringBuilder("<< received invalid: ");
                                    sb.append(message);
                                    sb.append(" : ");
                                    sb.append(ByteUtils.bytesToHex(messageArray));

                                    MSG_RX_LOGGER.warn(sb.toString());

                                    throw ex;
                                }
                                catch (Exception ex) {
                                    LOGGER
                                        .warn("Process received messages failed: {}",
                                            ByteUtils.bytesToHex(messageArray), ex);
                                }
                            }

                            protected void processMessage(final BidibMessageInterface message, final String contextKey)
                                throws ProtocolException {

                                LOGGER.info("Process the message: {}, contextKey: {}", message, contextKey);

                                switch (ByteUtils.getInt(message.getType())) {
                                    case BidibLibrary.MSG_LOCAL_PROTOCOL_SIGNATURE:
                                        // respond the protocol signature
                                        LocalProtocolSignatureMessage localProtocolSignatureMessage =
                                            (LocalProtocolSignatureMessage) message;
                                        String requestor = localProtocolSignatureMessage.getRequestorName();
                                        LOGGER
                                            .info("Received MSG_LOCAL_PROTOCOL_SIGNATURE from requestor: {}",
                                                requestor);
                                        break;
                                    case BidibLibrary.MSG_LOCAL_LINK:
                                        LocalLinkMessage localLinkMessage = (LocalLinkMessage) message;
                                        LOGGER.info("Received LocalLinkMessage.");
                                        if (localLinkMessage
                                            .getLinkDescriptor() == BidibLibrary.BIDIB_LINK_DESCRIPTOR_UID) {

                                            long senderUniqueId = localLinkMessage.getSenderUniqueId();
                                            LOGGER
                                                .info(
                                                    "Received MSG_LOCAL_LINK with BIDIB_LINK_DESCRIPTOR_UID, senderUniqueId: {}",
                                                    ByteUtils.formatHexUniqueId(senderUniqueId));

                                            netBidibServiceListener.signalUniqueId(senderUniqueId, contextKey);
                                        }
                                        break;
                                    case BidibLibrary.MSG_LOCAL_ANNOUNCE:
                                        LocalAnnounceMessage localAnnounceMessage = (LocalAnnounceMessage) message;
                                        LOGGER
                                            .info("Received MSG_LOCAL_ANNOUNCE, opCode: {}, tcpPort: {}",
                                                localAnnounceMessage.getOpCode(), localAnnounceMessage.getTcpPort());

                                        netBidibServiceListener.signalLocalAnnounce(localAnnounceMessage, contextKey);
                                        break;
                                    case BidibLibrary.MSG_LOCAL_DISCOVER:
                                        LOGGER.info("Received MSG_LOCAL_DISCOVER.");
                                        break;
                                    default:
                                        LOGGER.warn("Received unhandled : {}", message);
                                        break;
                                }

                            }
                        };

                        try {
                            messageParser.parseInput(messageProcessor, receivedData, receivedData.length);
                        }
                        catch (Exception ex) {
                            LOGGER.warn("Parse received data and publish to message listeners failed.", ex);
                        }

                    });
                    future = searchWorker.submit(receiver);

                    Thread.sleep(20);

                    for (InetAddress broadcastAddress : broadcastAddresses) {
                        performSearch(broadcastAddress, sourceAddress, broadcastMessage);
                    }

                    future.get(5, TimeUnit.SECONDS);
                    receiver.stopReceiver();

                    future.cancel(true);
                }
                catch (TimeoutException ex) {
                    LOGGER.warn("Search the simple netBiDiB device timed out.");
                }
                catch (Exception ex) {
                    LOGGER.warn("Search the simple netBiDiB device failed.", ex);
                }
                finally {
                    if (receiver != null) {
                        LOGGER.info("The receiver is assigned. Stop the receiver.");
                        receiver.stopReceiver();
                    }

                    if (future != null) {
                        try {
                            LOGGER.info("Cancel the future.");
                            future.cancel(true);
                        }
                        catch (Exception ex) {
                            LOGGER.warn("Cancel future failed.", ex);
                        }
                    }

                    try {
                        LOGGER.info("Close the socket.");
                        this.socket.close();
                    }
                    catch (Exception ex) {
                        LOGGER.warn("Close socket failed.", ex);
                    }

                }

            }
            else {
                LOGGER.info("No broadcast addresses available.");
            }
        }
        catch (Exception ex) {
            LOGGER.warn("Search simple bidib devices failed.", ex);
        }
        finally {
            LOGGER.info("Release the semaphore.");
            this.semaphore.release();
        }
    }

    protected static byte[] getBroadcastMessge() {
        final byte[] broadcastMessage =
            ByteUtils
                .concat(new byte[] { 0x08, 0x00, 0x00, (byte) 0xFE },
                    ByteUtils
                        .concat(EMITTER_PREFIX_BIDIB.getBytes(StandardCharsets.UTF_8),
                            new LocalDiscoverMessage().getContent()));

        return broadcastMessage;
    }

    private void performSearch(
        final InetAddress broadcastAddress, final InetAddress sourceAddress, final byte[] broadcastMessage) {

        LOGGER
            .info("Perform search with broadcast address: {}, sourceAddress: {}, broadcastMessage: {}",
                broadcastAddress, sourceAddress, ByteUtils.bytesToHex(broadcastMessage));

        try {
            LOGGER.info("Send the broadcastMessage from port: {}", this.socket.getLocalPort());

            final DatagramPacket packet =
                new DatagramPacket(broadcastMessage, broadcastMessage.length, broadcastAddress, NETBIDIB_PORT);
            socket.send(packet);
        }
        catch (Exception ex) {
            LOGGER.warn("Search the simple netBiDiB device failed.", ex);
        }
    }

    @Override
    public void triggerSearch() {
        // TODO Auto-generated method stub

    }

    private static class MulticastReceiver extends Thread {
        private final DatagramSocket socket;

        private final byte[] buf = new byte[2048];

        private final AtomicBoolean runReceiver = new AtomicBoolean();

        private final byte[] broadcastMessage;

        private final Consumer packetConsumer;

        public MulticastReceiver(final DatagramSocket socket, final byte[] broadcastMessage,
            final Consumer packetConsumer) {
            this.socket = socket;
            this.broadcastMessage = broadcastMessage;
            this.packetConsumer = packetConsumer;
        }

        public void stopReceiver() {
            LOGGER.info("Stop the receiver.");

            this.runReceiver.set(false);
        }

        @Override
        public void run() {

            try {
                runReceiver.set(true);

                LOGGER.info("The receiver will wait for data.");

                while (runReceiver.get()) {
                    DatagramPacket packet = new DatagramPacket(buf, buf.length);

                    try {
                        socket.receive(packet);

                        LOGGER
                            .info("Received paket: {}, address: {}, port: {}",
                                ByteUtils.bytesToHex(packet.getData(), packet.getLength()), packet.getAddress(),
                                packet.getPort());

                        if (packetConsumer != null) {
                            packetConsumer.accept(packet);
                        }

                    }
                    catch (SocketException ex) {
                        if (runReceiver.get()) {
                            LOGGER.warn("Receive data from broadcast socket failed with socket exception.", ex);
                        }
                        else {
                            LOGGER.info("Receive data from broadcast socket failed with socket exception during stop.");
                        }
                    }
                    catch (Exception ex) {
                        LOGGER.warn("Receive data from broadcast socket failed.", ex);

                        // TODO: handle exception
                    }

                    String received = new String(packet.getData(), 0, packet.getLength());
                    if ("end".equals(received)) {
                        break;
                    }
                }
            }
            catch (Exception e) {
                LOGGER.warn("Open receiver socket failed.", e);
            }

            LOGGER.info("The receiver has terminated.");
        }
    }

    private List getBroadcastAddresses() throws SocketException, UnknownHostException {

        final List broadcastAddresses = new ArrayList<>();
        broadcastAddresses.add(InetAddress.getByName("255.255.255.255"));
        broadcastAddresses.add(InetAddress.getByName("224.0.0.1"));

        Enumeration interfaces = NetworkInterface.getNetworkInterfaces();
        while (interfaces.hasMoreElements()) {
            NetworkInterface networkInterface = interfaces.nextElement();

            if (!NetworkAddressHelper.isUsableNetworkInterface(networkInterface, null)) {
                continue; // Do not want to use the unusable interface.
            }

            for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
                InetAddress broadcast = interfaceAddress.getBroadcast();
                if (broadcast == null) {
                    continue;
                }

                // Do something with the address.
                LOGGER.info("Found broadcast address: {}", broadcast);
                broadcastAddresses.add(broadcast);
            }
        }

        return broadcastAddresses;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy