EOST Instrument Controller Software Programmers Manual

Danbury Corporation

Gary J. McGrath

April 30, 1997


Table of Contents


Introduction

The purpose of the Instrument controller is to control all of the peripheral components of the instruments that are not controlled by the Leach Controller and Guide Camera Controller. These components include the WFCam Shutter Controller, WFCam and HRCam filter changers, and HRCam Adaptive Optics Tertiary Servo Controller. All of the commands to control these components originate from the Telescope Operator Interface, and are transmitted to the Instrument Controller via a network connection. In addition, the components can all be controlled from dialog windows in the Instrument controller. A Network interface window is also available to view the bi-directional communications on the Network.

Windows 95 was chosen as the operating system because of the built-in support for network communications, the integrated Winsock TCP/IP stack, the availability of low-cost high quality programming tools, and the use of low cost industry standard PC hardware.

The Instrument Controller hardware consists of a Pentium PC with ISA and PCI slots. A National Instruments PC-DIO-96 Digital I/O board is used to interface to the WFCam shutter controller and the filter insert logic boards. A National Instruments AT-232 board is used to communicate to the tertiary controller and filter changer stepper motor controller. An NE2000 compatible Network Interface Card is used to connect to the ethernet network hub.


Development Environment

Borland C++ 5.0 is used to develop the Instrument controller project. Borland C++ has an Integrated Development Environment (IDE) that contains all of the tools needed for the development. The AppExpert was used to create a project file and source code for a very simple application. Once AppExpert creates the project, it cannot be run again to edit the parameters. Also, there is no way to determine the parameters that were used to generate the initial application project that AppExpert created.

Once the initial project was created, it was customized to the final application by adding classes with ClassExpert, adding controls and menus with the Resource Editor, and editing source code with text editors.

The source code is located on the Pentium 100 machine (venus), in the directory C:\DATA\SRC\INSTRMENT. The IDE can be run by double clicking on the Borland C++ 5.0 icon on the Windows 95 desktop.


Object Windows Library

Windows programming naturally lends itself to Object Oriented Design. Borland includes a class library called "Object Windows Library" that contains most of the Windows API. OWL is used extensively wherever possible to encapsulate code and data, and to increase code reuse. In addition, ClassExpert is a very helpful tool that works well with OWL. Some of the classes used in this project include TInstrumentApp, TSDIDecFrame, TInstrumentAboutDlg, TMainDlg, TFilterDlg, TShutterDlg, TTOISocketDlg, TTertiaryDlg, and TTertiaryEdit.

AppExpert originally created the classes TInstrumentApp, TSDIDecFrame, TInstrumentAboutDlg, and TMainDlg. TInstrumentApp is the application class for this application. TSDIDecFrame is the Single Document Interface (SDI) extension of the TDecoratedFrame class. The TInstrumentAboutDlg is the dialog that is displayed when "About" is selected from the help menu. These three classes required little or no changes since AppExpert created them.

TMainDlg is the main TDialog class. This is the dialog that you see when the program is first started. It has a menu bar that contains "File", "View", and "Help" menus. The "File" menu has only one item, "Exit", which exits the program. The "Help" menu also has only one item, "About", which displays general information about the program. The "View" menu contains several items for displaying the other dialog boxes. These include "Operator Interface Socket", "Tertiary RS-232", "Shutter Servo", and "Filter Changer". The TMainDlg also contains radio buttons for selecting the active instrument and filter. There are edit controls for displaying the status of guiding, adaptive optics, and shutter.

The TFilterDlg is the class for the dialog that controls the filter changers. It contains an edit control for entering the stepper motor desired position, and a Push Button control for telling the stepper motor to move. There are also radio buttons for inserting and retracting the filter.

The TShutterDlg class contains Static Text, Text Edits, Push Buttons, Check Boxes, and Group Boxes. These controls are used in four levels of control for the shutter controller. The lowest level controls the Digital I/O ports directly. The second level allows the user to read and write to a particular address on the motion control chip. The third level controls the individual registers of the motion control chip. The highest level of control allows the user to initialize the controller and start an exposure of a specified duration.

