Monday, December 16, 2013

Introducing Console User-Exits

There are many things to do with the NMEA Data available in the cache, and the console is just doing a little - obvious - part of them.
In order for the users to implement their own features and ideas, we now provide a "user-exit" mechanism.
The user-exits are to be written in Java, and implement a specific interface named olivsoftdesktop.DesktopUserExitInterface, and defined as foillow:
      
     1  package olivsoftdesktop;
     2  
     3  public interface DesktopUserExitInterface
     4  {
     5    public void start();
     6    public void stop();
     7    public void describe();
     8  }
      
    
It could probably not be any simpler.

A Simple User-exit implementation

To develop your own features, you would need to put - at least - into your classpath:
  • desktop.jar
And probably, to access the NMEA Data Cache:
  • nmeaparser.jar
  • nmeareader.jar
  • coreutilities
  • geomutil.jar
Then, write your code, and archive it into a jar-file.
Here is a simple implementation of this interface. This one evaluates the True Wind Speed every time a sentence is received from the NMEA station, and displays a message if the TWS is above 10 knots.
See how the NMEAReaderListener is registered.
      
     1  package olivsoftdesktop.sampleue;
     2  
     3  import nmea.event.NMEAReaderListener;
     4  import nmea.server.ctx.NMEAContext;
     5  import nmea.server.ctx.NMEADataCache;
     6  import ocss.nmea.parser.Angle360;
     7  import ocss.nmea.parser.Speed;
     8  import olivsoftdesktop.DesktopUserExitInterface;
     9  
    10  public class UserExitSample
    11    implements DesktopUserExitInterface
    12  {
    13    public UserExitSample()
    14    {
    15      super();
    16    }
    17  
    18    @Override
    19    public void start()
    20    {
    21      System.out.println("User exit is starting...");
    22      NMEAContext.getInstance().addNMEAReaderListener(new NMEAReaderListener()
    23      {
    24          @Override
    25          public void manageNMEAString(String nmeaString)
    26          {
    27  //        System.out.println("     ... From user exit, got NMEA Data [" + nmeaString + "]");
    28            NMEADataCache dc = NMEAContext.getInstance().getCache();
    29            double tws = ((Speed) dc.get(NMEADataCache.TWS)).getValue();
    30            double twd = ((Angle360) dc.get(NMEADataCache.TWD)).getValue();
    31            if (tws > 10 && !Double.isInfinite(tws))
    32            {
    33              System.out.println("Wind is over 10 kts:" + tws + ", TWD:" + twd);
    34              // TODO Send an email...
    35            }
    36          }
    37      });
    38    }
    39  
    40    @Override
    41    public void stop()
    42    {
    43      System.out.println("Terminating User exit");
    44    }
    45  
    46    @Override
    47    public void describe()
    48    {
    49      System.out.println("This is a simple user-exit example that shows howto register an NMEAReaderListener from your code.");
    50    }
    51  }
      
    

User-exit runtime

To have your user-exit to be taken care if, you need to:
  • Archive it in a jar-file, and put the jar (along with the ones it may depend on) in one of the following directories
    • all-user-exits (recommended)
    • all-libs
    • all-3rd-party
  • Mention the name of the user-exit in the command-line parameters, like -ue:myspecial.feature.SuperUserExit. For the example above, the parameter would be -ue:olivsoftdesktop.sampleue.UserExitSample.
And that's it. This works from the console, as well as from the headless one.

A more complex sample

