InputManager.java

/*
 * Copyright © 2016 Greg Chabala
 *
 * This file is part of brick-control-lab.
 *
 * brick-control-lab is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * brick-control-lab is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with brick-control-lab.  If not, see http://www.gnu.org/licenses/.
 */
package org.chabala.brick.controllab;

import org.chabala.brick.controllab.sensor.SensorEvent;
import org.chabala.brick.controllab.sensor.SensorListener;
import org.chabala.brick.controllab.sensor.SensorValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.*;

/**
 * Manages registration of input event listeners, and parsing input
 * data into events for those listeners.
 */
class InputManager {

    private final Logger log = LoggerFactory.getLogger(getClass());
    private final Map<InputId, byte[]> sensorData;
    private final Map<InputId, Set<SensorListener>> sensorListeners;
    private final List<InputId> frameInputOrder =
            Arrays.asList(InputId.I4, InputId.I8, InputId.I3, InputId.I7,
                          InputId.I2, InputId.I6, InputId.I1, InputId.I5);
    private ByteConsumer processStopButton = null;

    InputManager() {
        sensorData = Collections.synchronizedMap(new EnumMap<>(InputId.class));
        sensorListeners = Collections.synchronizedMap(new EnumMap<>(InputId.class));
        Arrays.stream(InputId.values()).forEach(i -> {
            sensorData.put(i, new byte[] {0, 0});
            sensorListeners.put(i, Collections.synchronizedSet(new HashSet<>(2)));
        });
    }

    /**
     * Attach a listener for {@link SensorEvent}s.
     *
     * <p>Multiple listeners are allowed. A listener instance will only be registered
     * once even if it is added multiple times.
     * @param input    input to add the listener to
     * @param listener listener to add
     */
    void addSensorListener(InputId input, SensorListener listener) {
        sensorListeners.get(input).add(listener);
    }

    /**
     * Remove a listener for {@link SensorEvent}s.
     * @param input    input to remove the listener from
     * @param listener listener to remove
     */
    void removeSensorListener(InputId input, SensorListener listener) {
        sensorListeners.get(input).remove(listener);
    }

    /**
     * Expects 19 bytes of data.
     * @param inputFrame byte array of 19 bytes
     */
    void processInputSensors(byte[] inputFrame) throws IOException {
        int frameIndex = 0;
        if (inputFrame.length != Protocol.FRAME_SIZE) {
            StringBuilder sb = new StringBuilder();
            for (byte b : inputFrame) {
                sb.append(String.format("0x%02X ", b));
            }
            throw new IOException("Expected 19 bytes, got " + inputFrame.length + " - " + sb.toString());
        }
        if (!isChecksumValid(inputFrame)) {
            log.warn("Bad checksum received");
            return;
        }
        processStopButton(inputFrame[frameIndex++]);
        int lastCommandIndex = frameIndex++;
        if (0x00 != inputFrame[lastCommandIndex]) {
            //TODO: make event listener for output feedback
            log.debug("Ports affected by last command {}",
                    OutputId.decodeByteToSet(inputFrame[lastCommandIndex]));
        }
        for (InputId in : frameInputOrder) {
            setSensorValue(in, inputFrame[frameIndex++], inputFrame[frameIndex++]);
        }
    }

    private boolean isChecksumValid(byte[] inputFrame) {
        int checksum = 0;
        for (byte b : inputFrame) {
            checksum += Byte.toUnsignedInt(b);
        }
        return (checksum & 0xFF) == 0xFF;
    }

    private void processStopButton(byte b) {
        if (processStopButton != null) {
            processStopButton.accept(b);
        }
    }

    private void setSensorValue(InputId input, byte high, byte low) {
        byte[] newValue = {high, low};
        byte[] oldValue = sensorData.put(input, newValue);
        if (!Arrays.equals(newValue, oldValue)) {
            SensorEvent<SensorValue> event =
                    new SensorEvent<>(input, oldValue, newValue, SensorValue.newSensorValue(high, low));
            synchronized (sensorListeners) {
                sensorListeners.get(input).forEach(l -> l.sensorEventReceived(event));
            }
        }
    }

    void setStopButtonCallback(ByteConsumer processStopButton) {
        this.processStopButton = processStopButton;
    }
}