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

com.thecoderscorner.menu.remote.RemoteMenuController Maven / Gradle / Ivy

There is a newer version: 4.4.0
Show newest version
/*
 * Copyright (c)  2016-2019 https://www.thecoderscorner.com (Nutricherry LTD).
 * This product is licensed under an Apache license, see the LICENSE file in the top-level directory.
 *
 */

package com.thecoderscorner.menu.remote;

import com.thecoderscorner.menu.domain.*;
import com.thecoderscorner.menu.domain.state.CurrentScrollPosition;
import com.thecoderscorner.menu.domain.state.MenuTree;
import com.thecoderscorner.menu.domain.state.PortableColor;
import com.thecoderscorner.menu.domain.util.MenuItemVisitor;
import com.thecoderscorner.menu.remote.commands.*;
import com.thecoderscorner.menu.remote.protocol.CorrelationId;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;

import static com.thecoderscorner.menu.remote.AuthStatus.AWAITING_CONNECTION;
import static com.thecoderscorner.menu.remote.AuthStatus.CONNECTION_READY;
import static com.thecoderscorner.menu.remote.RemoteInformation.NOT_CONNECTED;
import static com.thecoderscorner.menu.remote.commands.CommandFactory.*;
import static java.lang.System.Logger.Level.ERROR;
import static java.lang.System.Logger.Level.INFO;

/**
 * This class manages a single remote connection to an Arduino. It is responsible for check
 * if the connection is still alive, and sending heartbeat messages to keep the connection
 * alive too. This class abstracts the connectivity part away from the business logic.
 * The remote connection is then handled by the RemoteConnector. Normally, one creates a
 * whole remote stack using one the builders, such as Rs232ControllerBuilder.
 */
public class RemoteMenuController {

    private final System.Logger logger = System.getLogger(getClass().getSimpleName());
    private final RemoteConnector connector;
    private final MenuTree managedMenu;
    private final AtomicReference remoteParty = new AtomicReference<>(NOT_CONNECTED);
    private final ConcurrentMap itemsInProgress = new ConcurrentHashMap<>();
    private final List listeners = new CopyOnWriteArrayList<>();

    public RemoteMenuController(RemoteConnector connector, MenuTree managedMenu) {
        this.connector = connector;
        this.managedMenu = managedMenu;
    }

    /**
     * starts the remote connection such that it will attempt to establish connectivity
     */
    public void start() {
        connector.registerConnectorListener(this::onCommandReceived);
        connector.registerConnectionChangeListener(this::onConnectionChange);
        connector.start();
    }

    private void onConnectionChange(RemoteConnector remoteConnector, AuthStatus status) {
        logger.log(INFO, "Connection state changed to connected = " + status);

        if(status == AWAITING_CONNECTION) {
            itemsInProgress.forEach((key, item) ->
                    listeners.forEach(rcl -> rcl.ackReceived(key, item, AckStatus.UNKNOWN_ERROR))
            );
            itemsInProgress.clear();
        }

        listeners.forEach(l-> l.connectionState(remoteParty.get(), status));
    }

    /**
     * attempt to stop the underlying connector
     */
    public void stop() {
        connector.stop();
    }

    /**
     * Use to send commands directly. Should not be used outside of this class, instead
     * prefer the helper methods to send each type of item.
     * @param command a command to send to the remote side.
     */
    protected void sendCommand(MenuCommand command) {
        try {
            connector.sendMenuCommand(command);
        } catch (IOException e) {
            logger.log(ERROR, "Error while writing out command", e);
            connector.close();
        }
    }


    /**
     * Send a dialog update
     * @param buttonType the type of button press to activate on the remote.
     */
    public CorrelationId sendDialogAction(MenuButtonType buttonType) {
        CorrelationId correlationId = new CorrelationId();
        sendCommand(newDialogCommand(DialogMode.ACTION, "", "", buttonType, buttonType, correlationId));
        return correlationId;
    }

    /**
     * Send a delta change for the given menuitem
     * @param item the item to change
     * @param deltaChange the amount to change by
     */
    public CorrelationId sendDeltaUpdate(MenuItem item, int deltaChange) {
        CorrelationId correlationId = new CorrelationId();
        itemsInProgress.put(correlationId, item);
        sendCommand(newDeltaChangeCommand(correlationId, item, deltaChange));
        return correlationId;
    }

    /**
     * Send an asbolute change for the given item
     * @param item the item
     * @param newValue the absolute change
     */
    public CorrelationId sendAbsoluteUpdate(MenuItem item, Object newValue) {
        CorrelationId correlationId = new CorrelationId();
        itemsInProgress.put(correlationId, item);
        sendCommand(newAbsoluteMenuChangeCommand(correlationId, item, newValue));
        return correlationId;
    }

    /**
     * get the underlying connectivity, rarely needed
     * @return underlying connector
     */
    public RemoteConnector getConnector() {
        return connector;
    }

    /**
     * Check if all the menu items from the remote device are available locally yet.
     * @return true if the populated, otherwise false.
     */
    public boolean isTreeFullyPopulated() {
        return connector.getAuthenticationStatus() == CONNECTION_READY;
    }

    /**
     * register for events when the tree becomes fully populated, a menu item changes
     * or there's a change in connectivity.
     * @param listener your listener to register for events
     */
    public void addListener(RemoteControllerListener listener) {
        listeners.add(listener);
    }