TTOISocketDlg is the class that displays the communications between the Telescope Operator Interface (TOI) and the Instrument controller. This class contains a Text Edit control to display the communications.

The TTertiaryDlg class displays the dialog between the Instrument Controller and the Tertiary Servo Controller. It also allows the user to enter commands from the keyboard to be sent to the Tertiary Servo Controller. This class uses the derived class TTertiaryEdit to display the communications. This class was derived from TEdit to add the functionality of capturing the keyboard input in order to send commands to the RS-232 serial port.

In addition, there are other classes that are not derived from OWL classes. The class TTOIServerSocket is the interface to the network communications. It is derived from the class TServerSocket. TTOIServerSocket has member functions for interpreting received messages and sending response messages. TServerSocket is responsible for opening a network socket, sending, and receiving data. TServerSocket is derived from TSocketBuf that serves to buffer the incoming data.

The TTertSerialBuf class is the interface to the serial communications with the Tertiary Servo Controller. TTertSerialBuf is derived from TSerialBuf. TSerialBuf is responsible for opening the serial port, sending, and receiving data.


Programming Conventions

Adding a New Window Class

Every window in this program consists of a class derived from the TDialog class. To create a new class:

  1. Open ClassExpert by double clicking on the executable (AppExpert) line in the project window.
  2. Go to the Classes window of ClassExpert. Right click, and select "Add New Class…"
  3. Select TDialog for the base class. Type in the class name. The class name should start with the letter "T" (for Type), end with "Dlg" (for Dialog). Use upper and lower case, and chose a name that is intuitive (such as TShutterDlg).
  4. Check the "Use Long Filenames" checkbox. The Source File and Header File names will be automatically filled in.
  5. Enter a Resource ID. It should start with "IDD_" (Dialog ID), and use all upper case characters. Use a descriptive name such as "IDD_SHUTTER".
  6. A dialog will be displayed listing "New Dialog Resource Options". Verify that all options are correct. The "New resource to be placed in:" field should contain "instrumentapp.rc". The "New identifiers to be placed in:" field should contain "instrumentapp.rh" (not "instrumentapp.rc"). Select the desired type of dialog to create and press the "OK" button.
  7. The dialog class is now created. You can use the Resource Editor to customize the new dialog (See "Adding Controls to a Window" below).
  8. An instance of the dialog must now be created. All dialogs are created as children of the TMainDlg class. Go to the constructor of the TMainDlg in ClassExpert. Right click and select "Use Class…"
  9. Modify the added line to use the class pointer variable that you wish to use. It should begin with "p" (pointer), and contain the class name (such as pShutterDlg).
  10. Modify the constructor arguments. "Twindow* parent" should be changed to "this". "TResId resId…" should be changed to the resource ID label. "TModule* module…" should be changed to 0 (zero). Use the short version of the class constructor. (See OWL help for the TDialog class.)
  11. Add the variable declaration of the dialog pointer to the top of MainDlg.cpp under "Application Global Variables / Dialog Pointers".
  12. Add the #include for the dialog class header file to the top of MainDlg.cpp.
  13. Edit the file "instrumentapp.rh" so that dialog resource IDs are separated with their respective control resource IDs under them. This makes it easier to see which controls go with which dialog.

Adding Controls to a Window

Controls can be added, removed and modified using the Resource Editor.

  1. Select the dialog class in the ClassExpert, and right click. Select "Edit Dialog".
  2. The Resource Editor will open. You can select controls from the control pallet and add them to the dialog. Control resources can be edited by right clicking on the control and selecting "Properties…"
  3. Controls that are not accessed by the program (such as Static text) don't need to be assigned a resource ID. In this case, -1 (the default) should be used in the "ID" field.
  4. Controls that will be accessed by the program (such as Text Edit, and Push Button) should be assigned a resource ID. The "ID" field should contain a descriptive label that begins with "IDC_" (Control ID), and contains all uppercase characters (such as IDC_SHUTTER_INIT_BUTTON). An ID number will be assigned to the ID label automatically.
  5. The file "instrumentapp.rh" should be edited so that each dialog has its own controls listed in numerical order under it.

