Scripting Software Bisque TheSkyX with JavaScript

Tutorial 1: Controlling the Imaging Camera

Terry R. Friedrichsen

Bunker Ranch Observatory

May 6, 2017

Introduction

If you're reading this, I'll assume that you're interested in writing scripts to control astrophotographic imaging 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 imaging. Much of what is said also applies if you're using some flavor of Windows.

Note that, while quite elementary, this is not a tutorial aimed at complete programming novices. It will be helpful to know something of programming, in particular object-oriented style, exception handlers, and the C and/or C++ programming languages. You may, however, find that you can glean what you need to know from context.

Here's the overall view of the way things work: TheSkyX is listening for JavaScript programs on TCP port 3040 (this is enabled via Tools | TCP Server). The script interpreter in TheSkyX will run the script and write a response over the TCP connection back to the uploader. You may be able to use the nc(1) command to send the script and receive the response, as:

nc -4 localhost 3040 < SCRIPTNAME.js

where SCRIPTNAME.js is the name of your script. I've found, however, that the Mac version of nc(1) is unreliable in that it does not always wait for a response, though it should. An alternate way to test your scripts is to paste them into Software Bisque's tcpscriptclient utility. Another way, especially good for initial script testing and debugging, is to invoke the built-in script handler in TheSkyX via Tools | Run Java Script. This brings up a window with a buffer into which you can paste the script and a checkbox to "Enable Debugger" so you can run the script and get execution feedback.

Sending scripts to TheSky

As an aside, let me note here that if you're going to write your own scriptable utility to send scripts to TheSkyX, the utility needs to send the script to TheSkyX in one single write(2); sending the script line-by-line will cause random failures in unpredictable ways. This is an unfortunate misfeature of TheSkyX or at least of the JavaScript engine it uses.

Further, there is a limit of 4096 bytes on the size of a script. While this, too, is an undesirable limitation, it is not as restrictive as it would appear to be, since scripts tend to be much shorter than this.

I use a utility I wrote called tsxfeeder to send scripts to TheSkyX and capture the returned result; it handles the write(2) restriction mentioned above and enforces the 4096-byte limit. It has other useful features, such as the ability to use command-line arguments to modify metavariables inside the script, so the same generic script may be used in multiple circumstances rather than having hard-coded values. The tsxfeeder utility will also optionally strip comments and blank lines from the script in order to conserve as much of the 4096-byte limit as possible.

Should you like or prefer Perl, tsxfeed is a Perl script that does much the same thing; it only talks to TheSkyX on the local host, however, due to laziness on my part.

A very trivial script

Let's use a simple script to return the status of the imaging camera. You can try out these scripts by selecting the "Software Bisque Camera Simulator" in TheSkyX.


/* Java Script */

ccdsoftCamera.Status;

The first line is a comment; it's used as a flag line to tell the script interpreter in TheSkyX that a JavaScript program is incoming, and must be presented exactly as shown. The second line tells the script interpreter to return the value of the Status variable in the ccdsoftCamera object. If the camera is not connected to TheSkyX, the result will be "Not Connected"; if the camera is connected and ready, the result will be "Ready".

Whatever value the script returns will have the script interpreter's final status appended with a vertical bar ("|") as a separator. For example, the script above returns

Not Connected|No error. Error = 0.

indicating that the camera is not connected and that the script completed execution normally. If your script doesn't return "No error.", that is cause for investigation.

More formally, the script interpreter will return the value of the last expression executed as the script result (which is not necessarily the last line of the script). This doesn't have to be a single variable value; it can be a string consisting of a number of values, as we'll see below. The result can even be built up gradually during the script's execution and returned as a value at the end.

A useful script

Now let's connect the camera, turn on the cooler, and return the camera's temperature and cooler status:


/* Java Script */

var Out;

/* copy the camera object */
var Imager = ccdsoftCamera;

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

/* connect the camera, since we want to talk to it */
Imager.Connect();

/* turn temperature regulation on */
Imager.RegulateTemperature = true;

/* report the current temperature status */
Out = Imager.Status + "/" +
                        String(Imager.RegulateTemperature) + "/" +
                        String(Imager.TemperatureSetPoint) + "/" +
                        String(Imager.Temperature) + "/" +
                                    String(Imager.ThermalElectricCoolerPower);

We do a couple of things here that will help make things easier for more complicated scripts. First, we declare a variable named Out which will hold the intended final script result. It's not really necessary for this simple script, but the convention can be useful for more complex scripts. Second, we use the variable Imager to give a succinct name to the camera object that is also somewhat easier to type.

The next line turns off asynchronous operation. If you don't know what this means, just ignore it for now. If you do know what it means, just be aware that we're always going to run synchronously for the purposes of this initial tutorial.

A digression into the documentation

Now it's time for a word on how to read the scripting documentation on the Software Bisque web site.

In the blue bar across the near-top of the page, you'll see Modules. Select that, and you'll be presented with a list of all modules. The module that provides imaging control is CCDSoft Classic Objects. Selecting that shows two classes; the one you want is ccdsoftCamera for camera control. ("CCDSoft" is historical; it is the name of Software Bisque's predecessor camera-control program.)

You're now presented with the ccdsoftCamera Class Reference. We'll skip the Public Types for now and move down to the next category, Public Slots. These are the "methods", in object parlance — callable functions that cause the software to perform some task. They are "Public" in the sense that they are externally visible to users; there may also be "private" methods that are used internally by the software package and are not intended for the use of others.

Farther down the documentation page is a heading for Properties. Look, for example, at RegulateTemperature; this is a variable that holds an integer 0 or 1 if temperature regulation is off or on. Assigning a 1 (or the synonym value true as we use in this script) will turn regulation on. Many of the properties are value-result variables like this; they not only hold the value of the property, but also, changing the property's value updates the camera hardware.

We previously skipped over the Public Types documentation; this describes enumeration data types in the class. This defines (enumerates) a restricted set of values that are the only values a variable of the given enumeration type may contain. The C programming language calls this an enum type, so we'll adopt that convention. Unfortunately, these enum definitions are not visible to our script, so you'll have to know what values are associated with the names in the enum definition.

As an example, the first is ccdsoftImageReduction, with values

cdNone, cdAutoDark, cdBiasDarkFlat

which are 0, 1, and 2, respectively. These are the only values ccdsoftImageReduction is allowed to have. However, the next enum in the documentation, for ccdsoftImageFrame, defines the first value, cdLight, as 1, so the following values are 2, 3, and 4:

cdLight = 1, cdBias, cdDark, cdFlat

It is a good idea, when using an integer constant that is really one of the enum values, to document via the comments, for posterity, which enum value corresponds to that integer. We'll take our own advice in subsequent scripts.

Returning information from the script

Referring back to our script, we invoke the Connect() method, which establishes communication with the camera. It returns an int data type; the documentation eventually leads you to the source listing for sberrorx.h, which documents all possible error codes (most of which are not actually relevant to a camera connection) and their corresponding error message strings. You'll find that a return value of 0 means "No error". However, the documentation also notes that Connect(), along with many other methods, throws an exception on error. Throwing an exception terminates execution of the script at that point and causes the exception handler to run; the remainder of the script will not be run, and thus an error in Connect() will never return to our script. As a consequence, the script won't actually see any return value other than 0 — so our script ignores the return value of the Connect() call.

The exception handler does, however, get a meaningful error message from Connect(), which it uses as the script termination value in lieu of whatever your script normally would have returned. For example, though preventing the return of an error code from the Connect() call, the exception handler causes the script interpreter to terminate the script and returns (e. g.)


TypeError: No device has been selected. Error = 225.|No error. Error = 0.
as the script result, meaning that TheSkyX has not been configured to control a camera device. The "No device" message has been provided by the Connect() code for the use of the exception handler in terminating the script. (Recall that "No Error." after the vertical bar means the script itself had no errors; the execution of the script may, and in the current hypothetical example has, encountered an error.)

The return message we'd like from the script if no errors occur is built from several of the camera's property values in which we are interested: whether the cooler is enabled, what its set point is, the camera's current temperature, and the percentage of cooler power being used. Take note of the variable type of the property as specified in the documentation; it is most often int or double (for an integer or a decimal value), but may be a string or an enum defined in the Public Types section of the documentation. If you wish to return the value of an enum, it would be worthwhile to go to the effort of translating the actual small integer to the corresponding enum value's name string rather than cryptically returning the integer.

Finally, we build an output string consisting of the values we want from the camera, separated by slashes. For the numeric values, e. g. the temperatures, the function String() is used to convert the numeric value to a string for output. This string will be the return value of the script; we assign it to Out, as mentioned above, as a convention which would allow us to extend this script to add additional information to Out before returning. In a similar manner, any of the other properties can be returned.

A script that takes a dark frame

As a final example for this part of the tutorial, let's command the camera to take a dark frame:


/* Java Script */

var Out;

var result;

/* copy the camera object */
var Imager = ccdsoftCamera;

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

/* take a frame of the given type (cdDark == 3) */
Imager.Frame = 3;

/* how long to expose the dark frame (seconds) */
Imager.ExposureTime = 30;

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

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

/* take an image */
result = Imager.TakeImage();

/* report the result */
Out = String(result);

Notice that we don't connect the camera in this script; if the camera is disconnected, it will be automatically connected by the TakeImage() call when this script is run. Not all camera actions will automatically cause a Connect(), however (and which ones do is undocumented); so it would be safer to connect the camera in the script, as connecting an already-connected camera is harmless. Also note that despite setting BinX and BinY before the camera is connected by TakeImage, the values set will be given to the camera.

We set the frame type to 3 (cdDark from the ccdsoftImageFrame enum) and set the ExposureTime property to 30 (it's in seconds, though the documentation omits this fact). We set the binning to 1 in both X and Y, and turn off automatic image reduction (cdNone from the ccdsoftImageReduction enum). Turning off image reduction is really unnecessary for dark frames; it is included here pedantically as an additional example of a variable that takes an enum value.

Finally, we invoke the TakeImage() method, which waits for the camera to take the dark frame, and capture the result value. As with Connect() above, the return value is 0 if no error occurred; any error causes an exception to be thrown and execution of the script to terminate (thus TakeImage() will never return an error), and the exception handler sends an error message provided by TakeImage() as the script result. If TakeImage() returns normally, we convert the return value into a string, and make that string the return value of the script, following our convention of using Out for the purpose.

It is perhaps interesting to realize that the result of the TakeImage() is an integer, not a string. Even so, if you remove the final line of this script, the script interpreter will still return a string "0" as the script value. One of the joys of an interpreted language is a very loose allegiance to data types.

This script isn't quite ready to use yet, because the dark frame will not be saved. You can accomplish this by enabling AutoSave, either manually or within the script. AutoSave can be mostly configured (not just enabled) via script, as well, though there is no facility to set the filename formats. My practice is to enable AutoSave before TakeImage() and disable it immediately after; that way, I ensure that unwanted images do not get saved.

Demonstrating some features of the language

Bonus round! Here's a script to list the names of the filters in the filter wheel. This demonstrates how to write if tests and one way to perform loops. You'll also see how a script return value can be built in pieces during script execution and returned as a whole at the end.


/* Java Script */

var Out;
var idx;

// copy the camera object
var Imager = ccdsoftCamera;

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

// there may not be a filter wheel
if (Imager.filterWheelIsConnected()) {
    // return the number of filters
    Out = "Camera filters (" + String(Imager.lNumberFilters) + "):  ";

    // and the list of filter names
    for (idx = 0; idx < Imager.lNumberFilters; idx++) {
        Out += " " + Imager.szFilterName(idx) + ",";
    }

    // remove the trailing comma
    Out = Out.slice(0, -1);
}
else {
    Out = "Filter wheel not connected";
}

Out = Out;

The final "Out = Out;" is there just to make the script's return value clear (and, if you've been following along, you'll realize that it could simply read "Out;"). You can test that this script even works if you omit that final line completely, because the last thing the script does, in either branch of the if, is calculate the result string we want to emit (recall that the script returns the result of the last expression evaluated). The script also illustrates a different comment style. Note that it helps a great deal to be familiar with the C and/or C++ programming languages!

For the uninitiated: inside the parentheses of the for looping statement in this example, the first expression (delimited by a semicolon) is an initialization that is performed once before the loop begins. The second expression is the loop "continuation condition": the loop continues so long as the value of the integer variable idx (which the loop started at 0) is less than the number of filters (it is common practice in many programming languages to begin indexing operations with index number zero). The third expression uses the "++" operation to increment the variable idx; since idx is an integer, this means to add one.

Also note the use of "+=" in the line after the for statement to mean "add this new stuff to whatever the variable Out already contains". In the next statement, slice is used to modify the string in Out beginning at position 0 (note indexing from 0 again!) by removing one character counting from the end of the string.

One last word: there is no contest prize for writing a script in the fewest number of lines, words, or characters. The important thing is that the script be clear, readable, and comprehensible (and yes, this means writing comments, too). A script that you can't fully understand a year from now is a script you can't easily modify two years from now, so give yourself a break. And remember that somebody else might be trying to modify the script 3 years from now — give him or her a break, too. Especially bear in mind that comments and descriptive variable names are your friend, not your enemy. And allow me to mention that comments are an excellent idea, if I haven't done so more than a couple of times already.

Some next steps

At this point, you're ready to explore some more. Try these tasks:

Good luck!