Scripting Software Bisque TheSkyX™ with JavaScript

Tutorial 3: A Digression -- Scripting Your Scripting

Terry R. Friedrichsen

Bunker Ranch Observatory

June 21, 2014

Introduction

So far, we've learned a bit about controlling a camera, and a bit about controlling a mount, all using Software Bisque's TheSkyX™ Professional with the Camera Add-On. I use TheSkyX Pro on a Mac running OS X (MacOS); accordingly, this tutorial will use the JavaScript capability of TheSkyX to control things. Much of what is said also applies if you're using some flavor of Windows.

Up to this point, we've fed predetermined scripts to TheSkyX, hand-editing them as necessary (or making trivial modifications with sed(1)) to customize them for such things as selecting the target object. What we'd really like is to have a nice user interface, asking questions in obsequious tones and placating us with unctuous phrases when we commit the occasional but inevitable faux pas. (Well, actually, that's not my personal preference — user interfaces I write for myself tend more toward the Don Rickles school of interaction!)

But that, of course, leaves open the question of how to get the choices, once solicited, into the JavaScript programs that will actually command TheSkyX. To resolve that, we need a convention by which the JavaScript programs can contain placeholders at which we substitute the variable pieces of information the scripts require. Such a convention should allow for multiple substitutions within a single JavaScript program, and for easy identification of which substitution to employ.

To that end, let's agree to identify these metavalues with a dollar sign ("$") followed by exactly 3 decimal digits with leading zeros expressed -- thus "$000", "$001", etc. That gives us a total of 1,000 possible metavalues, which is approximately 990 more than we're ever likely to need.

Next we need some way of substituting the actual values for the metavalues written into the JavaScript code. This is a perfect job for perl(1), so I've written a program called tsxfeed.pl to do this; it reads the script, makes the substitutions for the metavalues, and then delivers the resultant modified script directly to the script interpreter in TheSkyX for execution. The command line for this program takes the name of the script and then a list of the substitute values, in the order required by the numeric metavalue naming scheme.

We could write our scripts by sprinkling those metavalues throughout, when and where needed, but that would make the scripts difficult to read and difficult to modify, which, if you've been following along in these tutorials, violates the very highest rule of coding espoused here. Rather, we adopt the convention that the first few lines of a script immediately contain assignments that give meaningful names to the metavalues; those names are then used throughout the script. This also serves as a bit of documentation of the meaning of each value and its place in the command line, and therefore addresses code maintainabilty, as well.

Let's take a look at an example; this script commands the imaging camera to take a dark or bias frame, and save the resulting image in a location of our choosing:

/* Java Script */

// take a light or flat frame with the given parameters
// return:   frameType/filterName/{0/Success | 1/error message}

/* external variables */
var autosavePath = "$000";
var autosavePrefix = "$001";
var frameType = $002;
var binXY = $003;
var exposureTime = $004;

/* values from the enum types we reference */
const cdNone = 0;

var result;

var Out;

/* grab a camera object */
var Imager = ccdsoftCamera;

/* always wait for the camera */
Imager.Asynchronous = false;

/* take a frame of the given type (cdBias = 2, cdDark = 3) and duration */
Imager.Frame = frameType;
Imager.ExposureTime = exposureTime;

/* set binning (assume x and y are the same for now) */
Imager.BinX = Imager.BinY = binXY;

/* and turn off automatic image reduction */
Imager.ImageReduction = cdNone;

/* save the frame here */
Imager.AutoSavePath = autosavePath;
Imager.AutoSavePrefix = autosavePrefix;
Imager.AutoSaveOn = true;

/* take an image, watching for errors */
try {
    result = Imager.TakeImage();
    Out = String(frameType) + "/0/Success";
}
catch (imgerr) {
    Out = String(frametype) + "/1/" + imgerr.message;
}

/* turn AutoSave back off for safety */
Imager.AutoSaveOn = false;

/* report the result */
Out;

Note that we have 5 metavalues, some of which are strings and some of which are numeric, all assigned to descriptive variable names at the very top of the program. Those variables are then used throughout the remainder of the code. Assuming that the script's name is camtakedarkbias.js, a typical command to run this script would be:

tsxfeed camtakedarkbias.js "/u/terry/astroimages/140615" "STL" 3 1 600
specifying the autosave path and use of an STL camera to take a dark frame (cdDark) at 1x1 binning for 10 minutes (= 600 seconds).

As you can tell, we're beginning to get more serious about writing production-quality code here, by steps, with a standardized error message format documented in a comment near the top. In fact, we're serious enough about production mode that we're actually saving these dark frames with TheSkyX's autosave feature. We've also taken a different approach to documenting the enum value names we need in the program. Rather than simply documenting the correspondence in a comment, we use a JavaScript const declaration to assign a name and give it a value.

We wrap the TakeImage() in a try ... catch exception handler. Despite the fact that TakeImage() returns a value, and that value is documented to contain the error code, the only value ever returned is 0 on success; failure throws an error that we catch here.

I know what you're thinking: "This script isn't done; it doesn't say anything about the camera temperature, which is rather important, and besides, I need to take way more than just one dark frame!" Patience, grasshopper; we are learning to walk, but soon we will run.

There are multiple ways to approach scripting for entire observing sessions. You could write long and complex JavaScript programs that essentially turn all control over to TheSkyX for the evening, or you could write short scripts to do single individual tasks and control the session with other host-level logic that is external to the scripts themselves. You can also envisage some mixture of the two approaches.

We take the approach of writing multiple short, almost trivial, scripts and guiding the overall process with programs that have access not only to the scripts but also to the host filesystem and other resources. This offers levels of flexibility that we can't get by restricting ourselves to doing everything in JavaScript under control of TheSkyX. If, for example, we decided to refocus after every LRGB set, it would not entail modifying scripts (with the potential for error that would bring), but simply insertion of a new script into the overall flow.

So in this case we have a script that takes one dark frame, at any temperature. Other scripts will set and control the camera's temperature, and higher-level logic will execute the one-dark-frame script the requisite number of times to capture all of the dark frames desired. Scripts such as these are not written with user-friendliness in mind; they are not intended for direct user access, but rather for utilization by other software.

It's a lot more fun to take light frames than dark frames; the chief difference (aside from an open shutter!) being that it is common to employ filters for RGB or narrowband imaging. Since we also take flat-field frames through filters, the next script handles both light frames and flats; it mostly follows the previous script, with the addition of a filter name argument and some filter-handling code:

/* Java Script */

// take a light or flat frame with the given parameters
// return:   frameType/filterName/{0/Success | 1/error message}

/* external variables */
var autosavePath = "$000"
var autosavePrefix = "$001";
var frameType = $002;
var binXY = $003;
var exposureTime = $004;
var filterName = "$005";

/* values from the enum types we reference */
const cdNone = 0;

/* grab a camera object */
var Imager = ccdsoftCamera;

var Out;


/* find the filter index given the filter name */

function filterIndex(filtername) {
    var i;
    var filter;

    /* search the filter wheel slots for the given filter */
    for (i = 0; i < Imager.lNumberFilters; i++) {
        filter = Imager.szFilterName(i);
        if (filtername.toUpperCase() == filter.toUpperCase())
            return i;
    }

    /* no such filter */
    return -1;
}



/* take a light or flat frame through the requested filter */

function main() {
    var filterIdx;
    var result;

    /* all error messages start this way */
    Out = String(frameType) + "/" + filterName;

    /* always wait for the camera */
    Imager.Asynchronous = false;

    /* take a frame of the given type (cdLight = 1, cdFlat = 4) and duration */
    Imager.Frame = frameType;
    Imager.ExposureTime = exposureTime;

    /* if we have a filter wheel, set the filter to use */
    if (Imager.filterWheelIsConnected) {
	filterIdx = filterIndex(filterName);
	if (filterIdx < 0) {
            Out += "/1/Filter not found";
	    return;
	}

	/* select the appropriate filter */
	Imager.FilterIndexZeroBased = filterIdx;
    }

    /* set binning (assume x and y are the same for now) */
    Imager.BinX = Imager.BinY = binXY;

    /* and turn off automatic image reduction */
    Imager.ImageReduction = cdNone;

    /* save the frame here */
    Imager.AutoSavePath = autosavePath;
    Imager.AutoSavePrefix = autosavePrefix;
    Imager.AutoSaveOn = true;

    /* take an image, watching for errors */
    try {
	result = Imager.TakeImage();
	Out += "/0/Success";
	return;
    }
    catch (imgerr) {
	Out += "/1/" + imgerr.message;
    }	

    /* turn AutoSave back off for safety */
    Imager.AutoSaveOn = false;
}

main();
Out = Out;
We're back to using functions in JavaScript: one to find the filter index corresponding to the filter name, and another to handle the main flow of control, which allows us to bail out early if the filter name is not found.

We declare a couple of variables in the filterIndex() function that are not used by the rest of the code; declaring them inside the function makes them visible only to that function. It is good programming practice to let code only see what data it needs to see; it's a precept of object-oriented programming, but you don't have to be writing object-oriented code to follow it. We do the same thing with some variables used only by function main().

A little object-oriented JavaScript is also in use inside filterIndex(). We compare the filter name we're given with the filter name from the filter wheel by invoking the toUpperCase() method of the string object to cover the possibility of case mismatches in the names.

Now all that is necessary to take a possibly-filtered light frame or flat-field frame is:

tsxfeed camtakelightflat.js "/u/terry/astroimages/140615" "STL" 1 1 600 Blue
specifying the autosave path, an STL camera, a light frame (cdLight) at 1x1 binning, and a 10 minutes (= 600 seconds) exposure using the blue filter. (If the particular camera in use doesn't have a filter wheel, specifying "none" in place of "Blue" will cause a harmless value substitution for the "$005" filterName metavalue.)

We can also use the metavalue mechanism to fix up the mount-slewing program from the previous tutorial (#2). We finished that tutorial by setting the target name to "farble" and using sed(1) to change "farble" to the object name of interest before sending the script to TheSkyX. Now, instead, we can make the top few lines of the script read:

/* slew to this target */
var Target = "$000";

/* don't slew to an object below this altitude (degrees) */
var altLimit = $001;

A command sequence like:

tsxfeed mountslew.js "M 33" 30

tsxfeed camtakelightflat.js "/u/terry/astroimages/140615" "STL" 1 1 300 Red
tsxfeed camtakelightflat.js "/u/terry/astroimages/140615" "STL" 1 1 300 Green
tsxfeed camtakelightflat.js "/u/terry/astroimages/140615" "STL" 1 1 300 Blue

tsxfeed camtakedarkbias.js "/u/terry/astroimages/140615" "STL" 3 1 300
will slew to Messier 33, take an RGB series, and take a dark frame. If your setup is capable of taking 5-minute images without guiding, you're practically done with scripting your imaging sessions (we need to worry about camera temperature yet).

Given the versatility of the tsxfeed program and the associated metavalue mechanism, we can control an evening's astrophotography session using practically any programming language or programmable shell. We can thus script our use of the scripts — scripting our scripting will be the way to build whole imaging sessions out of sequences of small standalone actions.