Here is the scenario:
You have an internet connection on the boat, it is docked or anchored in the harbor.
From wherever you are, you want to know what the wind is like where the boat is.
This user-exit monitors the True Wind Speed (TWS), and send an email when it is above a given threshold. It looks at the wind speed every X minutes, the X comes from a configuration file (email.properties) that can be edited.
      
     1  package olivsoftdesktopuserexits;
     2  
     3  import java.io.FileInputStream;
     4  import java.text.DecimalFormat;
     5  import java.text.SimpleDateFormat;
     6  import java.util.Calendar;
     7  import java.util.Date;
     8  import java.util.Properties;
     9  import java.util.TimeZone;
    10  import nmea.server.ctx.NMEAContext;
    11  import nmea.server.ctx.NMEADataCache;
    12  import ocss.nmea.parser.Angle360;
    13  import ocss.nmea.parser.GeoPos;
    14  import ocss.nmea.parser.Speed;
    15  import ocss.nmea.parser.UTCDate;
    16  import olivsoftdesktop.DesktopUserExitInterface;
    17  import olivsoftdesktopuserexits.emailutil.EmailSender;
    18  
    19  public class DesktopEmailSender
    20    implements DesktopUserExitInterface
    21  {
    22    private final static SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");
    23    private final static DecimalFormat DF22   = new DecimalFormat("##0.00 'kts'");
    24    private final static DecimalFormat DF30   = new DecimalFormat("##0'\272'");
    25    private static String SEND_PROVIDER = "google";
    26    private Thread watcher = null;
    27    private boolean keepWatching = true;
    28    private EmailSender sender = null;
    29  
    30    private double windThreshold = -1;
    31    private long betweenLoops = 600 * 1000L; // 10 minutes default
    32  
    33    public DesktopEmailSender()
    34    {
    35      super();
    36    }
    37  
    38    @Override
    39    public void start()
    40    {
    41      System.out.println("Method 'start':" + this.getClass().getName() + " User exit is starting...");
    42      Properties props = new Properties();
    43      String propFile = "email.properties";
    44      try
    45      {
    46        FileInputStream fis = new FileInputStream(propFile);
    47        props.load(fis);
    48      }
    49      catch (Exception e)
    50      {
    51        System.err.println("email.properies file problem..., from " + System.getProperty("user.dir"));
    52        throw new RuntimeException("File not found:email.properies");
    53      }
    54      SEND_PROVIDER = props.getProperty("ue.preferred.provider", SEND_PROVIDER);
    55      sender = new EmailSender(SEND_PROVIDER);
    56      try
    57      {
    58        windThreshold = Double.parseDouble(props.getProperty("ue.wind.threshold"));
    59        System.out.println("Will send emails when the wind is above [" + windThreshold + "]");
    60      }
    61      catch (NumberFormatException nfe)
    62      {
    63        throw new RuntimeException("Bad wind threshold:" + props.getProperty("ue.wind.threshold"));
    64      }
    65      try
    66      {
    67        betweenLoops = Long.parseLong(props.getProperty("ue.between.loops.in.minute"));
    68      }
    69      catch (NumberFormatException nfe)
    70      {
    71        throw new RuntimeException("Bad Loop interval:" + props.getProperty("ue.between.loops.in.minute"));
    72      }
    73      final long _betweenLoops = betweenLoops;
    74      watcher = new Thread()
    75        {
    76          private boolean started = false;
    77          private final long BETWEEN_LOOPS = _betweenLoops * 60 * 1000;
    78          private final long TEN_SECONDS   =  10000L;
    79          private long waitTime = BETWEEN_LOOPS;
    80          public void run()
    81          {
    82            while (keepWatching)
    83            {
    84              waitTime = BETWEEN_LOOPS;
    85              NMEADataCache dc = NMEAContext.getInstance().getCache();
    86              try
    87              {
    88                double tws = ((Speed) dc.get(NMEADataCache.TWS)).getValue();
    89                double twd = ((Angle360) dc.get(NMEADataCache.TWD)).getValue();
    90                String date = "";
    91                UTCDate utcDate = (UTCDate)NMEAContext.getInstance().getCache().get(NMEADataCache.GPS_DATE_TIME);
    92                if (utcDate != null && utcDate.getValue() != null)
    93                {
    94                  Date d = utcDate.getValue();
    95                  Calendar cal = Calendar.getInstance();
    96                  cal.setTime(d);
    97                  cal.setTimeZone(TimeZone.getTimeZone("Etc/UTC"));
    98                  date = SDF.format(cal.getTime());
    99                }
   100                String pos = "";
   101                try { pos = ((GeoPos)dc.get(NMEADataCache.POSITION)).toString(); } catch (Exception ex) {}
   102                if (!started)
   103                {
   104                  started = true;
   105                  System.out.println(" -- User exit started for good.");
   106                }
   107                if (tws > windThreshold && !Double.isInfinite(tws))
   108                {
   109                  String alertMessage =
   110                    (date.trim().length() > 0 ? "Date:" + date + "\n": "") +
   111                    (pos.trim().length() > 0 ? "Pos:" + pos + "\n" : "") +
   112                    "Wind is over " + DF22.format(windThreshold) + ":" + DF22.format(tws) + ", TWD:" + DF30.format(twd);
   113                  System.out.println(alertMessage);
   114                  // Send an email...
   115                  try
   116                  {
   117                    sender.send(alertMessage);
   118                    System.out.println("Email sent.");
   119                  }
   120                  catch (Exception ex)
   121                  {
   122                    System.err.println("Sending email failed through [" + SEND_PROVIDER + "]");
   123                    ex.printStackTrace();
   124                  }
   125                }
   126              }
   127              catch (NullPointerException npe)
   128              {
   129                // Just wait til next time...
   130                System.out.println("Cache not initialized (yet)");
   131                waitTime = TEN_SECONDS;
   132              }
   133              synchronized (this)
   134              {
   135                System.out.println("  ...User exit going to wait, at " + new Date().toString() + " (will wait for " + (waitTime / 1000) + " s)");
   136                try { wait(waitTime); }
   137                catch (InterruptedException ie)
   138                {
   139                  System.out.println("Told to stop!");
   140                  keepWatching = false;
   141                }
   142              }
   143            }
   144            System.out.println("Stop waiting.");
   145          }
   146        };
   147      keepWatching = true;
   148      watcher.start();
   149    }
   150  
   151    @Override
   152    public void stop()
   153    {
   154      System.out.println(this.getClass().getName() + " is terminating");
   155      keepWatching = false;
   156      synchronized (watcher)
   157      {
   158        watcher.notify();
   159      }
   160    }
   161  
   162    @Override
   163    public void describe()
   164    {
   165      System.out.println("Polls the NMEA Cache on a regular base, and sends an email if the TWS is above a given threshold.");
   166      System.out.println("Driven by a properties file named email.properties, in the all-scripts directory.");
   167    }
   168  }      
      
