14.6. GTK: Sending Queries - send_query, .get_data

The send_query function calls the .assign_values method to get the currently-entered values from the various entry widgets, and then sends two queries. The first query is a set-up query, which is used to find the maximum and minimum X and Y values for the interpolated data. Those values are then stored in a list.

   /*--------------------------------------------------------------------
    * Function:    send_query
    * Returns:     t or nil
    * Description: Calls the necessary functions to send a query to the
    *              Historian, and handle the data returned.  It is activated
    *              by the "Record" button popping out, or any radio button.
    *              It sends two queries, the first to calculate the minimum
    *              and maximum values for use in creating plots, and the
    *              second to get the actual data.
    * Arguments:   button:       The button that activated the function
    *              iset:         An InterpolatorSettings instance
    *              e1 - e6:      The six Interpolator options entry widgets
    *              e7:           The "Number of points from last query" display
    *              b1 - b4:      The four Interpolator options buttons
    *              b5:           The Record button
    *              b6:           The Update display button
    *------------------------------------------------------------------*/
   function send_query(button, iset, text, e1, e2, e3, e4,
                       e5, e6, e7, b1, b2, b3, b4, b5, b6)
   {
     local fnholder, datalist, minmaxlist;
     if ((button == nil) || (button == b6) || (button.switched_on()))
       {
         if ((button == b1 || button == b2 ||
              button == b3 || button == b4 ||
              button == b5 || button == b6))
           iset.dbflag = "NONE";
         
         iset.assign_values(e1, e2, e3, e4, e5, e6);
         
         datalist = list();
         fnholder = iset.fn;
   
         /* Assign a name for the setup query. */
         if ((iset.fn == "NoInterpolator") || (iset.fn == "Periodic"))
           iset.fn = "TimeSetup";
         else
           iset.fn = "RelSetup";
   

There are two possible setups: time and relative, depending on what the X-axis represents. The NoInterpolator and Periodic options plot a single history against time, and thus give time-based output. The Relative and FixedRelative options plot one history against another, and thus require the X-axis to have values ranging from 0 to 200.

To create the largest plot possible in the given space, we need to set the range of our axes to the minimums and maximums of the data set. This information is obtained using the min_max function.

         /* Make the setup query and assign minimum and maximum values. */
         datalist = iset.get_data(e1, e2, e3, e4, e5, e6, e7);
         if (car(datalist) && cadr(datalist))
           minmaxlist = list(min_max(car(datalist)), min_max(cadr(datalist)));
         else
           minmaxlist = nil;
   
         /* Make the actual query. */
         iset.fn = fnholder;
         datalist = iset.get_data(e1, e2, e3, e4, e5, e6, e7);
         show_data(text, iset, button, datalist, minmaxlist);
       }
     else
       {
         write_minmax(iset.misc_info);
       }
   }
   

Both the first and second query bring in their data using the .get_data method (below). The second query then shows the actual requested query data using the show_data function. The final else statement is not necessary when this function gets called by pressing the Record button, or any of the Interpolator options buttons. But when it gets called by one of the Deadband options buttons, it is necessary to write the minimum and maximum values to the plhistsetup.dat file for gnuplot. This is done with the write_minmax function.