Setting up a Dialog to Open From a Menu

The View menu on the Main Dialog allows the user to open other dialogs. To add a dialog to this menu:

  1. Select the TMainDlg class in the ClassExpert, and right click. Select "Edit Menu".
  2. The Resource Editor will open to edit the menu bar for the Main Dialog. Select the "View" menu.
  3. Select the menu item where you want the new menu item to appear, and right click. Select "New Menuitem". A new menu item will appear in the menu.
  4. Select the new menu item. Edit the properties in the Property Inspector dialog. Enter the text you wish to display in the menu. Use an ampersand (&) before a character that you wish to use as a hot key. Enter the menu item ID, which should start with "CM_" (Command Notification) and contain all capital letters.
  5. Save and exit the Resource Editor. Select the TMainDlg in the ClassExpert. Open the "Command Notifications" in the "Events" box. Open the Command Notification of the menu item you just created. Right click on "Command", and select "Add Handler".
  6. Press the "OK" button in the Add Handler dialog box to accept the default handler function name.
  7. A new Command Notification function will be created in the source code for the TMainDlg. You must add the code to open the dialog. Add the following lines to the function:
          If(pDialogPointer->HWindow == 0) 
          {
             pDialogPointer->Create();
          }

This will create an instance of the dialog.

Using a Control Class in a Window Class

In order for a control to be accessed by a member function of the dialog class that it belongs to, a pointer to the control must be declared local to the class. This pointer will point to a control object that is created within the dialog class and contains the resources that are specified by the resource ID. This is useful for such things as setting and getting data from a control such as a Text Edit. Push Buttons do not need to be created in this way, since they contain no data to access. Control Notifications (discussed later) are used by a control to signal events such as Push Button presses. To create the control object:

  1. Select the dialog class from the ClassExpert to display the source code. Place the cursor in the constructor function of the source code where the control object will be created. Right click and select "Use Class".
  2. A "Use Class" dialog will appear. Select the "Dynamic" radio button, and select the "Use OWL base classes" check box. Select the control class from the list, and press the "OK" button. A template for the object creation will appear in the source code.
  3. Modify the added line to use the class pointer variable that you wish to use. It should begin with "p" (pointer), and be descriptive (such as pExposeDurationEdit).
  4. Modify the constructor arguments. "Twindow* parent" should be changed to "this". "TResId resId…" should be changed to the resource ID label. "TModule* module…" should be changed to 0 (zero). Use the short version of the class constructor. (See OWL help for the class you are working with.)
  5. The declaration of the control pointer must be added to the dialog class. Edit the header file for the dialog class, and add the declaration in an appropriate place in the class definition.

Getting feedback from Controls (Control Notifications)

It may be necessary to notify the class when events such as a button press or a check box click occur. These events notify the program through Control Notification functions. A Control Notification function can be added to a class as follows:

  1. Select the dialog class in the ClassExpert. Open "Control Notifications" in the Events window. Select the ID of the control that you want to be notified by. Select the event that you wish to be notified of (for example BN_CLICKED). Right click on the event and select "Add Handler".
  2. An "Add Handler" dialog will appear. Don't bother changing the function name because it will use the default anyway (Borland C++ bug). Press the "OK" button.
  3. A handler function will be added to the end of the source file, and the function will be added to the response table near the top of the source file. The function declaration will also be added to the class definition in the header file. The default function name must be changed in all three places to a more descriptive name. Code can be added to the function, which will execute when the event occurs.

Other event functions can be added to a dialog class in a similar way.

Using a Timer

