// Script to be run from within TheSkyX. // Copyright © 2019 Richard McDonald // Use, modification, and free redistribution permitted. // Rename this file to have a suffix .js, then copy and paste into theSky's // Javascript window. // Take a series of dark frames at a given temperature and during a given // time window. The desired dark frames are specified in a 2-dimensional array and, // since they might not all fit within a typical evening, you then go back and comment-out // completed sets and change the number of frames to be taken in the partially-completed // set. Over several days, you will get the entire dark frame collection. // This script does not set where the frames go - you must set that up with the "AutoSave" // button under your camera control in TheSkyX before running this script // User-changed parameters // *************** Time window for taking frames. *************** // Why delay start when the camera has a shutter? I like to do this for 2 reasons: // 1. Even though the camera has a shutter, I like to take dark frames when // it is actually dark to eliminate the possibility of light leakage. // 2. As the ambient temperature drops the CCD cooler has to work less. Delaying // may make it possible to reach a lower target temperature. // These strings contain an optional date and a time. If no date is included, today is assumed. // For the end time, today is assumed if the end time is later than the start time. // If the end time is earlier than the start time, tomorrow is assumed. // So saying "start 6PM", "end 5AM" works, meaning start at 6PM today, end at 5AM tomorrow morning. // Format "Nov 1, 2019 6:00 PM" or just "6:00 PM". // 24-hour time also works. "Nov 1, 2019 18:00" or "18:00" // Time zone is that used by your running copy of TheSkyX - usually local time. // Does NOT compensate if daylight time starts or ends during your run, so // expect to gain or lose an hour on those 2 evenings. // If the start time is in the past, the run will just start immediately. So if you // don't want to use the time windows, just set Start to a date in the past and set // End to a date in the far future, then leave them that way. var startTimeString = "5:15 PM"; var endTimeString = "5:30 AM"; // *************** CCD Temperature *************** var targetTemperature = -21.0; // cooled CCD temperature set-point (°C) var temperatureWithin = 0.1; // start when temp +/- this much (°C) var risingTempAbortDelta = +1.0; // Abort run if CCD temp rises this much after start // (would indicate rising ambient has overwhelmed the CCD's // cooling capability; could happen in morning.) // Parameters that probably don't need to be changed, but can be. // Typically these are constant for a given setup, but you might need to tune them // for your circumstances. The values here work well for my QSI583 camera and USB2. var sleepParms = { sleepBatchSizeSecs: 30 * 60, // Long sleeps in batches of this many seconds gentleSleepBinning: 3, // Use camera at this binning to waste time gentleSleepDownloadSecs: 8}; // How long does download take at above binning (secs)? var temperatureSettleSecs = 60; // Seconds between temperature checks var coolingTimeoutSecs = 30 * 60; // Max seconds we'll run cooler to get to target temperature var coolingRetryTimes = 5; // Retry cooling this many times (as ambient temp falls, // ability to reach target may improve after a while) var coolingRetryPauseSecs = 30 * 60; // Wait this long before cooling retries // *************** What exposures to take. *************** // Array of arrays // Each element is #frames, exposure length, binning // Comment out lines once you have collected a set so you can use the script again // the following night to keep going. var exposureSets = [ // In the following specs, time of -1 means bias frames // while any other time is a dark frame // #Subs Time Binning //[32, -1, 1], //[32, -1, 2], //[32, -1, 3], //[32, 180, 1], //[32, 180, 2], //[32, 300, 1], //[32, 300, 2], //[18, 600, 1], //[32, .5, 1], //[32, .5, 2], //[32, .5, 3], //[32, 1, 1], //[32, 1, 2], //[32, 1, 3], //[32, 2, 1], //[32, 2, 2], //[32, 2, 3], //[32, 3, 1], //[32, 3, 2], //[32, 3, 3], //[5, 5, 1], //[32, 5, 2], //[32, 5, 3], //[32, 7, 1], //[32, 7, 2], //[32, 7, 3], //[32, 10, 1], //[32, 10, 2], //[32, 10, 3], //[32, 12, 1], //[32, 12, 2], //[32, 12, 3], //[32, 15, 1], //[32, 15, 2], //[32, 15, 3], //[32, 20, 1], //[32, 20, 2], //[32, 20, 3], //[32, 30, 1], //[32, 30, 2], //[32, 30, 3], //[32, 60, 1], //[32, 60, 2], //[32, 60, 3], //[32, 90, 1], //[12, 90, 2], //[32, 90, 3], //[32, 120, 1], //[32, 120, 2], //[32, 120, 3], //[32, 180, 3], //[32, 240, 1], //[32, 240, 2], //[11, 240, 3], //[4, 900, 1], //[2, 1200, 1], //[9, 1500, 1], //[16, 1800, 1], //[32, 300, 3], //[20, 360, 1], //[32, 360, 2], //[32, 360, 3], [12, 600, 2], [32, 600, 3], [32, 900, 2], [32, 900, 3], [32, 1200, 2], [32, 1200, 3], [32, 1500, 2], [32, 1500, 3], [32, 1800, 2], [32, 1800, 3], [0, 0, 0] // Null line so non-comma doesn't require editing ]; // Nothing below here should require change unless you are modifying the code // Version history // 2.2 2019-11-03 Abandon run if temp rises more than a given amount // 2.1 2019-11-02 Add retry loop if cooling can't reach target // 2.0 2019-11-01 Refactor and tidy up // dev June-Oct 2019 // // -------------------------------------------------------------------------------------- // // Main Program // // -------------------------------------------------------------------------------------- // We assume that functions called produce any needed error messages, so we deal here // only with the success path. say("Starting dark-capture script.\n"); var timeWindow = getValidatedTimeWindow(startTimeString, endTimeString); if (timeWindow.valid) { if (setUpCamera()) { if (waitUntilStartTime(timeWindow.startTime, sleepParms)) { if (coolToTargetTemp(targetTemperature, temperatureWithin, coolingTimeoutSecs, temperatureSettleSecs, coolingRetryTimes, coolingRetryPauseSecs, sleepParms)) { if (collectDarkFrames(timeWindow.endTime, exposureSets, targetTemperature + risingTempAbortDelta)) { say("\nSuccessful end of run"); } } ccdsoftCamera.RegulateTemperature = false; } } } // -------------------------------------------------------------------------------------- // // getValidatedTimeWindow // // Validate the given start and end time strings and return the start // and end times as date objects in an object. The returned object // contains the attributes: // valid Boolean, whether the inputs were valid // startTime Date object of start of window // endTime Date object of end of window // // The input string can be a date-time in the form "Nov 1, 2019 6:30 PM" // or a time only, in the form "6:30 PM" // // If the "time-only" form is used, today's date is assumed for the start time. // If the "time-only" form of the end-time is less than the start time, then // that time is assumed to be in the following day. // // -------------------------------------------------------------------------------------- function getValidatedTimeWindow(startTimeString, endTimeString) { //say("getValidatedTimeWindow('" + startTimeString + "', '" + endTimeString + "' entered."); var startTime = 0, endTime = 0; var valid = false; startTime = parseDate(startTimeString); if (startTime) { //say("Good start time: " + startTime); endTime = parseDate(endTimeString); if (endTime) { //say("Good end time: " + endTime); valid = true; if (endTime < startTime) { //say("end time is before start time; add one day to end time"); var oneDayInMs = 24 // Hours * 60 // 60 minutes per hour * 60 // 60 seconds per minute * 1000; // Milliseconds endTime = new Date(endTime.getTime() + oneDayInMs); //say("Adjusted date: " + endTime); } } } return {valid:valid, startTime: startTime, endTime: endTime}; } // -------------------------------------------------------------------------------------- // // parseDate // // Parse a single date/time string, returning a date object. // If it won't parse, it's possible that it was given as only a time, so // try parsing it as a time with a fake date, then move that time to today's date. // // Return 0 if not valid // // -------------------------------------------------------------------------------------- function parseDate(inputString) { var date = 0; //say("parseDate(" + inputString + ") entered"); date = new Date(inputString); // Try initial parse if (!isNaN(date)) { //say("Initial parse worked: " + date); } else { //say("Parse failed. Check if it's a time-only string"); var fakeDateString = "January 1, 2019 " + inputString; var fakeDate = new Date(fakeDateString); //say("Trying fake date string '" + fakeDateString + "'."); if (isNaN(fakeDate)) { say("Invalid date string: '" + inputString); date = 0; } else { //say("Valid time given. Transfer to today's date." + fakeDate); date = new Date(); // Gets date and time right now date.setHours(fakeDate.getHours()); date.setMinutes(fakeDate.getMinutes()); date.setSeconds(fakeDate.getSeconds()); //say("Result: " + date); } } return date; } // -------------------------------------------------------------------------------------- // // setUpCamera // // Set basic camera parameters and connect to camera. Return success indicator. // // -------------------------------------------------------------------------------------- function setUpCamera() { ccdsoftCamera.Autoguider = false; // Main camera ccdsoftCamera.Asynchronous = false; // Always wait for camera ccdsoftCamera.Frame = 3; // Set for dark frames (cdDark = 3) ccdsoftCamera.ImageReduction = 0; // No image reduction ccdsoftCamera.ccdsoftAutoSaveAs = 0; // Autosave file to disk cdFITS = 0 var connectTimeout = 30; // Seconds to connect to camera ccdsoftCamera.Disconnect(); var wait_time = 0; var cameraCode = 0; try { cameraCode = ccdsoftCamera.Connect(); while((cameraCode != 0) && (wait_time < connectTimeout)) { sky6Web.Sleep(500); wait_time += 0.5; cameraCode = ccdsoftCamera.Connect(); } } catch(err) { say("Error connecting to camera: " + err.message); cameraCode = -1; } var result = (cameraCode == 0); if (!result) { say ("Unable to connect to camera."); } return result; } // -------------------------------------------------------------------------------------- // // waitUntilStartTime // // Wait until the specified start time arrives. Use "gentle sleep" in // batches to pass the time until then. // // -------------------------------------------------------------------------------------- function waitUntilStartTime(startTime, sleepParms) { //say("waitUntilStartTime(" + startTime + ") entered"); var now = new Date(); var waitInMs = startTime.getTime() - now.getTime(); var success = true; //say("Wait " + waitInMs + " milliseconds"); if (waitInMs <= 0) { say("No start delay necessary"); } else { say("To reach specified start time, delay for approximately " + formatInterval(waitInMs/1000)); success = sleepInBatchesUntil(startTime, sleepParms); } return success; } // -------------------------------------------------------------------------------------- // // coolToTargetTemp // // Turn on CCD cooling and wait (a given maximum amount of time) until the // CCD temperature reaches its target, within a given tolerance. // Return a success indicator. (Fail if time limit is exceeded.) // // -------------------------------------------------------------------------------------- function coolToTargetTemp( targetTemperature, temperatureWithin, coolingTimeoutSecs, temperatureSettleSecs, coolingRetryTimes, coolingRetryPauseSecs, sleepParms) { //say("coolToTargetTemp(" + targetTemperature + ", " // + temperatureWithin + ", " + coolingTimeoutSecs + ", " // + temperatureSettleSecs + ", " // + coolingRetryTimes + ", " // + coolingRetryPauseSecs + ", " // + sleepParms + ")"); var success = true; say("Cooling to target temperature of " + targetTemperature); // Try to reach cooling target once, plus "retry" extra times var coolingTryTimes = coolingRetryTimes + 1; while (success & (coolingTryTimes > 0)) { coolingTryTimes -= 1; // Turn on temperature regulation to specified temperature ccdsoftCamera.TemperatureSetPoint = targetTemperature; ccdsoftCamera.RegulateTemperature = true; // Cause it to notice changed target temp // Wait for camera to reach specified temperature, testing at given interval, until within // given range of target var waitTime = 0; var difference = Math.abs(ccdsoftCamera.Temperature - targetTemperature); //say(" difference = " + difference); while (success && (difference > temperatureWithin) && (waitTime < coolingTimeoutSecs)) { success = gentleSleep(temperatureSettleSecs, sleepParms.gentleSleepBinning, sleepParms.gentleSleepDownloadSecs); if (success) { say(" Camera temperature " + ccdsoftCamera.Temperature + ", power " + ccdsoftCamera.ThermalElectricCoolerPower + "%, at " + new Date().toLocaleTimeString()); waitTime += temperatureSettleSecs; difference = Math.abs(ccdsoftCamera.Temperature - targetTemperature); //say(" difference = " + difference); } } if (waitTime >= coolingTimeoutSecs) { RunJavaScriptOutput.writeLine(" Timed out waiting for temperature to stabilize."); ccdsoftCamera.RegulateTemperature = false; // Give cooler a rest if (coolingTryTimes > 0) { say(" Waiting " + formatInterval(coolingRetryPauseSecs) + " for ambient temp to drop."); success = sleepInBatches(coolingRetryPauseSecs, sleepParms); say(" Trying cooling again.") } else { say(" Maximum attempts to reach desired target temperature reached, giving up."); success = false; } } } if (success) { say("Camera has cooled to temperature " + ccdsoftCamera.Temperature + ", cooler power now " + ccdsoftCamera.ThermalElectricCoolerPower + "%, at " + new Date().toLocaleTimeString()); } return success; } // -------------------------------------------------------------------------------------- // // collectDarkFrames // // Collect the given sets of dark frames until either the entire set is captured // or the given end-time is reached. Return a success indicator. Set completion // and reaching the end time are both considered success - only an error from the // camera module causes a failure. // // -------------------------------------------------------------------------------------- function collectDarkFrames( endTime, exposureSets, risingTempAbortAt) { //say("collectDarkFrames(" + endTime + ", " + exposureSets + ")"); var success = true; // Loop through all the desired times. // Before each frame, calculate what time the exposure will finish // If it would finish after the requested session end time, don't do it; we are done. var thisSet; var numFrames; var exposureTime; var binning; var withinTime = true; var nowInMs; var frameWouldFinish; var endTimeAsDate = new Date(endTime); for (i = 0; (i < exposureSets.length) && withinTime && success; i += 1) { thisSet = exposureSets[i]; numFrames = thisSet[0]; exposureTime = thisSet[1]; binning = thisSet[2]; ccdsoftCamera.BinX = binning; ccdsoftCamera.BinY = binning; // Dark or bias frames? if (exposureTime == -1) { ccdsoftCamera.Frame = 2; // Set for bias frames (cdbias = 2) ccdsoftCamera.ExposureTime = 0.1; // Exposure time ignored for bias frames say("Expose " + numFrames + " bias frames, binned " + binning + " x " + binning); } else { ccdsoftCamera.Frame = 3; // Set for dark frames (cdDark = 3) ccdsoftCamera.ExposureTime = exposureTime; // Set camera exposure time say("Expose " + numFrames + " dark frames for " + exposureTime + " seconds, binned " + binning + " x " + binning); } // Take specified number of frames - or until time window exceeded for (frameCount = 0; (frameCount < numFrames) && withinTime && success; frameCount += 1) { nowInMs = new Date().getTime(); frameWouldFinish = nowInMs + exposureTime*1000; finishForecastAsDate = new Date(frameWouldFinish); //say("now: " + nowInMs // + ", wouldFinish: " + finishForecastAsDate // + ", endTime: " + endTimeAsDate); if (frameWouldFinish < endTime) { //say("Frame will end at " + finishForecastAsDate + ", within allowed time " // + endTimeAsDate + ", proceed"); say(" " + (frameCount+1) + " of " + numFrames + ", " + exposureTime + " seconds, binned " + binning + " x " + binning + " starts at " + new Date().toLocaleTimeString()); try { var cameraResult = ccdsoftCamera.TakeImage(); } catch (err) { say("Camera error: " + err.message); success = false; } // Has temperature risen above acceptable limit? // (Might happen in morning before the run ends - temp is rising anyway, // and the computer and camera are throwing some heat) if (ccdsoftCamera.Temperature > risingTempAbortAt) { say("Rising temperature, " + ccdsoftCamera.Temperature + " is above cut-off of " + risingTempAbortAt + ". Ending run."); success = false; } } else { //say("Frame would end at " + finishForecastAsDate // + " and exceed allowed time " + endTimeAsDate + ", ending now."); withinTime = false; } } } return success; } // -------------------------------------------------------------------------------------- // // say // // Concise access to the console output javascript function in theSkyX // // -------------------------------------------------------------------------------------- function say(message) { RunJavaScriptOutput.writeLine(message); } // -------------------------------------------------------------------------------------- // // sleepInBatchesUntil // // Use the camera to sleep until a given time (discard the images) // Rather than a potentially absurdly-long exposure, sleep in // batches of a given length. We re-calculate the delay after each batch, // so that we are adjusting to inaccuracies in the camera download time // // sleepParms object contains the relevant sleeping parameters // // -------------------------------------------------------------------------------------- function sleepInBatchesUntil(sleepUntilTime, sleepParms) { var success = true; //say("sleepInBatchesUntil(" + sleepUntilTime + ") entered"); //say("SleepParms: " + sleepParms.sleepBatchSizeSecs + ", " // + sleepParms.gentleSleepBinning + ", " + sleepParms.gentleSleepDownloadSecs); var now = new Date(); var secondsToSleep = (sleepUntilTime.getTime() - now.getTime()) / 1000; //say(" secondsToSleep = " + secondsToSleep); if (secondsToSleep > 0) { while ((secondsToSleep > sleepParms.sleepBatchSizeSecs) && success) { say(" Sleep for " + formatInterval(sleepParms.sleepBatchSizeSecs) + " starting at " + new Date().toLocaleTimeString()); if (gentleSleep(sleepParms.sleepBatchSizeSecs, sleepParms.gentleSleepBinning, sleepParms.gentleSleepDownloadSecs)) { now = new Date(); // Recalculate time remaining each time in case gentleSleep doesn't produce // a delay of precisely the requested length (it might be off by a few // seconds because of variability in the camera download time) secondsToSleep = (sleepUntilTime.getTime() - now.getTime()) / 1000; } else { success = false; } } now = new Date(); secondsToSleep = (sleepUntilTime.getTime() - now.getTime()) / 1000; if ((secondsToSleep > 0) && success) { say(" Sleep for " + formatInterval(secondsToSleep) + " starting at " + new Date().toLocaleTimeString()); success = gentleSleep(secondsToSleep, sleepParms.gentleSleepBinning, sleepParms.gentleSleepDownloadSecs); } } //say("sleepInBatchesUntil returns " + success); return success; } // -------------------------------------------------------------------------------------- // // sleepInBatches // // Use the camera to sleep for a given amount of time (discard the images) // Rather than a potentially absurdly-long exposure, sleep in // batches of a given length // // sleepParms object contains the relevant sleeping parameters // // -------------------------------------------------------------------------------------- function sleepInBatches(secondsToSleep, sleepParms) { var success = true; //say("sleepInBatches(" + secondsToSleep + ") entered"); //say("SleepParms: " + sleepParms.sleepBatchSizeSecs + ", " // + sleepParms.gentleSleepBinning + ", " + sleepParms.gentleSleepDownloadSecs); if (secondsToSleep > 0) { var timeRemaining = secondsToSleep; while ((timeRemaining > sleepParms.sleepBatchSizeSecs) && success) { say(" Sleep for " + formatInterval(sleepParms.sleepBatchSizeSecs) + " starting at " + new Date().toLocaleTimeString()); if (gentleSleep(sleepParms.sleepBatchSizeSecs, sleepParms.gentleSleepBinning, sleepParms.gentleSleepDownloadSecs)) { timeRemaining -= sleepParms.sleepBatchSizeSecs; } else { success = false; } } if ((timeRemaining > 0) && success) { say(" Sleep for " + formatInterval(timeRemaining) + " starting at " + new Date().toLocaleTimeString()); success = gentleSleep(timeRemaining, sleepParms.gentleSleepBinning, sleepParms.gentleSleepDownloadSecs); } } //say("sleepInBatches returns " + success); return success; } // -------------------------------------------------------------------------------------- // // formatInterval (seconds) // // Given a number of seconds, make up a nice, grammatical string version. // "second" or "seconds" depending on plural. // Add minutes and hours for longer intervals. // // -------------------------------------------------------------------------------------- function formatInterval(seconds) { var hoursString = ""; var minutesString = ""; var secondsString = ""; var secondsInHour = 60*60; //say("formatInterval(" + seconds + " entered)"); if (seconds > secondsInHour) { var hours = Math.floor(seconds / secondsInHour); seconds -= hours*secondsInHour; //say(hours + " hours, leaving " + seconds + " seconds"); hoursString = hours + " hour" + ((hours > 1) ? "s" : ""); } if (seconds > 60) { var minutes = Math.floor(seconds / 60); seconds -= minutes*60; //say(minutes + " minutes, leaving " + seconds + " seconds"); minutesString = minutes + " minute" + ((minutes > 1) ? "s" : ""); } seconds = Math.round(seconds); if (seconds > 0) { //say(seconds + " remain"); secondsString = seconds + " second" + ((seconds > 1) ? "s" : ""); } var result = hoursString; if (minutesString != "") { result += (result != "" ? ", " : "") + minutesString; } if (secondsString != "") { result += (result != "" ? ", " : "") + secondsString; } //say("formatInterval result: " + result); return result; } // -------------------------------------------------------------------------------------- // // gentleSleep (seconds, binning, downloadTime) // // "Sleep" for the given number of seconds. // Since this environment doesn't have a Sleep command (except sky6Web.Sleep, // which is highly disruptive), we will use the camera, taking an image of the // desired duration then discarding it. So, obviously, we can't use this as a // sleep while the camera is already running asynchronously // // -------------------------------------------------------------------------------------- function gentleSleep (seconds, binning, downloadTime) { //say("gentleSleep(" + seconds + ") entered at " + new Date().toLocaleTimeString()); // We're going to use the camera to cause a delay. // We assume the camera is already connected and set to Synchronous // Set aside the "autosave" setting, and turn it off so we don't create an image file. // Also save the binning, frame type, exposure, and whether a new window shows image var saveAutoSaveSetting = ccdsoftCamera.AutoSaveOn; var saveXBin = ccdsoftCamera.BinX; var saveYBin = ccdsoftCamera.BinX; var saveFrameType = ccdsoftCamera.Frame; var saveExposure = ccdsoftCamera.ExposureTime; var saveToNewWindow = ccdsoftCamera.ToNewWindow; var success = true; ccdsoftCamera.AutoSaveOn = false; ccdsoftCamera.BinX = binning; ccdsoftCamera.BinY = binning; ccdsoftCamera.Frame = 3; ccdsoftCamera.ToNewWindow = false; // Take an exposure, binned 3x3 to minimize download time, for the desired // delay. Take away an estimate of how long the download will TakeImage var secondsMinusDownload = seconds - downloadTime; if (secondsMinusDownload < 0) { secondsMinusDownload = 0.1; } //say("Using exposure of " + secondsMinusDownload + " seconds to allow for download."); // Take a dark frame of this length ccdsoftCamera.ExposureTime = secondsMinusDownload; try { var cameraResult = ccdsoftCamera.TakeImage(); } catch (err) { say("Camera error " + err + " ignored."); success = false; // Ignore any error - we only wanted the delay this takes } finally { // Reset the saved values ccdsoftCamera.AutoSaveOn = saveAutoSaveSetting; ccdsoftCamera.BinX = saveXBin; ccdsoftCamera.BinY = saveYBin; ccdsoftCamera.Frame = saveFrameType; ccdsoftCamera.ExposureTime = saveExposure; ccdsoftCamera.ToNewWindow = saveToNewWindow; //say("gentleSleep(" + seconds + ") exits at " + new Date().toLocaleTimeString()); } //say("gentleSleep returns " + success); return success; }