View Javadoc
1   package org.opentrafficsim.remotecontrol;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Dimension;
5   import java.awt.EventQueue;
6   import java.awt.FlowLayout;
7   import java.awt.Font;
8   import java.awt.event.ActionEvent;
9   import java.awt.event.ActionListener;
10  import java.awt.event.WindowEvent;
11  import java.awt.event.WindowListener;
12  import java.io.IOException;
13  import java.io.PrintStream;
14  import java.net.URL;
15  import java.nio.charset.StandardCharsets;
16  import java.util.Scanner;
17  
18  import javax.swing.JButton;
19  import javax.swing.JComponent;
20  import javax.swing.JFrame;
21  import javax.swing.JPanel;
22  import javax.swing.JScrollPane;
23  import javax.swing.JTextArea;
24  import javax.swing.WindowConstants;
25  import javax.swing.border.EmptyBorder;
26  
27  import org.djunits.unit.DurationUnit;
28  import org.djunits.value.vdouble.scalar.Duration;
29  import org.djunits.value.vdouble.scalar.Time;
30  import org.djutils.cli.Checkable;
31  import org.djutils.cli.CliUtil;
32  import org.djutils.decoderdumper.HexDumper;
33  import org.djutils.io.URLResource;
34  import org.djutils.logger.CategoryLogger;
35  import org.djutils.logger.LogCategory;
36  import org.djutils.serialization.SerializationException;
37  import org.pmw.tinylog.Level;
38  import org.sim0mq.Sim0MQException;
39  import org.sim0mq.message.Sim0MQMessage;
40  import org.zeromq.SocketType;
41  import org.zeromq.ZContext;
42  import org.zeromq.ZMQ;
43  import org.zeromq.ZMQException;
44  
45  import picocli.CommandLine.Command;
46  import picocli.CommandLine.Option;
47  
48  /**
49   * Remotely control OTS using Sim0MQ messages.
50   * <p>
51   * Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
52   * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
53   * <p>
54   * @version $Revision$, $LastChangedDate$, by $Author$, initial version Mar 4, 2020 <br>
55   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
56   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
57   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
58   */
59  public class Sim0MQRemoteController extends JFrame implements WindowListener, ActionListener
60  {
61      /** ... */
62      private static final long serialVersionUID = 20200304L;
63  
64      /**
65       * The command line options.
66       */
67      @Command(description = "Test program for Remote Control OTS", name = "Remote Control OTS", mixinStandardHelpOptions = true,
68              version = "1.0")
69      public static class Options implements Checkable
70      {
71          /** The IP port. */
72          @Option(names = {"-p", "--port"}, description = "Internet port to use", defaultValue = "8888")
73          private int port;
74  
75          /**
76           * Retrieve the port.
77           * @return int; the port
78           */
79          public final int getPort()
80          {
81              return this.port;
82          }
83  
84          /** The host name. */
85          @Option(names = {"-H", "--host"}, description = "Internet host to use", defaultValue = "localhost")
86          private String host;
87  
88          /**
89           * Retrieve the host name.
90           * @return String; the host name
91           */
92          public final String getHost()
93          {
94              return this.host;
95          }
96  
97          @Override
98          public final void check() throws Exception
99          {
100             if (this.port <= 0 || this.port > 65535)
101             {
102                 throw new Exception("Port should be between 1 and 65535");
103             }
104         }
105     }
106 
107     /** The instance of the RemoteControl. */
108     @SuppressWarnings("checkstyle:visibilitymodifier")
109     static Sim0MQRemoteController gui = null;
110 
111     /** Socket for sending messages that should be relayed to OTS. */
112     private ZMQ.Socket toOTS;
113 
114     /**
115      * Start the OTS remote control program.
116      * @param args String[]; the command line arguments
117      */
118     public static void main(final String[] args)
119     {
120         CategoryLogger.setAllLogLevel(Level.WARNING);
121         CategoryLogger.setLogCategories(LogCategory.ALL);
122         // Instantiate the RemoteControl GUI
123         try
124         {
125             EventQueue.invokeAndWait(new Runnable()
126             {
127                 /** {@inheritDoc} */
128                 @Override
129                 public void run()
130                 {
131                     try
132                     {
133                         gui = new Sim0MQRemoteController();
134                         gui.setVisible(true);
135                     }
136                     catch (Exception e)
137                     {
138                         e.printStackTrace();
139                         System.exit(ERROR);
140                     }
141                 }
142             });
143         }
144         catch (Exception e)
145         {
146             e.printStackTrace();
147             System.exit(ERROR);
148         }
149         // We don't get here until the GUI is fully running.
150         Options options = new Options();
151         CliUtil.execute(options, args); // register Unit converters, parse the command line, etc..
152         gui.processArguments(options.getHost(), options.getPort());
153     }
154 
155     /** ... */
156     private ZContext zContext = new ZContext(1);
157 
158     /** Message relayer. */
159     private Thread pollerThread;
160 
161     /**
162      * Poller thread for relaying messages between the remote OTS and local AWT.
163      */
164     class PollerThread extends Thread
165     {
166         /** The ZContext. */
167         private final ZContext context;
168 
169         /** The host that runs the OTS simulation. */
170         private final String slaveHost;
171 
172         /** The port on which to connect to the OTS simulation. */
173         private final int slavePort;
174 
175         /**
176          * Construct a new PollerThread for relaying messages.
177          * @param context ZContext; the ZMQ context
178          * @param slaveHost String; host name of the OTS server machine
179          * @param slavePort int; port number on which to connect to the OTS server machine
180          */
181         PollerThread(final ZContext context, final String slaveHost, final int slavePort)
182         {
183             this.context = context;
184             this.slaveHost = slaveHost;
185             this.slavePort = slavePort;
186         }
187 
188         @Override
189         public final void run()
190         {
191             int nextExpectedPacket = 0;
192             ZMQ.Socket slaveSocket = this.context.createSocket(SocketType.PAIR); // changed to PAIR
193             slaveSocket.setHWM(100000);
194             ZMQ.Socket awtSocketIn = this.context.createSocket(SocketType.PULL);
195             awtSocketIn.setHWM(100000);
196             ZMQ.Socket awtSocketOut = this.context.createSocket(SocketType.PUSH);
197             awtSocketOut.setHWM(100000);
198             slaveSocket.connect("tcp://" + this.slaveHost + ":" + this.slavePort);
199             awtSocketIn.bind("inproc://fromAWT");
200             awtSocketOut.bind("inproc://toAWT");
201             ZMQ.Poller items = this.context.createPoller(2);
202             items.register(slaveSocket, ZMQ.Poller.POLLIN);
203             items.register(awtSocketIn, ZMQ.Poller.POLLIN);
204             while (!Thread.currentThread().isInterrupted())
205             {
206                 byte[] message;
207                 items.poll();
208                 if (items.pollin(0))
209                 {
210                     message = slaveSocket.recv(0);
211                     String expectedSenderField = String.format("slave_%05d", ++nextExpectedPacket);
212                     try
213                     {
214                         Object[] messageFields = Sim0MQMessage.decode(message).createObjectArray();
215                         String senderTag = (String) messageFields[3];
216                         if (!senderTag.equals(expectedSenderField))
217                         {
218                             System.err.println("Got message " + senderTag + " , expected " + expectedSenderField
219                                     + ", message is " + messageFields[5]);
220                         }
221                     }
222                     catch (Sim0MQException | SerializationException e)
223                     {
224                         e.printStackTrace();
225                     }
226 
227                     // System.out.println("poller has received a message on the fromOTS DEALER socket; transmitting to AWT");
228                     awtSocketOut.send(message);
229                 }
230                 if (items.pollin(1))
231                 {
232                     message = awtSocketIn.recv(0);
233                     // System.out.println("poller has received a message on the fromAWT PULL socket; transmitting to OTS");
234                     slaveSocket.send(message);
235                 }
236             }
237 
238         }
239 
240     }
241 
242     /**
243      * Open connections as specified on the command line, then start the message transfer threads.
244      * @param host String; host to connect to (listening OTS server should already be running)
245      * @param port int; port to connect to (listening OTS server should be listening on that port)
246      */
247     public void processArguments(final String host, final int port)
248     {
249         this.output.println("host is " + host + ", port is " + port);
250 
251         this.pollerThread = new PollerThread(this.zContext, host, port);
252 
253         this.pollerThread.start();
254 
255         this.toOTS = this.zContext.createSocket(SocketType.PUSH);
256         this.toOTS.setHWM(100000);
257 
258         new OTS2AWT(this.zContext).start();
259 
260         this.toOTS.connect("inproc://fromAWT");
261     }
262 
263     /**
264      * Write something to the remote OTS.
265      * @param command String; the command to write
266      * @throws IOException when communication fails
267      */
268     public void write(final String command) throws IOException
269     {
270         this.toOTS.send(command);
271         // output.println("Wrote " + command.getBytes().length + " bytes");
272         this.output.println("Sent string \"" + command + "\"");
273     }
274 
275     /**
276      * Write something to the remote OTS.
277      * @param bytes byte[]; the bytes to write
278      * @throws IOException when communication fails
279      */
280     public void write(final byte[] bytes) throws IOException
281     {
282         this.toOTS.send(bytes);
283         // output.println("Wrote " + command.getBytes().length + " bytes");
284         // output.println(HexDumper.hexDumper(bytes));
285     }
286 
287     /** Step to button. */
288     private JButton stepTo;
289 
290     /**
291      * Construct the GUI.
292      */
293     Sim0MQRemoteController()
294     {
295         // Construct the GUI
296         setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
297         setBounds(100, 100, 1000, 800);
298         JPanel panelAll = new JPanel();
299         panelAll.setBorder(new EmptyBorder(5, 5, 5, 5));
300         panelAll.setLayout(new BorderLayout(0, 0));
301         setContentPane(panelAll);
302         JPanel panelControls = new JPanel();
303         panelAll.add(panelControls, BorderLayout.PAGE_START);
304         JTextArea textArea = new JTextArea();
305         textArea.setFont(new Font("monospaced", Font.PLAIN, 12));
306         JScrollPane scrollPane = new JScrollPane(textArea);
307         scrollPane.setPreferredSize(new Dimension(800, 400));
308         panelAll.add(scrollPane, BorderLayout.PAGE_END);
309         this.output = new PrintStream(new TextAreaOutputStream(textArea), true);
310         JPanel controls = new JPanel();
311         controls.setLayout(new FlowLayout());
312         JButton sendNetwork = new JButton("Send network");
313         sendNetwork.setActionCommand("SendNetwork");
314         sendNetwork.addActionListener(this);
315         controls.add(sendNetwork);
316         this.stepTo = new JButton("Step to 10 s");
317         this.stepTo.setActionCommand("StepTo");
318         this.stepTo.addActionListener(this);
319         controls.add(this.stepTo);
320         JButton step100TimesTo = new JButton("Step 100 times 10 s");
321         step100TimesTo.setActionCommand("Step100To");
322         step100TimesTo.addActionListener(this);
323         controls.add(step100TimesTo);
324         JButton getGTUPositions = new JButton("Get all GTU positions");
325         getGTUPositions.setActionCommand("GetAllGTUPositions");
326         getGTUPositions.addActionListener(this);
327         controls.add(getGTUPositions);
328         panelAll.add(controls, BorderLayout.CENTER);
329     }
330 
331     /** Debugging and other output goes here. */
332     @SuppressWarnings("checkstyle:visibilitymodifier")
333     PrintStream output = null;
334 
335     /**
336      * Shut down this application.
337      */
338     public void shutDown()
339     {
340         // Do we have to kill anything for a clean exit?
341     }
342 
343     /** {@inheritDoc} */
344     @Override
345     public void windowOpened(final WindowEvent e)
346     {
347         // Do nothing
348     }
349 
350     /** {@inheritDoc} */
351     @Override
352     public final void windowClosing(final WindowEvent e)
353     {
354         shutDown();
355     }
356 
357     /** {@inheritDoc} */
358     @Override
359     public void windowClosed(final WindowEvent e)
360     {
361         // Do nothing
362     }
363 
364     /** {@inheritDoc} */
365     @Override
366     public void windowIconified(final WindowEvent e)
367     {
368         // Do nothing
369     }
370 
371     /** {@inheritDoc} */
372     @Override
373     public void windowDeiconified(final WindowEvent e)
374     {
375         // Do nothing
376     }
377 
378     /** {@inheritDoc} */
379     @Override
380     public void windowActivated(final WindowEvent e)
381     {
382         // Do nothing
383     }
384 
385     /** {@inheritDoc} */
386     @Override
387     public void windowDeactivated(final WindowEvent e)
388     {
389         // Do nothing
390     }
391 
392     /**
393      * Thread that reads results from OTS and (for now) writes those to the textArea.
394      */
395     class OTS2AWT extends Thread
396     {
397         /** Socket where the message from OTS will appear. */
398         private final ZMQ.Socket fromOTS;
399 
400         /**
401          * Construct a new OTS2AWT thread.
402          * @param zContext ZContext; the ZContext that is needed to construct the PULL socket to read the messages
403          */
404         OTS2AWT(final ZContext zContext)
405         {
406             this.fromOTS = zContext.createSocket(SocketType.PULL);
407             this.fromOTS.setHWM(100000);
408             this.fromOTS.connect("inproc://toAWT");
409         }
410 
411         /** {@inheritDoc} */
412         @Override
413         public void run()
414         {
415             do
416             {
417                 try
418                 {
419                     // Read from remotely controlled OTS
420                     byte[] bytes = this.fromOTS.recv(0); /// XXX: this one is okay to block
421                     // System.out.println("remote controller has received a message on the fromOTS PULL socket");
422                     Object[] message = Sim0MQMessage.decode(bytes).createObjectArray();
423                     if (message.length > 8 && message[5] instanceof String)
424                     {
425                         String command = (String) message[5];
426                         switch (command)
427                         {
428                             case "GTUPOSITION":
429                                 Sim0MQRemoteController.this.output
430                                         .println(String.format("%10.10s: %s x=%8.3f y=%8.3f z=%8.3f heading=%6.1f, v=%s, a=%s",
431                                                 message[8], message[9], message[10], message[11], message[12],
432                                                 Math.toDegrees((Double) message[13]), message[14], message[15]));
433                                 break;
434 
435                             case "TIME_CHANGED_EVENT":
436                                 Sim0MQRemoteController.this.output.println(message[8]);
437                                 break;
438 
439                             case "TRAFFICCONTROL.CONTROLLER_WARNING":
440                                 Sim0MQRemoteController.this.output
441                                         .println(String.format("%s: warning %s", message[8], message[9]));
442                                 break;
443 
444                             case "TRAFFICCONTROL.CONTROLLER_EVALUATING":
445                                 Sim0MQRemoteController.this.output
446                                         .println(String.format("%s: evaluating %s", message[8], message[9]));
447                                 break;
448 
449                             case "TRAFFICCONTROL.CONFLICT_GROUP_CHANGED":
450                                 Sim0MQRemoteController.this.output.println(String.format(
451                                         "%s: conflict group changed from %s to %s", message[8], message[9], message[10]));
452                                 break;
453 
454                             case "NETWORK.GTU.ADD":
455                                 Sim0MQRemoteController.this.output.println(String.format("GTU added %s", message[8]));
456                                 break;
457 
458                             case "NETWORK.GTU.REMOVE":
459                                 Sim0MQRemoteController.this.output.println(String.format("GTU removed %s", message[8]));
460                                 break;
461 
462                             case "READY":
463                                 Sim0MQRemoteController.this.output.println("Slave is ready for the next command");
464                                 break;
465 
466                             default:
467                                 Sim0MQRemoteController.this.output.println("Unhandled reply: " + command);
468                                 Sim0MQRemoteController.this.output.println(HexDumper.hexDumper(bytes));
469                                 Sim0MQRemoteController.this.output.println("Received:");
470                                 Sim0MQRemoteController.this.output.println(Sim0MQMessage.print(message));
471                                 break;
472 
473                         }
474                     }
475                     else
476                     {
477                         Sim0MQRemoteController.this.output.println(HexDumper.hexDumper(bytes));
478                     }
479                 }
480                 catch (ZMQException | Sim0MQException | SerializationException e)
481                 {
482                     e.printStackTrace();
483                     return;
484                 }
485             }
486             while (true);
487 
488         }
489     }
490 
491     /**
492      * Open an URL, read it and store the contents in a string. Adapted from
493      * https://stackoverflow.com/questions/4328711/read-url-to-string-in-few-lines-of-java-code
494      * @param url URL; the URL
495      * @return String
496      * @throws IOException when reading the file fails
497      */
498     public static String readStringFromURL(final URL url) throws IOException
499     {
500         try (Scanner scanner = new Scanner(url.openStream(), StandardCharsets.UTF_8.toString()))
501         {
502             scanner.useDelimiter("\\A");
503             return scanner.hasNext() ? scanner.next() : "";
504         }
505     }
506 
507     /** {@inheritDoc} */
508     @Override
509     public void actionPerformed(final ActionEvent e)
510     {
511         switch (e.getActionCommand())
512         {
513             case "SendNetwork":
514             {
515                 String networkFile = "/TrafCODDemo2/TrafCODDemo2.xml";
516                 Duration warmupDuration = Duration.ZERO;
517                 Duration runDuration = new Duration(3600, DurationUnit.SECOND);
518                 Long seed = 123456L;
519                 URL url = URLResource.getResource(networkFile);
520                 // System.out.println("url is " + url);
521                 try
522                 {
523                     String xml = readStringFromURL(url);
524                     System.out.println("xml length = " + xml.length());
525                     try
526                     {
527                         write(Sim0MQMessage.encodeUTF8(true, 0, "RemoteControl", "OTS", "LOADNETWORK", 0, xml, warmupDuration,
528                                 runDuration, seed));
529                     }
530                     catch (IOException e1)
531                     {
532                         this.output.println("Write failed; Caught IOException");
533                         ((JComponent) e.getSource()).setEnabled(false);
534                     }
535                     catch (Sim0MQException e1)
536                     {
537                         e1.printStackTrace();
538                     }
539                     catch (SerializationException e1)
540                     {
541                         e1.printStackTrace();
542                     }
543                 }
544                 catch (IOException e2)
545                 {
546                     System.err.println("Could not load file " + networkFile);
547                     e2.printStackTrace();
548                 }
549                 break;
550             }
551 
552             case "StepTo":
553             {
554                 JButton button = (JButton) e.getSource();
555                 String caption = button.getText();
556                 int position;
557                 for (position = 0; position < caption.length(); position++)
558                 {
559                     if (Character.isDigit(caption.charAt(position)))
560                     {
561                         break;
562                     }
563                 }
564                 Time toTime = Time.valueOf(caption.substring(position));
565                 try
566                 {
567                     write(Sim0MQMessage.encodeUTF8(true, 0, "RemoteControl", "OTS", "SIMULATEUNTIL", 0, toTime));
568                     toTime = toTime.plus(new Duration(10, DurationUnit.SECOND));
569                     button.setText(caption.substring(0, position)
570                             + String.format("%.0f %s", toTime.getInUnit(), toTime.getDisplayUnit()));
571                 }
572                 catch (IOException | Sim0MQException | SerializationException e1)
573                 {
574                     e1.printStackTrace();
575                 }
576                 break;
577             }
578 
579             case "Step100To":
580             {
581                 for (int i = 0; i < 100; i++)
582                 {
583                     actionPerformed(new ActionEvent(this.stepTo, 0, "StepTo"));
584                 }
585                 break;
586             }
587 
588             case "GetAllGTUPositions":
589             {
590                 try
591                 {
592                     write(Sim0MQMessage.encodeUTF8(true, 0, "RemoteControl", "OTS", "SENDALLGTUPOSITIONS", 0));
593                 }
594                 catch (IOException | Sim0MQException | SerializationException e1)
595                 {
596                     e1.printStackTrace();
597                 }
598                 break;
599             }
600 
601             default:
602                 this.output.println("Oops: unhandled action command");
603         }
604 
605     }
606 }