A timer can be used to periodically trigger an event, such as servicing a communications port or updating a display. It is most convenient to use the SetTimer() member function of the TWindow class (parent of TDialog). A timer set up in this way is local to the window it was created in. See TWindow::SetTimer() for more information. A timer can be added to the system by:

  1. Add a #define for the timer ID near the top of the source file.
  2. Add the line SetTimer(TIMERID, timeout); to the SetupWindow() function in the dialog class. TIMERID is the ID defined in step 1. timeout is the period of the timer in milliseconds.
  3. Next, a timer event function must be added to the dialog class similarly to the way control notification functions were added in the previous section. Under "Windows Messages" in the events window, right click on "WM_TIMER". Select "Add Handler".
  4. Use the default function name, and change the name in the Event Table, Function Definition, and Declaration in the header file.
  5. Add the code to be executed periodically to the timer event function.

Using Multiple Threads

Multiple threads can be used for functions that take a lot of time to execute. For example, we don't want to hold up the whole program while we wait for a peripheral device. A thread can be easily added to the system using the base class TThread as follows:

  1. Open a new header file, and define a new class derived from the class TThread. It should have a public constructor and a private member function called run() that returns int. The header file should #include the files <classlib\thread.h> and <iostream.h>.
  2. Open a new source file and #include the files necessary to access the public functions you wish to run in the thread (globals.h and the class definition header file).
  3. Define the constructor function to do nothing. The start() member function should run the desired function(s) that you wish the thread to run and return zero.
  4. The thread object must be dynamically created in the dialog class where it will be used. This can be done in the dialog class constructor. A local pointer should be used to point to the thread object.
  5. A member function of the dialog class should be created which will be used to start the thread running. Its name should begin with "Start", and contain the action that will be performed (such as StartInitialization() ). This function should call the TThread member function GetStatus() using the thread pointer. A switch statement can be used on the return value of GetStatus(). Possible return values are created, running, suspended, finished, or invalid. (See TThread in OWL help.) The thread should never be suspended or invalid, so display an error message box if these occur. If the thread is created, call the TThread member function Start(). If the thread is running, wait until the GetStatus() function returns finished (you can use Sleep() ). Then, delete the thread object, and create a new thread object. Then, run the Start() member function. Alternatively, you can create another thread object and use an array of pointers to point to the various thread objects, and call the Start() function on the new thread. Once any thread objects are finished, they must be deleted.
  6. Wherever you want to run the thread, you can call the dialog class member function created in the previous step. This function should return relatively quickly unless it is called repeatedly and needs to wait for the previous thread to finish.


The Main Dialog

The TMainDlg class has several command notification functions that open the respective dialog window when it is chosen from the View menu. It also has a timer that is set up in the SetupWindow() function. This timer services the communication ports approximately once per second. All global data, including dialog pointers are declared at the top of the maindlg.cpp file. These global variables are necessary so that data can be communicated between windows. All dialog objects and other global objects are created dynamically in the constructor function of TMainDlg. Notice that only the objects are created here. The actual dialog windows cannot be created until after the main dialog window is created since it is the parent window.


Shutter Control

The constructor function for TShutterDlg dynamically creates the control objects. It then initializes the digital I/O ports by calling NIDAQ Library functions. The shutter init and expose threads are then created. Finally, the shutter is initialized by calling the start function of the shutter init thread.

The destructor function terminates the shutter init and expose threads.

The SetupWindow() function sets up the timer to control the display. It then initializes the default display values.

There are two general purpose functions called ReadPort() and WritePort(), which are used to read and write to a particular port on the HCTL-1100 controller chip. They do this by manipulating the control bits with a particular timing and reading and writing to the data and address registers. See the HCTL-1100 data sheet and shutter controller schematic for more details.

The InitServo() member function initializes the shutter servo by performing a software reset on the HCTL-1100 chip and seeking the reference position. The position counter is then reset and the servo is moved to the home position.

The UpdateDisplay() member function reads the flags and status registers of the HCTL-1100 controller chip and displays the information on the screen. This function is called periodically from the EvTimer() function whenever a WM_TIMER signal is received.

The move() member function moves the shutter to the given position with the given maximum velocity and acceleration.