The .get_data method of the InterpolatorSettings class sends a query to the Cascade Historian, using an interpolate command. The interpolator used is determined by calling the .set_interpolator method. The Cascade Historian returns that interpolated query as a list of lists, with each sub-list containing a pair of numbers. The .get_data method extracts those numbers and writes them in two columns in the output file. This method then creates and returns a list of two sub-lists, each of which contains all the values for one column. Why bother writing the data to a file when we have it in memory as a list? Because for plotting we need access to the query results of all three deadband options at any given time.

   /*--------------------------------------------------------------------
    * Method:      InterpolatorSettings.get_data
    * Returns:     a list of two lists
    * Description: Sends the query and retrieves the data.  Activated by 
    *              the send_query() function.
    *------------------------------------------------------------------*/
   method InterpolatorSettings.get_data(e1, e2, e3, e4, e5, e6, e7)
   {
     local tsk, intpl, buf_length, alldata, count, dur, data, outfile;
     local xdata = list();
     local ydata = list();
     local midnite = find_midnite();
   

There are several ways that this function might fail. For example, the Cascade Historian might send an error message instead of the requested data. The .set_interpolator function that receives that message will also throw an error. This error is contained here by this try/catch block. The catch statement (at the end of the function) simply sends nil instead of data.

     try
       {
         /* Make sure there are valid values in the entry widgets.*/
         if (undefined_p(.start) || (!number_p(.start)))
           {
             .start = 0;
             e2.set_text("0");
           }
         if (undefined_p(.duration) || (!number_p(.duration))
             || (.duration <= 0))
           {
             .duration = 10;
             e3.set_text("10");
           }
         if (undefined_p(.interval) || (!number_p(.interval))
             || (.interval < .01))
           {
             .interval = .01;
             e5.set_text("0.01");
           }

We first test the values entered in three of the entry widgets, using Gamma's undefined_p and number_p predicate functions.

         
         if ((tsk = locate_task("demohistdb", nil)) != nil)
           {
             alldata = list();
             count = 0;      
             intpl = self.set_interpolator(tsk);
             buf_length = caddr(send(tsk, `bufferIdLength(@intpl)));
             if(!((.fn == "TimeSetup") || (.fn == "RelSetup")))
               e7.set_text(string(buf_length));
             
             while (count < buf_length)
               {
                 if((buf_length - count) > 100)
                   dur = 100;
                 else
                   dur = buf_length - count;
                 data = send(tsk, `bufferIdDataAscii(@intpl, @count, @dur));
                 data = cddr(cdr(data));
                 alldata = append(alldata, data);
                 count = count + 100;
               }
             

Since the interpolated data set can be very large, potentially requiring long processing times, we used the Cascade Historian's bufferIdLength command to calculate the length of the buffer, which then allows us to process it in chunks. The chunks can be any size; we chose a size of 100 values. For each chunk we send a bufferIdDataAscii command to get the values in ASCII format.

Next we choose an output file, based on the deadband selected in the Deadband options, and then after checking to make sure there is data, we write the data to the output file.

             if(.dbflag == "NONE")
               outfile = open("/tmp/cogentdemo/hsoutput.dat", "w");
             if(.dbflag == "DB")
               outfile = open("/tmp/cogentdemo/hsoutput2.dat", "w");
             if(.dbflag == "DBP")
               outfile = open("/tmp/cogentdemo/hsoutput3.dat", "w");
             if (outfile)
               if (buf_length == 0)
                 {
                   writec(outfile, "# Sorry, no data matched your query.");
                   writec(outfile, string("\n", midnite, " 0",
                                          "\n", (midnite + 1), " 0"));
                   close(outfile);
                 }
               else
                 {
                   with d in alldata do
                     writec(outfile, string(car(d), " ", cadr(d), "\n"));
                   close(outfile);
                 }
             
             while (alldata != nil)
               {
                 if(.fn == "TimeSetup" || 
                    .fn == "NoInterpolator" || .fn == "Periodic")
                   xdata = cons(caar(alldata) - midnite, xdata);
                 else
                   xdata = cons(caar(alldata), xdata);
                 ydata = cons(cadar(alldata), ydata);
                 alldata = cdr(alldata);
               }
             xdata = reverse(xdata);
             ydata = reverse(ydata);
             send(tsk, `bufferIdDestroy(@intpl)); 
           }
         list(xdata, ydata);
       }
     catch
       list(nil, nil);
   }
   

Each deadband option get its own output data file (hsoutput*.dat), which gets rewritten each time a query is made on that deadband. All the data files in the demo are written to the /tmp/cogentdemo/ temporary directory, and deleted when the Controller exits.

In addition to the written output file, we also want to have the data available as a list, to be returned to the send_query function (above) for further use. After constructing the list, we have no more use for the data buffer, and so destroy it to free up memory, using the Cascade Historian's bufferIdDestroy command. Then we create the data list using the Gamma list function, and it becomes the return value of the function.