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.junit.Ignore;
22  import org.junit.Test;
23  import org.slf4j.Logger;
24  import org.slf4j.LoggerFactory;
25  
26  import java.io.IOException;
27  import java.lang.invoke.MethodHandles;
28  import java.util.*;
29  import java.util.concurrent.CountDownLatch;
30  import java.util.concurrent.atomic.AtomicBoolean;
31  import java.util.stream.Collectors;
32  import java.util.stream.IntStream;
33  import java.util.stream.Stream;
34  
35  import static javax.management.timer.Timer.ONE_SECOND;
36  import static org.awaitility.Awaitility.await;
37  import static org.chabala.brick.controllab.PortChooser.choosePort;
38  import static org.hamcrest.Matchers.*;
39  import static org.junit.Assert.assertThat;
40  import static org.junit.Assume.assumeNoException;
41  
42  /**
43   * Integration tests for working with multiple control labs.
44   *
45   * <p>These tests require a connection to the hardware. They expect
46   * four control labs to be connected.
47   */
48  @SuppressWarnings({"squid:S2699","squid:S2925"})
49  public class MultipleControlLabIT {
50  
51      private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
52  
53      private static final int AVAILABLE_CONTROL_LABS = 4;
54  
55      /**
56       * Attempts to connect to every serial port in turn, connect to a control lab,
57       * and waits for the stop button to be pressed before moving on.
58       * @throws Exception on any issue with the test
59       */
60      @Ignore("Requires interaction with stop button to complete, only run manually")
61      @Test
62      public void testPortIdentification() throws Exception {
63          for (int portPosition = 1; portPosition <= AVAILABLE_CONTROL_LABS; portPosition++) {
64              AtomicBoolean stop = new AtomicBoolean(false);
65              try (ControlLab controlLab = ControlLab.newControlLab()) {
66                  try {
67                      controlLab.open(choosePort(controlLab, portPosition));
68                  } catch (IOException e) {
69                      assumeNoException(e);
70                  }
71                  controlLab.getStopButton().addListener(stopButtonEvent -> stop.set(true));
72                  await().forever().until(stop::get);
73              }
74          }
75      }
76  
77      /**
78       * Starts all control labs and iterates through turning on their outputs in unison.
79       * @throws Exception on any issue with the test
80       */
81      @Test
82      public void testOperatingInUnison() throws Exception {
83          Set<ControlLab> controlLabs = new HashSet<>();
84          try {
85              for (int portPosition = 1; portPosition <= AVAILABLE_CONTROL_LABS; portPosition++) {
86                  ControlLab controlLab = ControlLab.newControlLab();
87                  controlLabs.add(controlLab);
88                  try {
89                      controlLab.open(choosePort(controlLab, portPosition));
90                  } catch (IOException e) {
91                      assumeNoException(e);
92                  }
93              }
94              OutputId prev = OutputId.H;
95              for (OutputId next : OutputId.ALL) {
96                  switchOutput(controlLabs, prev, next);
97                  prev = next;
98              }
99          } finally {
100             controlLabs.parallelStream().forEach(c -> {
101                 try {
102                     c.close();
103                 } catch (IOException e) {
104                     assumeNoException(e);
105                 }
106             });
107         }
108     }
109 
110     private void switchOutput(Set<ControlLab> controlLabs, OutputId off, OutputId on) throws InterruptedException {
111         controlLabs.parallelStream().forEach(c -> {
112             try {
113                 c.getOutput(off).turnOff();
114                 c.getOutput(on).turnOn();
115             } catch (IOException e) {
116                 assumeNoException(e);
117             }
118         });
119         Thread.sleep(ONE_SECOND / 2);
120     }
121 
122     /**
123      * Example of advanced coordination of multiple control labs, activating the motors in
124      * a chasing pattern. To use, arrange the control labs in a grid with two rows, start
125      * the test, wait for all the control labs to show they are connected, then press each
126      * stop button in a clockwise direction from the top left. Pressing any stop button
127      * after the pattern has started will end the test.
128      * @throws Exception on any issue with the test
129      */
130     @Ignore("Requires interaction with stop button to complete, only run manually")
131     @Test
132     public void testOperatingInTandem() throws Exception {
133         List<ControlLab> controlLabs = new ArrayList<>();
134         AtomicBoolean stop = new AtomicBoolean(false);
135         List<String> portNameOrder = getSerialPortsInOrderOfStopButtonPressed();
136         log.info("Ports in order pressed: {}", String.join(", ", portNameOrder));
137         assertThat(portNameOrder, hasSize(AVAILABLE_CONTROL_LABS));
138         try {
139             for (String portName : portNameOrder) {
140                 ControlLab controlLab = ControlLab.newControlLab();
141                 controlLabs.add(controlLab);
142                 try {
143                     controlLab.open(portName);
144                 } catch (IOException e) {
145                     assumeNoException(e);
146                 }
147                 controlLab.getStopButton().addListener(event -> stop.set(true));
148             }
149             List<Output> outputOrder = getOutputOrder(controlLabs);
150             Output prev = null;
151             Output next = null;
152             Iterator<Output> outputIterator = new CyclicalIterator<>(outputOrder);
153             while (outputIterator.hasNext()) {
154                 next = outputIterator.next();
155                 next.turnOn();
156                 if (prev != null) {
157                     prev.turnOff();
158                 }
159                 prev = next;
160                 Thread.sleep(ONE_SECOND / 8);
161                 if (stop.get()) {
162                     break;
163                 }
164             }
165             if (next != null) {
166                 next.turnOff();
167             }
168         } finally {
169             controlLabs.parallelStream().forEach(c -> {
170                 try {
171                     c.close();
172                 } catch (IOException e) {
173                     assumeNoException(e);
174                 }
175             });
176         }
177     }
178 
179     private List<Output> getOutputOrder(List<ControlLab> controlLabs) {
180         List<Output> outputOrder = new ArrayList<>();
181         List<EnumSet<OutputId>> columnOrder = Arrays.asList(
182             EnumSet.of(OutputId.A, OutputId.E),
183             EnumSet.of(OutputId.B, OutputId.F),
184             EnumSet.of(OutputId.C, OutputId.G),
185             EnumSet.of(OutputId.D, OutputId.H)
186         );
187         int splitIndex = AVAILABLE_CONTROL_LABS / 2;
188         List<ControlLab> leftToRight = controlLabs.subList(0, splitIndex);
189         List<ControlLab> rightToLeft = controlLabs.subList(splitIndex, controlLabs.size());
190         for (ControlLab controlLab : leftToRight) {
191             for (EnumSet<OutputId> column : columnOrder) {
192                 outputOrder.add(controlLab.getOutput(column));
193             }
194         }
195         Collections.reverse(columnOrder);
196         for (ControlLab controlLab : rightToLeft) {
197             for (EnumSet<OutputId> column : columnOrder) {
198                 outputOrder.add(controlLab.getOutput(column));
199             }
200         }
201         return outputOrder;
202     }
203 
204     /**
205      * Cyclical iterator, no mutation supported. Easier than making a circular
206      * linked list for traversal only.
207      * @param <E> element contained by the iterable
208      */
209     private final class CyclicalIterator<E> implements Iterator<E> {
210 
211         private final Iterable<E> iterable;
212         private Iterator<E> iterator;
213 
214         private CyclicalIterator(Iterable<E> iterable) {
215             this.iterable = iterable;
216             iterator = iterable.iterator();
217         }
218 
219         /**
220          * {@inheritDoc}
221          * <p>Always {@code true}, unless the initial iterable was empty.
222          * This method also handles mutation of the iterator reference when
223          * the underlying iterator reaches its end.
224          * @return {@inheritDoc}
225          */
226         @Override
227         public boolean hasNext() {
228             boolean hasNext = iterator.hasNext();
229             if (!hasNext) {
230                 iterator = iterable.iterator();
231                 hasNext = iterator.hasNext();
232             }
233             return hasNext;
234         }
235 
236         /**
237          * {@inheritDoc}
238          * <p>Guaranteed to return an element, as long as initial iterable was
239          * not empty.
240          * @return {@inheritDoc}
241          */
242         @Override
243         public E next() {
244             if (!hasNext()) {
245                 throw new NoSuchElementException();
246             }
247             return iterator.next();
248         }
249     }
250 
251     /**
252      * Starts all control labs and records the order that their stop buttons are pressed.
253      * @return list of system specific port names
254      * @throws InterruptedException if interrupted
255      */
256     private List<String> getSerialPortsInOrderOfStopButtonPressed() throws InterruptedException {
257         List<String> portNameOrder = Collections.synchronizedList(new ArrayList<>());
258         CountDownLatch portIdLatch = new CountDownLatch(AVAILABLE_CONTROL_LABS);
259         List<ControlLab> controlLabs = Stream.generate(ControlLab::newControlLab)
260                 .limit(AVAILABLE_CONTROL_LABS)
261                 .parallel()
262                 .peek(controlLab -> controlLab.getStopButton().addListener(event -> {
263                     portNameOrder.add(controlLab.getConnectedPortName());
264                     try {
265                         controlLab.close();
266                     } catch (IOException e) {
267                         assumeNoException(e);
268                     } finally {
269                         portIdLatch.countDown();
270                     }
271                 })).collect(Collectors.toList());
272         try {
273             IntStream.rangeClosed(1, AVAILABLE_CONTROL_LABS)
274                     .parallel()
275                     .forEach(portPosition -> {
276                         ControlLab controlLab = controlLabs.get(portPosition - 1);
277                         try {
278                             controlLab.open(choosePort(controlLab, portPosition));
279                         } catch (IOException e) {
280                             assumeNoException(e);
281                         }
282                     });
283             portIdLatch.await();
284         } finally {
285             controlLabs.parallelStream().forEach(c -> {
286                 try {
287                     c.close();
288                 } catch (IOException e) {
289                     assumeNoException(e);
290                 }
291             });
292         }
293         return portNameOrder;
294     }
295 }