The Expose() member function performs an exposure by either moving across the aperture at a constant velocity so that all parts of the field of view get equal exposure, or by quickly opening then quickly closing the shutter after the given exposure time expires. The method used depends on the exposure duration. Exposures of duration less than the threshold use the first method, and longer exposures use the second method.

 There are two functions that could take more than a few seconds to execute; Expose() and InitServo(). Since we are operating in an event driven environment, it is a requirement that any events be acted upon in a timely fashion, regardless of what state the system is in. For example, if we are taking a long exposure, we don't want the communications ports to be blocked for the entire exposure time. Therefore, we must use separate threads for functions that may take more than one or two seconds to execute.

The thread classes TShutterExposeThread and TShutterInitThread are defined in the files shutterthread.h and shutterthread.cpp. These objects are dynamically created in the TShutterDlg constructor. The TShutterDlg member functions StartExposure() and StartInitialization() are used to initiate the thread execution. (See "Using Multiple Threads" above.) These functions are called anytime an exposure or initialization is desired.

Software semaphore is used to coordinate access to the HCTL-1100 chip. Only one thread is permitted access to the chip at a time to prevent data from becoming corrupted. The data member shutter_semaphore is used for this purpose. The high-level routines (InitServo and Expose) pend on the semaphore by waiting until shutter_semaphore is zero. Once available, the semaphore is locked by setting shutter_semaphore to one. After all access to the HCTL-1100 is complete, the semaphore is unlocked by setting shutter_semaphore to zero.

Of course, it is possible for a low-level routine to collide with a high level routine in another thread because low level routines are not protected with semaphores. It would be difficult to protect these low-level functions since they are used by high level functions. It was decided to leave out this level of complexity since only the high level functions will be available for normal use. The purpose of the low-level functions is for testing only. They should not be used while multiple threads are executing.

 


Filter Changer

[This section of the software has not yet been completed.]


Tertiary Serial Interface

The Tertiary Serial interface consists of a class hierarchy for the buffered serial communications and a dialog class that contains a text edit class. The class hierarchy of the buffered serial I/O is given here:

 

We will start with a description of the base classes and work our way up to the highest level class. Then, we will discuss the dialog and edit control classes.

TSerialBuf

The TSerialBuf is a general-purpose class that is used to open, send and receive data from a serial port. The constructor takes the serial communications port name as an argument, and opens the serial port with the default parameters (9600, N, 8, 1). The destructor closes the serial port.

The member function SendSerialMessage() takes a null terminated string pointer as an argument, and writes the string to the serial port. The terminating null character is not sent, but a CR/LF combination is sent at the end of the message.

The virtual function TranslateMessage() must be overridden by the derived class.

The TransmitChar() member function accumulates characters in a string buffer until a CR character is transmitted. The function then transmits the message with a terminating CR/LF combination.

The virtual function TransmitResponse() must be overridden by the derived class.

The member function ServicePort() calls the member function SerialMessageReceiver().

SerialMessageReceiver() receives characters from the serial port until the serial port is empty. If a CR character is received, a NULL is appended to the string and the string is translated using the function TranlsateMessage(). LF characters are ignored and BS characters remove the last character from the buffer.

TTerminalBuf

The TTerminalBuf class is used to store multiple lines of characters. The AddLine() member function adds a character string to the buffer. The GetBuf() function returns a pointer to the character buffer. GetNChars() returns the number of characters in the buffer, and GetNLines() returns the number of lines in the buffer.

TTertSerialBuf

The TTertSerialBuf class is derived by multiple inheritance from TSerialBuf and TTerminalBuf.

The constructor for TTertSerialBuf takes the port name and opens the serial port using the inherited TSerialBuf constructor.

The virtual function, TransmitResponse(), inherited from TSerialBuf, is overridden in TTertSerialBuf. This function adds the message to the terminal buffer, redraws the terminal edit control, and transmits the message to the serial port.

The virtual function, TranslateMessage(), inherited from TSerialBuf, is overridden inTTertSerialBuf. It displays the message in the terminal edit control, then relays the message to the TOI via the network interface.

The RedrawTerminalEdit() member function updates the edit control with the contents of the terminal buffer.

TTertiaryEdit