    private void onCommandReceived(RemoteConnector remoteConnector, MenuCommand menuCommand) {
        switch(menuCommand.getCommandType()) {
            case ACKNOWLEDGEMENT:
                onAcknowledgementCommand((MenuAcknowledgementCommand)menuCommand);
                break;
            case ANALOG_BOOT_ITEM:
            case ENUM_BOOT_ITEM:
            case BOOLEAN_BOOT_ITEM:
            case SUBMENU_BOOT_ITEM:
            case TEXT_BOOT_ITEM:
            case LARGE_NUM_BOOT_ITEM:
            case REMOTE_BOOT_ITEM:
            case FLOAT_BOOT_ITEM:
            case ACTION_BOOT_ITEM:
            case RUNTIME_LIST_BOOT:
            case BOOT_RGB_COLOR:
            case BOOT_SCROLL_CHOICE:
                onMenuItemBoot((BootItemMenuCommand) menuCommand);
                break;
            case BOOTSTRAP:
                onBootstrap((MenuBootstrapCommand) menuCommand);
                break;
            case CHANGE_INT_FIELD:
                onChangeField((MenuChangeCommand) menuCommand);
                break;
            case DIALOG_UPDATE:
                onDialogChange((MenuDialogCommand) menuCommand);
        }
    }

    private void onBootstrap(MenuBootstrapCommand menuCommand) {
        if(menuCommand.getBootType() == MenuBootstrapCommand.BootType.END) {
            listeners.forEach(RemoteControllerListener::treeFullyPopulated);
        }
    }

    private void onDialogChange(MenuDialogCommand menuCommand) {
        listeners.forEach(l -> l.dialogUpdate(
                menuCommand.getDialogMode(),
                menuCommand.getHeader(), menuCommand.getBuffer(),
                menuCommand.getButton1(), menuCommand.getButton2()));
    }

    private void onAcknowledgementCommand(MenuAcknowledgementCommand menuCommand) {
        var item = itemsInProgress.get(menuCommand.getCorrelationId());
        listeners.forEach(rcl->
                rcl.ackReceived(menuCommand.getCorrelationId(), item, menuCommand.getAckStatus())
        );
    }

    @SuppressWarnings("unchecked")
    private void onMenuItemBoot(BootItemMenuCommand menuCommand) {
        managedMenu.addOrUpdateItem(menuCommand.getSubMenuId(), menuCommand.getMenuItem());
        managedMenu.changeItem(menuCommand.getMenuItem(), menuCommand.newMenuState(
                managedMenu.getMenuState(menuCommand.getMenuItem())));
        listeners.forEach(l-> l.menuItemChanged(menuCommand.getMenuItem(), false));
    }

    private void onChangeField(MenuChangeCommand menuCommand) {
        // we cannot process until the tree is populated
        if(!isTreeFullyPopulated()) return;

        managedMenu.getMenuById(menuCommand.getMenuItemId()).ifPresent((item) -> {
            item.accept(new MenuItemVisitor() {
                @Override
                public void visit(AnalogMenuItem item) {
                    managedMenu.changeItem(item, item.newMenuState(Integer.valueOf(menuCommand.getValue()), true, false));
                    listeners.forEach(l-> l.menuItemChanged(item, true));
                }

                @Override
                public void visit(BooleanMenuItem item) {
                    managedMenu.changeItem(item, item.newMenuState(Integer.valueOf(menuCommand.getValue()) != 0, true, false));
                    listeners.forEach(l-> l.menuItemChanged(item, true));
                }

                @Override
                public void visit(EnumMenuItem item) {
                    managedMenu.changeItem(item, item.newMenuState(Integer.valueOf(menuCommand.getValue()), true, false));
                    listeners.forEach(l-> l.menuItemChanged(item, true));
                }

                @Override
                public void visit(SubMenuItem item) { /*ignored*/ }

                @Override
                public void visit(EditableTextMenuItem item) {
                    managedMenu.changeItem(item, item.newMenuState(menuCommand.getValue(), true, false));
                    listeners.forEach(l-> l.menuItemChanged(item, true));
                }

                @Override
                public void visit(EditableLargeNumberMenuItem item) {
                    String val = menuCommand.getValue();
                    if(val != null && val.contains("[")) {
                        val = val.replaceAll("[\\[\\]]", "");
                    }
                    else if(val == null) val = "0.0";

                    BigDecimal dec = new BigDecimal(val);
                    managedMenu.changeItem(item, item.newMenuState(dec, true, false));
                    listeners.forEach(l-> l.menuItemChanged(item, true));
                }

                @Override
                public void visit(FloatMenuItem item) {
                    managedMenu.changeItem(item, item.newMenuState(Float.valueOf(menuCommand.getValue()), true, false));
                    listeners.forEach(l-> l.menuItemChanged(item, true));
                }

                @Override
                public void visit(Rgb32MenuItem item) {
                    managedMenu.changeItem(item, item.newMenuState(new PortableColor(menuCommand.getValue()), true, false));
                    listeners.forEach(l-> l.menuItemChanged(item, true));
                }

                @Override
                public void visit(ScrollChoiceMenuItem item) {
                    managedMenu.changeItem(item, item.newMenuState(new CurrentScrollPosition(menuCommand.getValue()), true, false));
                    listeners.forEach(l-> l.menuItemChanged(item, true));
                }

                @Override
                public void visit(ActionMenuItem item) {
                    /* ignored, there is no state for this type */
                }

                @Override
                public void visit(RuntimeListMenuItem listItem) {
                    // todo
                }

            });

        });
    }

    public MenuTree getManagedMenu() {
        return managedMenu;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy