3.12. lib/common.g - Common library

/*--------------------------------------------------------------------
 * File:        commond.g
 *
 * Description: Functions that run in QNX or Linux, Photon or GTK.
 *
 * Classes:     InterpolatorSettings
 *              DeadbandSettings
 *
 * Functions by category:
 *
 * General:     lib_require
 *              start_qnserves
 *              find_on_path
 *              program_startup
 *              start_stop
 *              start_process
 *              child_died
 *              kill_child
 *              stop_processes
 *              send_message
 *              started_died_hook
 * Controller:  all_white
 *              read_msg
 *              extra_space
 *              TaskInfo.reassign_prog_name
 *              show_names
 *              toggle_raw
 *              make_datadir
 * Log:         send_command
 *              log_toggle
 * History:     InterpolatorSettings.set_defaults
 *              InterpolatorSettings.set_interpolator
 *              assign_history
 *              send_hs_command
 *              display_hs_info
 *              DeadbandSettings.set_parms
 *              find_midnite
 *              min_max
 *------------------------------------------------------------------*/

/********************************************************
 *                  OPERATING SYSTEM                    *
 ********************************************************/

/*--------------------------------------------------------------------
 * Function:    lib_require
 * Returns:     t or nil
 * Description: Finds the name of the current OS, and requires (loads)
 *              the library files specific to that OS.
 *------------------------------------------------------------------*/
function lib_require ()
{
  if (_os_ == "Linux")
    require("lib/linux.g");
  else if (_os_ == "QNX4")
    {
      require("lib/qnx.g");
      require("lib/qnx4.g");
    }
  else if (_os_ == "QNX6")
    {
      require("lib/qnx.g");
      require("lib/qnx6.g");
    }
}

lib_require();

/*--------------------------------------------------------------------
 * Function:    start_qnserves
 * Returns:     t on success, or nil
 * Description: Used to start qserve or nserve, as specified.
 *------------------------------------------------------------------*/
function start_qnserves(command, name)
{
  for(i=0; i<=50; i++)
    {
      if ((anyos_find_process(command, name) == nil))
        {
          system(command);
          usleep(10000);
        }
      else
        {
          i = 50;
          nil;
        }
    }
}     

/*--------------------------------------------------------------------
 * Function:    find_on_path
 * Returns:     t on success, or nil
 * Description: Determines if a command exists on the operating system.
 *              Used by start_process when loading the Log or History
 *              programs to check whether the gnuplot and/or the Cascade
 *              TextLogger program is installed on the system.  Also
 *              used by program_startup to check that the Cascade
 *              DataHub and Cascade Historian are installed.
 *------------------------------------------------------------------*/
function find_on_path(name)
{
   local bin_path = string_split (getenv("PATH"), ":", 0);
   local found;

   for (; !found && bin_path; bin_path = cdr(bin_path))
   {
     if (access (string(car(bin_path),"/",name), 0) == 0)
       found = car(bin_path);
   }
   found;
 }

/*--------------------------------------------------------------------
 * Function:    program_startup
 * Returns:     t or nil
 * Description: Starts any necessary Cogent software and the main window
 *              of the Controller, Monitor, Log, and History programs,
 *              as specified by the proc_name and queue_name arguments.
 *------------------------------------------------------------------*/
function program_startup (proc_name, queue_name, win_fn, msg)
{
  local window, tsk;

  /* See if qserve and nserve are running. If not, start them. */
  start_qnserves("qserve", "qserve");
  start_qnserves("nserve", "sc/nserve");
  
  /* Start interprocess communication. */
  if (init_ipc(proc_name, queue_name, "toolsdemo") == nil)
      error("Could not initialize IPC.");
  
  /* See if the Cascade DataHub is installed and running in the toolsdemo domain.
     If not installed, send a message; if not running, start it up. */
  if (proc_name == "control")
    {
      if (find_on_path("datahub"))
        {
          if ((tsk = locate_task ("/dh/toolsdemo", t)) == nil)
            system("datahub -d toolsdemo -p 4600");
          else
            close_task (tsk);
        }
      else
        {
          after (1, `anygui_makemsg(string("This demo requires the Cascade DataHub,\n",
                                           "which apparently is not installed on your",
                                           " system.\n\nYou can download the Cascade",
                                           " DataHub from the Cogent Web Site\n",
                                           "at http://www.cogent.ca/Software/DataHub",
                                           "_Software.html")));
        }
      /* Add hooks for when tasks die, for the Process Status display. */
      add_hook (#taskstarted_hook, #started_died_hook);
      add_hook (#taskdied_hook, #started_died_hook);
      
      /* Prepare a new data files directory, and remove it when the Controller exits.*/
      system("rm -fr /tmp/cogentdemo/");
      make_datadir();     
      atexit(#system("rm -fr /tmp/cogentdemo/"));
    }
      
  /* Create and register with the datahub some of the points for the Monitor. */
  if (proc_name == "monitor")
    {
      usleep(10000);
      SP_001 = register_point (#SP_001);
      PV_001 = register_point (#PV_001);
      MV_001 = register_point (#MV_001);
      AUTO_001 = register_point (#AUTO_001);
      FREQ_001 = register_point (#FREQ_001);
      PID1_Kp = register_point (#PID1_Kp);
      PID1_Ki = register_point (#PID1_Ki);
      PID1_Kd = register_point (#PID1_Kd);
      PROP_001 = register_point (#PROP_001);
      INT_001 = register_point (#INT_001);
    }

  /* For use by the History program only.  Check to see if the Cascade Historian
     is installed, kill any running instances of it that are named "demohistdb",
     and then start it up.  Also create a histories/ directory to hold the data,
     if one doesn't already exist.*/
  if (proc_name == "history")
    {
      if (find_on_path("histdb"))
        {
          if ((tsk = locate_task("demohistdb", nil)) != nil)
            {
              send_string(tsk, "(exit)");
              close_task (tsk);
            }
          start_process(nil, "histdb", "demohistdb",        
                        list("-n", "demohistdb", "-d", "toolsdemo",
                             "-f", "hist.cfg", "-D", "-q", "demohistq"));
        }
      else
        {
          after (1, `anygui_makemsg(string
                                    ("To work correctly, this part of the demo requires",
                                     " the Cascade Historian,\nwhich apparently is not",
                                     " installed on your system.\n\nYou can download",
                                     " the Cascade Historian from the Cogent Web Site\n",
                                     "at http://www.cogent.ca/Software/Historian",
                                     "_Software.html")));
        }
      if(!is_file("/tmp/cogentdemo/histories/"))
        mkdir("/tmp/cogentdemo/histories", 0o777);
    }
  
  
  /* If necessary, kill any active TextLoggers named "tlog". */
  if ((proc_name == "log") && ((tsk = locate_task("tlog", nil)) != nil))
    {
      send_string(tsk, "(exit)");
      close_task (tsk);
    }


  /* Make sure all processes stop when this program exits. */
  atexit(`stop_processes());
  
  /* Set up handling for death of sub-processes. */
  signal(SIGCHLD, `anygui_sigchild());

  /* Display program info in the Controller text widget. */
  send_message(msg);

  /* Create the main window, and put in a destructor function for GTK */
  window = eval(win_fn);
  anygui_destroyer(window);
}

/********************************************************
 *                   CHILD PROCESSES                    *
 ********************************************************/

/* Keep track of all child processes that this program has started. */
Children := nil;

/*--------------------------------------------------------------------
 * Function:    start_stop
 * Returns:     t or nil
 * Description: Used by control buttons to call the start_process()
 *              and stop_processes() functions.
 *------------------------------------------------------------------*/
function start_stop(button, prog, process_name, args...)
{
  local child;
  
  if (button.switched_on())  
    start_process (button, prog, process_name, args);
  else
    stop_processes(prog, process_name);
}

/*--------------------------------------------------------------------
 * Function:    start_process
 * Returns:     t or nil
 * Description: Starts a process using a fork and exec pair.  It places
 *              the new process ID, name, and associated GUI button in
 *              a list so that we can use SIGCHLD to clean up the child
 *              list and the screen at once.
 *------------------------------------------------------------------*/
function start_process (button, prog, process_name, args)
{
  local pid;

  if ((prog == "gnuplot") && (find_on_path("gnuplot") == nil))
    {
      anygui_makemsg(string("\nIt looks like you don't have\n",
                            "gnuplot on your system.",
                            "\n\nYou must install gnuplot to make plots.\n"));
    }
  else if ((prog == "textlog") && (find_on_path("textlog") == nil))
    {
      anygui_makemsg(string("To work correctly, this part of the demo requires",
                            " the Cascade TextLogger,\nwhich apparently is not",
                            " installed on your system.\n\n",
                            "You can download the Cascade TextLogger from the Cogent Web Site\n",
                            "at http://www.cogent.ca/Software/TextLogger_Software.html"));
    }
  else
    {
      block_signal(SIGCHLD);
      if ((pid = fork()) == 0)        
        {
          /* This is the child process. */
          exec (prog, car(args), cadr(args), caddr(args),
                car(cdddr(args)), car(nth_cdr(args, 4)), 
                car(nth_cdr(args, 5)), car(nth_cdr(args, 6)),
                car(nth_cdr(args, 7)), car(nth_cdr(args, 8)),
                car(nth_cdr(args, 9)));      
          /* In case the above exec commands fails, the following will be
           * called, and it's virtually guaranteed not to fail.  Thus in
           * any circumstance a child process gets created.*/
          exec("/bin/true");
        }
      Children = cons (list (pid, process_name, button, prog), Children);
      unblock_signal(SIGCHLD);
  
      pid;
    }
}

/*--------------------------------------------------------------------
 * Function:    child_died
 * Returns:     t or nil
 * Description: Attached to SIGCHLD, this function wait()s for the child
 *              to die, extracts its process ID, and looks it up in the
 *              list of known children.  It then updates the list and if
 *              there is a GUI button attached, evaluates the cmd argument
 *              to pop out the button. Since this is attached to SIGCHLD,
 *              wait() should never block, as the data must be available
 *              immediately in order for SIGCHLD to have occurred. However,
 *              the Linux OS 'system' call creates spurious SIGCHLD calls,
 *              and as a general safety measure, we use the WNOHANG option,
 *              and call wait() on any signal (task ID parameter is 0).
 *------------------------------------------------------------------*/
function child_died (cmd)
{
  local child_data;
  local child, button;
  
  child_data = wait (0, WNOHANG);
  
  if(list_p(child_data))
    {
      child = car (assoc_equal (car(child_data), Children));
      Children = nremove (child, Children);
      if(cadr(child))
        princ (cadr(child), " died\n");
      if(button = caddr(child))
        if (!destroyed_p(button))
          eval (cmd);
    }
}

/*--------------------------------------------------------------------
 * Function:    kill_child
 * Returns:     t or nil
 * Description: Kills a child process.  We use exit_program() for Gamma
 *              programs, because that allows us to use atexit() within
 *              those programs to kill any of their children. The Cascade
 *              Historian must be flushed before it can exit.  The SIGCHLD
 *              handler does the cleanup for us.
 *------------------------------------------------------------------*/
function kill_child(child, prog, process_name)
{
  local retn, tsk;
  
  if (((prog == "gamma") || (prog == "phgamma")) && 
      ((tsk = locate_task(process_name, nil)) != nil))
    {
      send(tsk, #exit_program(5));
      close_task (tsk);
    }
  else
    if ((process_name == "demohistdb") &&
        ((tsk = locate_task("demohistdb", nil)) != nil))
      {
        send_string(tsk, "(flush)");
        send_string(tsk, "(exit)");
        close_task (tsk);
        
        /*Give time to let child_died() send a message.*/
        usleep(10000);        
      }
    else
      kill(car(child), SIGINT);
}

/*--------------------------------------------------------------------
 * Function:    stop_processes
 * Returns:     t or nil
 * Description: Gives us the option of stopping a single child process or
 *              multiple child processes.  Calls kill_child() to actually
 *              stop a process.
 *------------------------------------------------------------------*/
function stop_processes(prog? = nil, process_name = nil)
{
  try
    {
      if(process_name != nil)
        {
        /* Kill a specified child, looking it up in the list of known
         * children.  We look it up by name, even though the association
         * list is by process ID.  This means that we have to traverse
         * the list of children ourselves to find the proper name.
         */
          with x in Children do
            {
              if (cadr (x) == process_name)
                {
                  child = x;
                  if (child)
                    kill_child(child, prog, process_name);
                }
            }
        }
      else
        /* Kill all the children. */
        with child in Children do
          {
            prog = car(cdddr(child));
            process_name = cadr(child);
            if(prog && process_name)
              {
                kill_child(child, prog, process_name);
                usleep(10000);
              }
          }
    }
  catch
    {        
      princ("Error:\n", _error_stack_, "\n");
    }
}

/*--------------------------------------------------------------------
 * Function:    send_message
 * Returns:     t or nil
 * Description: Sends information messages for the Controller's text box.
 *              The text of the messages is stored in the file
 *              'messages.txt'.
 *------------------------------------------------------------------*/
function send_message(msg)
{
  local tsk;
  if ((tsk = locate_task("control", t)) != nil)
    {
      if(msg == "nsnames")
          send_async(tsk, `show_names(text2));
      else
          send_async(tsk, `anygui_show_text(text, read_msg(@msg), 1));
      close_task(tsk);
    }
}

/*--------------------------------------------------------------------
 * Function:    started_died_hook
 * Returns:     t or nil
 * Description: Called whenever a task starts or dies, this function 
 *              calls send_message() to change the process status display.
 *              Gets added to the Controller from within the
 *              program_startup() function.
 *------------------------------------------------------------------*/
function started_died_hook(!a?...=nil)
{
  send_message("nsnames");
}

/********************************************************
 *                      CONTROLLER                      *
 ********************************************************/

/*--------------------------------------------------------------------
 * Function:    all_white
 * Returns:     t or nil
 * Description: Determines whether a line contains only a blank space
 *              or tab characters.
 *------------------------------------------------------------------*/
function all_white(line)
{
  local i, len=strlen(line), white=t;
  for (i=0; i<len && white; i++)
    if (line[i] != ' ' && line[i] != '\t')
      white=nil;
  white;
}
/*--------------------------------------------------------------------
 * Function:    read_msg
 * Returns:     A string
 * Description: Finds a message in the messages.txt file, reads each 
 *              line, and returns the whole message as a single string.
 *              Uses the all_white function to create new lines.
 *------------------------------------------------------------------*/
function read_msg(msgno)
{
  local fp, marker, msg = "", line = nil, white;
  
  fp = open("messages.txt", "r", nil);
  while(line != _eof_)
  {
    line = read_line(fp);
    marker = string("#", msgno);
    if ((strstr(line, marker)) == 0)
      {
        line = read_line(fp);
        msg = "";
        while(line != "#END")
          {
            white = all_white(line);
            msg = string(msg, white ? "" : " ", line, white ? "\n" : "");
            line = read_line(fp);
          }
        line = _eof_;
      }
  }
  close(fp);
  msg;
}

/*--------------------------------------------------------------------
 * Function:    extra_space
 * Returns:     A string
 * Description: Creates a string of empty spaces for a specified length.
 *------------------------------------------------------------------*/
function extra_space(num)
{
  local str = "";
  for(i=0; i<num; i++)
    str = string(str, " ");
  str;
}

/*--------------------------------------------------------------------
 * Function:    TaskInfo.reassign_prog_name
 * Returns:     A string
 * Description: Reassigns a few specific .name ivars to the TaskInfo
 *              class, the return value of the task_info() Gamma function.
 *------------------------------------------------------------------*/
method TaskInfo.reassign_prog_name()
{
  if (strstr(.name, "dhview") != -1)
      .name = substr(.name, 0, 6);

  switch (.name)
    {
    case "/dh/toolsdemo": .name = "Cascade DataHub";
    case "control": .name = "Controller";
    case "emul": .name = "PID Emulator";
    case "monitor": .name = "Monitor";
    case "dhview": .name = "DataHub Viewer";
    case "log": .name = "Log";
    case "tlog": .name = "Cascade TextLogger";
    case "history": .name = "History";
    case "demohistdb": .name = "Cascade Historian";
    }
}

/* Initialize a global variable for the "Raw output" button. */
RAW = 0;

/*--------------------------------------------------------------------
 * Function:    show_names
 * Returns:     t or nil
 * Description: Displays nsnames output in the Controller.  Uses the
 *              Gamma function nserve_query() to get an array of
 *              TaskInfo instances that contain the needed information.
 *------------------------------------------------------------------*/
function show_names(widget)
{
  local namestring;

  if (RAW == 0)
    namestring = "Program name        Domain      PID\n";
  else
    namestring = "Process name        Domain      PID\n";

  with q in nserve_query() do
    {
      if (RAW == 0)
        q.reassign_prog_name();
      q.name = string(q.name, extra_space(20 - strlen(q.name)));
      if (q.domain == "toolsdemo")
        namestring = string(namestring, q.name, q.domain, "   ",
                            string(q.pid), "\n");
    }
  if (widget != nil)
    anygui_show_text(widget, namestring, 2);
}

/*--------------------------------------------------------------------
 * Function:    toggle_raw
 * Returns:     t or nil
 * Description: Toggles the nsnames output display.
 *------------------------------------------------------------------*/
function toggle_raw(button, wgt)
{
  if (button.switched_on())
      RAW = 1;
  else
      RAW = 0;
  show_names(wgt);
}


/*--------------------------------------------------------------------
 * Function:    make_datadir
 * Returns:     t or nil
 * Description: Creates a temporary directory to hold data from the Log
 *              and History programs.  Called when the Controller is
 *              created in program_startup().
 *------------------------------------------------------------------*/
function make_datadir()
{
  if (!is_dir("/tmp"))
    mkdir("/tmp", 0o777);
  system("rm -fr /tmp/cogentdemo/");
  mkdir("/tmp/cogentdemo", 0o777);
}
     
/********************************************************
 *                         LOG                          *
 ********************************************************/

/*--------------------------------------------------------------------
 * Function:    send_command
 * Returns:     t or nil
 * Description: Sends commands to the TextLogger.
 *------------------------------------------------------------------*/
function send_command(widget, purpose, arg)
{
  local tsk, ret;
  if ((tsk = locate_task("tlog", nil)) != nil)
    {
      switch (purpose)
        {
        case "send-text":
          send(tsk, `output(tldemoboth, @(widget.get_text())));
          send(tsk, `output(tldemostdout, @(widget.get_text())));
        case "send-cmd":
          send_string(tsk, string(widget.get_text()));
        case "collect":
                  if (widget.switched_on())
            {
              send(tsk, `collect(@arg, tldemoboth));
              send(tsk, `collect(@arg, tldemostdout));
            }
        case "file-stdout":
          if (widget.switched_on())
            send(tsk, `enable(@arg));
          else
            send(tsk, `disable(@arg));
        }
      close_task(tsk);
    }
}


/*--------------------------------------------------------------------
 * Function:    log_toggle
 * Returns:     t or nil
 * Description: Handles some extra work that must be done when the Cascade
 *              TextLogger starts up or stops.  It sets up timers for
 *              prepare_times() and get_recent_data().  It also enables
 *              or disables the output files, according to the respective
 *              check boxes.  When the Cascade TextLogger stops, this
 *              function stops the prepare_times() and get_recent_data()
 *              timers, and it puts the log data from output file tloutput.dat
 *              into the text display widget.
 *------------------------------------------------------------------*/
function log_toggle(button, txt_wgt, filebut, stdoutbut, anybut, fillbut, allbut)
{
  local tsk, font, infile, line, line_ret;

  if (button.switched_on())
    {
      if (find_on_path("gnuplot"))
        {
          PT_TIMER = every (.1, #prepare_times());
          RCNT_TIMER = every (30.1, #get_recent_data());
        }

      /* Give time for the TextLogger to start on a slow processor. */
      usleep(500000);
      
      if ((tsk = locate_task("tlog", nil)) != nil)
        {
          anygui_show_text(txt_wgt,
                           string("To stop logging and see the results,\n", 
                                  "press the Log to: button again.\n\n",
                                  read_msg("5.11")), 1);

          if (filebut.switched_on())
            send(tsk, #enable(tldemoboth));
          else
            send(tsk, #disable(tldemoboth));
          
          if (stdoutbut.switched_on())
            send(tsk, #enable(tldemostdout));
          else
            send(tsk, #disable(tldemostdout));
          
          if (anybut.switched_on())
            {
              send(tsk, #collect(any, tldemoboth));
              send(tsk, #collect(any, tldemostdout));
            }
          else if (fillbut.switched_on())
            {
              send(tsk, #collect(fill, tldemoboth));
              send(tsk, #collect(fill, tldemostdout));
            }
          else if (allbut.switched_on())
            {
              send(tsk, #collect(all, tldemoboth));
              send(tsk, #collect(all, tldemostdout));
            }
          close_task(tsk);
        }
    }
  else
    {
      if (find_on_path("gnuplot"))
        {
          cancel(PT_TIMER);
          cancel(RCNT_TIMER);
        }

      infile = open("/tmp/cogentdemo/tloutput.dat", "r");
      if (infile)
        {
          line = read_line(infile);
          line_ret = "";
          while(line != _eof_)
            {
              line_ret = string(line_ret, line, "\n");
              line = read_line(infile);
            }
          anygui_show_text(txt_wgt, line_ret, 3);
          close(infile);
        }
    }
}

/********************************************************
 *                       HISTORY                        *
 ********************************************************/

/*--------------------------------------------------------------------
 * Class:       InterpolatorSettings
 * Description: The instance variables of this class correspond to all
 *              the possible arguments to the Cascade Historian's
 *              "interpolate" command (plus a couple more for this
 *              program's use).  They are set by the radio buttons and
 *              entry values in the Interpolator options, and sent to
 *              the Cascade Historian when any query is made.
 *------------------------------------------------------------------*/
class InterpolatorSettings
{
  fn = "NoInterpolator";
  y_history = "MV_001";
  start = 20000;
  duration = RECORD_TIME + 1;
  x_history = "PV_001";
  interval = .3;
  max_gap = 100;
  dbflag = "NONE";
  misc_info = "";
}

/*--------------------------------------------------------------------
 * Method:      InterpolatorSettings.set_defaults
 * Returns:     t or nil
 * Description: Sets the default .start to correspond to the last time
 *              the "Record" button was toggled on and off, and resets
 *              the .dbflag.
 *------------------------------------------------------------------*/
method InterpolatorSettings.set_defaults(button, entry)
{
  if (button.switched_on())
    {
      .start = clock() - find_midnite();
      entry.set_text(string(.start));
      .dbflag = "NONE";
    }
}

/*--------------------------------------------------------------------
 * Method:      InterpolatorSettings.set_interpolator
 * Returns:     a query buffer ID number
 * Description: Sends a query to the Cascade Historian (corresponding to
 *              the task parameter), based on the information in the
 *              InterpolatorSettings class.  
 *------------------------------------------------------------------*/
method InterpolatorSettings.set_interpolator(task)
{
  local tmp_history, tmp_history1, tmp_history2, intpl;

  switch (.fn)
    {
    case "TimeSetup":
      tmp_history = string(substr(.y_history, 0, 3), "001");
      intpl = send(task, `interpolate(@tmp_history, NoInterpolator,
                                      (@.start + find_midnite()),
                                      @.duration));
    case "RelSetup":
      tmp_history1 = string(substr(.y_history, 0, 3), "001");
      tmp_history2 = string(substr(.x_history, 0, 3), "001");
      intpl = send(task, `interpolate(@tmp_history1, RelativeInterpolator,
                                      (@.start + find_midnite()),
                                      @.duration, @tmp_history2));
    case "NoInterpolator":
      intpl = send(task, `interpolate(@.y_history, NoInterpolator,
                                      (@.start + find_midnite()),
                                      @.duration));
    case "Periodic":
        intpl = send(task, `interpolate(@.y_history, PeriodicInterpolator,
                                        (@.start + find_midnite()),
                                        @.duration, @.interval,
                                        @.max_gap));
    case "Relative":
      intpl = send(task, `interpolate(@.y_history, RelativeInterpolator,
                                      (@.start + find_midnite()),
                                      @.duration, @.x_history));
    case "FixedRelative":
      intpl = send(task, `interpolate(@.y_history,
                                      FixedRelativeInterpolator,
                                      (@.start + find_midnite()),
                                      @.duration, @.x_history,
                                      @.interval));
    }

  if (car(intpl) != #error)
    cadr(intpl);
  else
    {
      princ ("Cascade Historian error: \n", cadr(intpl), "\n");
      error("The Historian returned an error.");
    }
}

/*--------------------------------------------------------------------
 * Function:    assign_history
 * Returns:     A string
 * Description: Changes the point-name suffix (_001) of a history to match
 *              the corresponding deadbanded history.  Called by the
 *              .assign_values() function, which selects the proper
 *              history based on the Y history and X history drop-down
 *              boxes in the GUI.
 *------------------------------------------------------------------*/
function assign_history(dbset, history, entry)
{
  switch (dbset)
  {
        case "NONE":
          history = entry;
        case "DB":
          history = string(substr(entry, 0, 3), "DB");
        case "DBP":
          history = string(substr(entry, 0, 3), "DBP");
  }
  history;
}

/*--------------------------------------------------------------------
 * Function:    send_hs_command
 * Returns:     The return from the Cascade Historian,  or nil
 * Description: Sends commands to the Cascade Historian.
 *------------------------------------------------------------------*/
function send_hs_command(widget, purpose, pt, args)
{
  local tsk, arg0, arg1, arg2, arg3, ret1, ret2;

  /* Choose the correct history, based on the point name. */
  local pt1 = parse_string(string(substr(string(pt), 0, 3), "DB"));
  local pt2 = parse_string(string(substr(string(pt), 0, 3), "DBP"));
  
  if ((tsk = locate_task("demohistdb", nil)) != nil)
    {
      switch (purpose)
        {
        case "enable":
          if (widget.switched_on())
            ret1 = send(tsk, `enable());
          else
            ret1 = send(tsk, `disable());
        case "deadband":
          arg0 = string("\(absolute ", args[0], "\)");
          arg1 = string("\(percent ", args[1], "\)");
          arg2 = string("\(timelimit ", args[2], "\)");
          arg3 = string("\(countlimit ", args[3], "\)");
          ret1 = send(tsk, `deadband(@pt1, @arg0, @arg1, @arg2, @arg3));
          ret2 = send(tsk, `deadband(@pt2, @arg0, @arg1, @arg2, @arg3));

          /* Print the return values. */
          princ("Deadband status for ", pt1, ":\n", ret1, "\n");
          princ("Deadband status for ", pt2, ":\n", ret2, "\n");
        case "version":
          ret1 = send(tsk, `version());
          ret1;
        }
      close_task(tsk);
    }
  ret1;
}

/*--------------------------------------------------------------------
 * Function:    display_hs_info
 * Returns:     An integer (50)
 * Description: Puts the Cascade Historian version number and instructions
 *              into the text widget.  Used only at start-up.
 *------------------------------------------------------------------*/
function display_hs_info(txt_wgt)
{
  local version_string;
  for(i=0; i<=50; i++)
    {
      if ((version_string = string(send_hs_command(nil, "version", nil, nil))) != "nil")
        {
          anygui_show_text(txt_wgt,
                           string("Now running ", substr(version_string, 9, 17), "\n",
                                  substr(version_string,
                                         27, strstr(version_string, "at") - 27),
                                  "\n\nTo start the demo, ensure that\nthe PID Emulator ",
                                  "is running,\nthen press the Record button."),
                           3);
          i = 50;
        }
      else
        usleep(10000);
    }
}

/*--------------------------------------------------------------------
 * Class:       DeadbandSettings
 * Description: The instance variables of this class correspond to the
 *              four possible types of Cascade Historian deadbands.
 *              They are set by entry values in the Parameters window,
 *              and sent to the Historian by the set_parms() method.
 *------------------------------------------------------------------*/
class DeadbandSettings
{
  history;
  absolute = 5;
  percent = 0;
  timelimit = 1;
  countlimit = 0;
}

/*--------------------------------------------------------------------
 * Method:      DeadbandSettings.set_parms
 * Returns:     The return from the Cascade Historian,  or nil
 * Description: Assigns the values from the parameter array to the
 *              appropriate instance variables of the class, and sends
 *              them to the Cascade Historian using the send_hs_command()
 *              function.  Activated by the "OK" button.
 *------------------------------------------------------------------*/
method DeadbandSettings.set_parms(button, p_array)
{
  .absolute = p_array[0];
  .percent = p_array[1];
  .timelimit = p_array[2];
  .countlimit = p_array[3];
  send_hs_command(button, "deadband", .history,
               array(.absolute, .percent, .timelimit, .countlimit));
}

/*--------------------------------------------------------------------
 * Function:    find_midnite
 * Returns:     t or nil
 * Description: Finds the time of midnight in system time.  This is the
 *              basis for the History time axis values and plots.  It
 *              makes the times shorter and easier to read, because all
 *              times are in seconds.
 *------------------------------------------------------------------*/
function find_midnite()
{
  local midnite;
  
  midnite = cadr(cddr(string_split(date(clock()), " ", 4)));
  midnite = string_split(midnite, ":", 2);
  midnite = (number(car(midnite)) * 3600)
    + (number(cadr(midnite)) * 60)
      + number(caddr(midnite));
  midnite = floor((clock() - midnite)/100) * 100;
  midnite;
}

/*--------------------------------------------------------------------
 * Function:    min_max
 * Returns:     t or nil
 * Description: Finds the minimum and maximum values in a
 *              list of numbers and returns them as a list.
 *------------------------------------------------------------------*/
function min_max (numlist)
{
  local max = car(numlist), min = car(numlist);
  with m in numlist do
    {
      if(min > m)
        min = m;
      if(max < m)
        max = m;
    }
  list(min, max);
}