The TTertiaryEdit class is derived from the OWL TEdit class and adds the functionality of intercepting the WM_CHAR message using the EvChar() handler function. Each character is not only displayed in the text edit control, but is also transmitted out the serial port.

TTertiaryDlg

The TTertiaryDlg class controls the display of the serial port buffer. The SetupWindow() member function sets the contents of the text edit to the contents of the serial terminal buffer. This allows the dialog to display previous serial communications even if the dialog is closed and reopened. The TTertiaryDlg class also has member functions for setting the contents of the text edit and setting the text insertion position. These functions are used by the TTertSerialBuf class when servicing the serial port.

 


Network Interface

The class hierarchy that comprises the network interface is complicated enough to warrant an inheritance diagram. These classes are related as follows:

 

We will start with a description of the base classes and work our way up to the highest level class.

TSocketBuf

The base class TSocketBuf contains three virtual functions that must be overridden in the derived class: ServiceSocket(), SendSocMsg(), and TranslateMessage().

The class also has a member function called SocketMessageReceiver(). This function assembles incoming characters into message strings. Characters are received until there are no more characters in the socket. They are combined into a message in the data member MessageBuf[ ] until a CR character is received. LF characters are ignored, and BS characters remove the last character in the buffer. When a complete message is received, the TranslateMessge() member function is called to translate the message.

The member function SocketErrorStr() returns a string containing the last Winsock error. This function can be called if an error is detected to get a string to display an error message. Not all errors are supported, but all error codes from accept(), bind(), closesocket(), connect(), listen(), recv(), send(), and socket() are supported. If an unknown error is detected, the string "Unknown Error" is returned. The returned string is stored as the data member SockErrMsg[ ] so that dynamic allocation and is not necessary.

TServerSocket

The TServerSocket class encapsulates the Winsock implementation to establish and service a server socket connection.

The constructor function takes as an argument the port number, and creates a server socket. It then binds the socket to the port, and listens for connections. The socket uses the I/P protocol, the INET address family, and an address of ANY.

The destructor closes the socket.

The private member function AcceptClient() attempts to accept a client using the Winsock function select(). This function is non-blocking. If successful, it returns the accepted client socket. If no client is available, it returns INVALID_SOCKET. This function is used when servicing the socket.

There are several virtual member functions that must be overridden in the derived class. The virtual member function SendSocMsg() sends a message to a socket. The virtual function ServiceSocket() is called periodically to read from the socket. The virtual function TranslateMessage() translates the incoming message. The virtual function SocketErrorStr() returns a pointer to a string containing the last socket error message.

 TTerminalBuf

The TTerminalBuf class is the same class that is used in the serial communications interface described above.

TTOIServerSocket

The TTOIServerSocket class is derived by multiple inheritance from TServerSocket and TTerminalBuf. Its constructor calls the constructor for TServerSocket to open the socket connection.

The virtual function TranslateMessage() inherited from TServerSocket is overridden in TTOIServerSocket. This function matches command strings with known commands and executes the command. Commands beginning with "TERTIARY" are relayed to the tertiary controller via the serial interface. If a command is not recognized, the response "Error: unrecognized command" is transmitted back to the TOI via the network interface. The command is then added to the terminal buffer and the display is updated.

The TransmitResponse() member function attempts to transmit the given message to the socket. If there is no client accepted, the message is displayed "Error, no client accepted, cannot transmit message".

The RedrawTerminalEdit() member function sets the text edit control to the text in the terminal buffer.

TTOISocketDlg

The TTOISocketDlg class is the dialog class that displays the network communications between the TOI and the Instrument controller. Unlike the TTertiaryDlg, it does not need a special text edit control class since this dialog does not accept keyboard input.

The constructor function creates a new TEdit control to display the network communications.

Like the TTertiaryDlg, the SetupWindow() member function sets the contents of the text edit to the contents of the network terminal buffer. This allows the dialog to display previous network communications even if the dialog is closed and reopened.

The TTOISocketDlg class also has a member function for setting the contents of the text edit. This function is used by the TTOISocketBuf class when servicing the network socket.