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