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 600specifying 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 Bluespecifying 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 300will 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.