View Javadoc
1   /*
2    * Copyright © 2016 Greg Chabala
3    *
4    * This file is part of brick-control-lab.
5    *
6    * brick-control-lab is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU Lesser General Public License as
8    * published by the Free Software Foundation, either version 3 of the
9    * License, or (at your option) any later version.
10   *
11   * brick-control-lab is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU Lesser General Public License for more details.
15   *
16   * You should have received a copy of the GNU Lesser General Public License
17   * along with brick-control-lab.  If not, see http://www.gnu.org/licenses/.
18   */
19  package org.chabala.brick.controllab;
20  
21  import org.chabala.brick.controllab.sensor.SensorEvent;
22  import org.chabala.brick.controllab.sensor.SensorListener;
23  import org.chabala.brick.controllab.sensor.SensorValue;
24  import org.slf4j.Logger;
25  import org.slf4j.LoggerFactory;
26  
27  import java.io.IOException;
28  import java.util.*;
29  
30  /**
31   * Manages registration of input event listeners, and parsing input
32   * data into events for those listeners.
33   */
34  class InputManager {
35  
36      private final Logger log = LoggerFactory.getLogger(getClass());
37      private final Map<InputId, byte[]> sensorData;
38      private final Map<InputId, Set<SensorListener>> sensorListeners;
39      private final List<InputId> frameInputOrder =
40              Arrays.asList(InputId.I4, InputId.I8, InputId.I3, InputId.I7,
41                            InputId.I2, InputId.I6, InputId.I1, InputId.I5);
42      private ByteConsumer processStopButton = null;
43  
44      InputManager() {
45          sensorData = Collections.synchronizedMap(new EnumMap<>(InputId.class));
46          sensorListeners = Collections.synchronizedMap(new EnumMap<>(InputId.class));
47          Arrays.stream(InputId.values()).forEach(i -> {
48              sensorData.put(i, new byte[] {0, 0});
49              sensorListeners.put(i, Collections.synchronizedSet(new HashSet<>(2)));
50          });
51      }
52  
53      /**
54       * Attach a listener for {@link SensorEvent}s.
55       *
56       * <p>Multiple listeners are allowed. A listener instance will only be registered
57       * once even if it is added multiple times.
58       * @param input    input to add the listener to
59       * @param listener listener to add
60       */
61      void addSensorListener(InputId input, SensorListener listener) {
62          sensorListeners.get(input).add(listener);
63      }
64  
65      /**
66       * Remove a listener for {@link SensorEvent}s.
67       * @param input    input to remove the listener from
68       * @param listener listener to remove
69       */
70      void removeSensorListener(InputId input, SensorListener listener) {
71          sensorListeners.get(input).remove(listener);
72      }
73  
74      /**
75       * Expects 19 bytes of data.
76       * @param inputFrame byte array of 19 bytes
77       */
78      void processInputSensors(byte[] inputFrame) throws IOException {
79          int frameIndex = 0;
80          if (inputFrame.length != Protocol.FRAME_SIZE) {
81              StringBuilder sb = new StringBuilder();
82              for (byte b : inputFrame) {
83                  sb.append(String.format("0x%02X ", b));
84              }
85              throw new IOException("Expected 19 bytes, got " + inputFrame.length + " - " + sb.toString());
86          }
87          if (!isChecksumValid(inputFrame)) {
88              log.warn("Bad checksum received");
89              return;
90          }
91          processStopButton(inputFrame[frameIndex++]);
92          int lastCommandIndex = frameIndex++;
93          if (0x00 != inputFrame[lastCommandIndex]) {
94              //TODO: make event listener for output feedback
95              log.debug("Ports affected by last command {}",
96                      OutputId.decodeByteToSet(inputFrame[lastCommandIndex]));
97          }
98          for (InputId in : frameInputOrder) {
99              setSensorValue(in, inputFrame[frameIndex++], inputFrame[frameIndex++]);
100         }
101     }
102 
103     private boolean isChecksumValid(byte[] inputFrame) {
104         int checksum = 0;
105         for (byte b : inputFrame) {
106             checksum += Byte.toUnsignedInt(b);
107         }
108         return (checksum & 0xFF) == 0xFF;
109     }
110 
111     private void processStopButton(byte b) {
112         if (processStopButton != null) {
113             processStopButton.accept(b);
114         }
115     }
116 
117     private void setSensorValue(InputId input, byte high, byte low) {
118         byte[] newValue = {high, low};
119         byte[] oldValue = sensorData.put(input, newValue);
120         if (!Arrays.equals(newValue, oldValue)) {
121             SensorEvent<SensorValue> event =
122                     new SensorEvent<>(input, oldValue, newValue, SensorValue.newSensorValue(high, low));
123             synchronized (sensorListeners) {
124                 sensorListeners.get(input).forEach(l -> l.sensorEventReceived(event));
125             }
126         }
127     }
128 
129     void setStopButtonCallback(ByteConsumer processStopButton) {
130         this.processStopButton = processStopButton;
131     }
132 }