All the sources of this example.

Possibilities are endless. The limit is your imagination.
Combining the two examples above, you can as well gather all the data into a single document (XML, json, etc), and send it through email on a regular base, so it can be rendered by the receipient.
Etc, etc...

How to do it for yourself, step by step

  1. Download all the sources, in the zip mentionned above
  2. Extract it is a new clean directory
  3. Make sure the jar mail.jar is in your all-3rd-party directory
  4. Make sure you have installed a JDK in your environment
  5. In a system console, navigate to the directory where you unzipped the sources
  6. If it does not exist, create a classes directory. Make sure it is empty
  7. Compile the code:
    On Windows
     Prompt> set OLIV_HOME=D:\OlivSoft
     Prompt> set CP=%OLIV_HOME%\all-3rd-party\mail.jar
     Prompt> set CP=%CP%;%OLIV_HOME%\all-libs\nmeaparser.jar
     Prompt> set CP=%CP%;%OLIV_HOME%\all-libs\nmeareader.jar
     Prompt> set CP=%CP%;%OLIV_HOME%\all-libs\desktop.jar
     Prompt> set CP=%CP%;%OLIV_HOME%\all-libs\geomutil.jar
     Prompt> javac -d classes -sourcepath src -cp %CP% src\olivsoftdesktopuserexits\*.java
            
    On Linux
     
     Prompt> bash
     Prompt> OLIV_HOME=/usr/OlivSoft
     Prompt> CP=$OLIV_HOME/all-3rd-party/mail.jar
     Prompt> CP=$CP:$OLIV_HOME/all-libs/nmeaparser.jar
     Prompt> CP=$CP:$OLIV_HOME/all-libs/nmeareader.jar
     Prompt> CP=$CP:$OLIV_HOME/all-libs/desktop.jar
     Prompt> CP=$CP:$OLIV_HOME/all-libs/geomutil.jar
     Prompt> javac -d classes -sourcepath src -cp $CP src/olivsoftdesktopuserexits/*.java
            
    Make sure you do not see any error.
  8. Archive the generated classes:
    On Windows
     
     Prompt> cd classes
     Prompt> jar -cvf ..\emailUserExit.jar *
            
    On Linux
     
     Prompt> cd classes
     Prompt> jar -cvf ../emailUserExit.jar *
            
  9. Copy the archive in the all-user-exits directory
    On Windows
     
     Prompt> cd ..
     Prompt> copy *.jar %OLIV_HOME%\all-user-exits
            
    On Linux
     
     Prompt> cd ..
     Prompt> cp *.jar $OLIV_HOME/all-user-exits
            
    Copy email.properties in the all-scripts directory
    On Windows
     
     Prompt> copy email.properties %OLIV_HOME%\all-scripts
            
    On Linux
     
     Prompt> cp email.properties $OLIV_HOME/all-scripts
            
    You are almost done...
  10. Modify the line that starts the console, so it takes your work in account:
    On Windows
     
    set COMMAND=java %JAVA_OPTIONS% -classpath %CP% olivsoftdesktop.OlivSoftDesktop %HEADLESS_OPTIONS% -ue:olivsoftdesktopuserexits.DesktopEmailSender
    start "Headless Console (User-Exit)" %COMMAND%
            
    On Linux
     
    java $JAVA_OPTIONS -classpath $CP olivsoftdesktop.OlivSoftDesktop $HEADLESS_OPTIONS -ue:olivsoftdesktopuserexits.DesktopEmailSender &
            
    That's it!
    Important: Do not forget to edit and modify email.properties, so it matches your environment, and your needs.
And all this runs just fine on a Raspberry PI, I've tested it.

No comments: