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:
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.
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.
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
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.
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.
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
enum
in the documentation, for
ccdsoftImageFrame, defines the first value, cdLight,
as 1, so the following
values are 2, 3, and 4:
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.
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.
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.
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.
At this point, you're ready to explore some more. Try these tasks:
Imager.Frame
Imager.FilterIndexZeroBased
(check to see that the filter
wheel is connected with Imager.filterWheelIsConnected
first)
Good luck!