Saturday, June 18, 2011

Automated Astrophotography with Python - Part 5

LOGGING TO A TEXT FILE
In previous posts all status messages have been sent only to the standard output (screen) and disappear when Python is closed. In this post I'll describe how to direct status messages to also appear in a permanent log file. In order to accomplish this, it is necessary to generate a new class named 'cLogger'. The listing for the 'cLogger' class is show below:
import time

##----------------------------------------------------------------------------
## Class: cLogger
##----------------------------------------------------------------------------
class cLogger:
    def __init__(self,filename):
        self.__filename = filename
        self.__level = 0
        self.__echo = True

    def log(self,level,value,echo=True):
        self.__level = level
        self.__echo = echo
        
        # echo to terminal with leading space if echo = True (default)
        if self.__echo:
            print " " + value
        # build the filename
        iDateTime = time.localtime()
        sDateStr = time.strftime('_%Y%m%d.log', iDateTime)
        # prepend spaces depending on log level (each additional level has two
        # spaces before text, for indenting)
        for i in range(level):
            value = "   " + value
        # build the full screen line including timestamp
        prefix = time.strftime("%H:%M:%S ", time.localtime(time.time()))
        if value.endswith("\n"):
            value = value[:-1]
        value = prefix + value
        # write all entries to detailed log file
        try:
            f = open (self.__filename[:-4] + sDateStr, "a")
            f.write(value)
            f.write("\n")
            f.close()
        except:
            print "Log: UNABLE TO WRITE TO LOG FILE:"
            print "Filename:", self.__filename[:-4] + sDateStr
            print "Log line:", value        

    def blank(self,echo=True):
        self.__echo = echo
        if self.__echo:
            print " "
        # build the filename
        iDateTime = time.localtime()
        sDateStr = time.strftime('_%Y%m%d.log', iDateTime)
        # write all entries to detailed log file
        try:
            f = open (self.__filename[:-4] + sDateStr, "a")
            f.write("\n")
            f.close()
        except:
            print "Log: UNABLE TO WRITE TO LOG FILE:"
            print "Filename:", self.__filename[:-4] + sDateStr

    def logHeader(self,strTitle):
        self.log(0,"")
        strVal = strTitle + " Log File"
        self.log(0,strVal,False)
        self.log(0,"",False)
        self.log(0,"*** START OF SCRIPT ***")

    def logFooter(self):
        self.log(0,"Script complete!")
        self.log(0,"*** END OF SCRIPT ***")
        self.log(0,"")

##
## END OF 'cLogger' Class
##
The constructor for 'cClass' is a short method that initializes class attributes. (Note: when an instance of the class is created, a complete path and filename must be provided to the class.) The 'log()' method is the routine that writes a status message to the logfile. The contents of the argument 'value' is the string text message that is logged. The 'level' argument determines the indentation level of the message and 'echo' determines whether the status message is also sent to the standard output (screen). The filename that is passed into the class is used as the base of the actual log filename. After the dot and three letter extension (ex. '.log') is removed, the remainder of the filename is appended with the year, month, day, and '.log' extension. Also, the text status message is prefixed with the local hour, minute, and second. The new filename is then opened in 'append' mode and the prefixed text message is written to the file with a carriage return character added to the end of the line. The file is then immediately closed to prevent inadvertent writes.

The 'blank()' method serves only to write a blank line (with no time-stamp on the line) to the log file. This method is used to separate logging of individual runs of a script. The final two methods, 'logHeader()' and 'logFooter()', write out pre-defined header and footer messages to the log file.

The unit test for this class demonstrates use of all methods in the 'cLogger' class and is shown below:
if __name__ == '__main__':

    # set up path and name of the logfile
    cLoggerLogFile = r"C:\Imaging_Scripts\logfiles\clogger.log"

    # instantiate the class with the path and name
    printer = cLogger(cLoggerLogFile)
    
    # print to logfile and to screen
    printer.logHeader('Unit Test for cLogger Class')
    printer.log(0,"Line 1 of status: no indentation")
    printer.log(1,"Line 2 of status: 2 spaces of indentation")
    printer.log(2,"Line 3 of status: 4 spaces of indentation")
    printer.log(3,"Line 4 of status: 6 spaces of indentation")
    # print to logfile only
    printer.log(2,"Line 5 of status: 4 spaces of indentation",False)
    # continue printing to logfile and to screen
    printer.log(1,"Line 6 of status: 2 spaces of indentation")
    printer.log(0,"Line 7 of status: no indentation")
    printer.logFooter()
    printer.blank()
LOGGING FROM WITHIN CLASSES
In order for status messages generated within classes to be logged, it is necessary to modify those classes to include calls to 'cLogger'. For example, shown below is the listing of affected methods in the 'cMount' class. The major change is that a 'cLogger' object is now passed as an argument into the methods that contain status message print statements. Then, in those arguments, the 'print' statement is changed to call the 'log()' method of the passed-in 'cLogger' object. Note that any messages that you wish to only show up on the screen should be left as normal Python 'print' statements. (Changes necessary to implement logging is shown in light cyan text.)
##------------------------------------------------------------------------------
## Class: cMount
##------------------------------------------------------------------------------
class cMount:
    def __init__(self,printlog):
        self.__info = {}
        self.__objectName = ''
        printlog.log(0,"Connecting to The Sky6...")
        self.__SKYCHART = win32com.client.Dispatch("TheSky6.StarChart")
        self.__SKYINFO = win32com.client.Dispatch("TheSky6.ObjectInformation")
        self.__UTIL = win32com.client.Dispatch("TheSky6.Utils")
        self.__MOUNT = win32com.client.Dispatch("TheSky6.RASCOMTele")

    def slewToObject(self,obj,printlog,delay=2.0):
        if not self.findObject(obj):
            coords = self.getCoordinates()
            printlog.log(0,"Slewing telescope to %s..." % obj)
            JnowRA = self.__UTIL.ConvertAngleToDMS(coords['RA Now'])
            printlog.log(0,"JNow RA : %02dh %02dm %0.2fs" % (JnowRA[0],
                                                             JnowRA[1],
                                                             JnowRA[2]))
            JnowDEC = self.__UTIL.ConvertAngleToDMS(coords['DEC Now'])
            printlog.log(0,"JNow DEC: %02d deg %02d' %0.2f\"" % (JnowDEC[0],
                                                                 JnowDEC[1],
                                                                 JnowDEC[2]))
            try:
                self.__MOUNT.SlewToRaDec(coords['RA Now'],coords['DEC Now'],obj)
            except:
                printlog.log(0,"ERROR: During slew to Object")
                return ERROR
            else:
                printlog.log(0,"Done slewing!")
                # delay (default = 2.0 seconds)
                time.sleep(delay)
                return NOERROR
        else:
            printlog.log(0,"%s could not be found." % obj)
            return ERROR

    def slewToAzAlt(self,azimuth,altitude,name,printlog,delay=2.0):
        printlog.log(0,"Slewing to Azimuth: %0.1f and Altitude: %0.1f..." % \
                     (azimuth,altitude))
        try:
            self.__MOUNT.SlewToAzAlt(azimuth,altitude,name)
        except:
            printlog.log(0,"ERROR: During slew to altitude/azimuth position")
            return ERROR
        else:
            printlog.log(0,"Done slewing!")
            # delay (default = 2.0 seconds)
            time.sleep(delay)
            return NOERROR
Additionally, the unit test for 'cMount' must be modified to implement the data logging feature. Here's the listing for the 'cMount' class unit test:
if __name__ == "__main__":
    
    import cLogger_5

    # set up path and name of the logfile
    cMountLogFile = r"C:\Imaging_Scripts\logfiles\cmount.log"

    # instantiate the class with the path and name
    printer = cLogger_5.cLogger(cMountLogFile)
    printer.logHeader('Unit Test for cMount Class')

    # create an instance of the cMount object
    testMount = cMount(printer)

    # prompt for name of object to locate
    print
    obj = raw_input(" Enter first object to slew to: ")
    # test slewToObject()
    testMount.slewToObject(obj,printer)

    # prompt for name of object to locate
    print
    obj = raw_input(" Enter second object to slew to: ")
    # test slewToObject()
    testMount.slewToObject(obj,printer)

    # prompt for name of object for sync
    print
    obj = raw_input(" Enter object to sync to: ")
    if not testMount.findObject(obj):
        coords = testMount.getCoordinates()
        # test syncToObject()
        printer.log(0,"Syncing control system to %s" % obj)
        testMount.syncToObject(coords['RA Now'],coords['DEC Now'],obj)
    else:
        printer.log(0,"%s could not be found." % obj)

    # prompt for azimuth and altitude for slew
    print
    azimuth = raw_input(" Enter azimuth for slew: ")
    azimuth = float(azimuth)
    altitude = raw_input(" Enter altitude for slew: ")
    altitude = float(altitude)

    # test slewToAzAlt()
    testMount.slewToAzAlt(azimuth,altitude,"park",printer)

    printer.logFooter()
    printer.blank()
In the unit test shown above, after importing the 'cLogger' module, an instance of the class named 'printer' is created. This object is then used in calls to methods of the 'cMount' class and to generate status messages for the unit test itself. The class listing and unit test for the 'cCamera' class has been similarly modified and included in the download files for this chapter.

TESTING THE CLASSES
Like in previous posts, before testing classes in this section, the Sky6 simulator must be manually connected to the telescope control system using the usual procedure. Also, the imaging and guide cameras in MaxIm DL should be first connected to the simulated cameras. The 'cLogger', 'cMount', and 'cCamera' classes can then be executed from the IDE in the normal manner. The source files described in this chapter can be downloaded from chapter_5.zip. Execute each unit test individually then inspect the generated log files to see that they are written as expected. Next, run each unit test again to verify that the log file is appended with new status messages for each run of the unit test.

WHAT'S NEXT?
In the next post, I will present a new class that implements plate solving using the Sky6 and CCDSoft. The class and unit test will also include a provision to perform a simulated test of the plate solve operation.

CLICK HERE FOR NEXT POST IN THIS SERIES

No comments:

Post a Comment