/** * @fileOverview This file has functions related to documenting JavaScript. * @author Thomas Bretz */ 'use strict'; dim.log("Start: "+__FILE__+" ["+__DATE__+"]"); //dimctrl.defineState(37, "TimeOutBeforeTakingData", "MCP took more than 5minutes to start TakingData"); // ================================================================ // Code related to the schedule // ================================================================ //this is just the class implementation of 'Observation' include('scripts/Observation_class.js'); // this file just contains the definition of // the variable observations, which builds our nightly schedule, hence the filename include('scripts/schedule.js'); // make Observation objects from user input and check if 'date' is increasing. for (var i=0; i0 && observations[i].start <= observations[i-1].start) { throw new Error("Start time '"+ observations[i].start.toUTCString()+ "' in row "+i+" exceeds start time in row "+(i-1)); } } // Get the observation scheduled for 'now' from the table and // return its index function getObservation(now) { if (now==undefined) now = new Date(); if (isNaN(now.valueOf())) throw new Error("Date argument in getObservation invalid."); for (var i=0; i10) return; dim.send("MCP/RESET"); reset = reset.filter(function(elem,pos){return reset.indexOf(elem)==pos;}); if (reset.length>0) { console.out(""); console.out(" FADs belong to crate(s): "+reset); console.out(""); } this.last_time = evt.time; this.last_inc = inc; } var sub_connections = new Subscription("FAD_CONTROL/CONNECTIONS"); /** * call-back function of FAD_CONTROL/CONNECTIONS * store IDs of problematic FADs * */ /* sub_connections.onchange = function(evt) { // This happens, but why? if (!evt.obj['status']) return; this.reset = [ ]; for (var x=0; x<40; x++) if (evt.obj['status'][x]!=66 && evt.obj['status'][x]!=67) this.reset.push(x); if (this.reset.length==0) return; //m.alarm("FAD board loss detected..."); dim.send("MCP/RESET"); dim.send("FAD_CONTROL/CLOSE_OPEN_FILES"); } */ /** * reconnect to problematic FADs * * Dis- and Reconnects to FADs, found to be problematic by call-back function * onchange() to have a different CONNECTION value than 66 or 67. * * @returns * a boolean is returned. * reconnect returns true if: * * nothing needed to be reset --> no problems found by onchange() * * the reconnection went fine. * * reconnect *never returns false* so far. * * @example * if (!sub_connections.reconnect()) * exit(); */ sub_connections.reconnect = function() { // this.reset is a list containing the IDs of FADs, // which have neither CONNECTION==66 nor ==67, whatever this means :-) if (this.reset.length==0) return true; console.out(" Reconnect: start ["+this.reset.length+"]"); for (var i=0; i this time should be pretty identical for each run // if this takes longer than say 3s: // there might be a problem with one/more FADs // // wait until "TakingData": // --> this seems to take even some minutes sometimes... // (might be optimized rather soon, but still in the moment...) // if this takes way too long: // there might be something broken, // so maybe a very high time limit is ok here. // I think there is not much that can go wrong, // when the Thr-Calib has already started. Still it might be nice // If in the future RateControl is written so to find out that // in case the threshold finding algorithm does // *not converge as usual* // it can complain, and in this way give a hint, that the weather // might be a little bit too bad. // else: // wait until "TakingData": // --> in a non-data run this time should be pretty short again // if this takes longer than say 3s: // there might be a problem with one/more FADs // // Use this if you use the rate control to calibrate by rates //if (!dim.wait("MCP", "TakingData", -300000) ) //{ // throw new Error("MCP took longer than 5 minutes to start TakingData"+ // "maybe this idicates a problem with one of the FADs?"); //} // Here we could check and handle fad losses try { dim.wait("MCP", "TakingData", 15000); } catch (e) { console.out(""); console.out("MCP: "+dim.state("MCP").name); console.out("FAD_CONTROL: "+dim.state("FAD_CONTROL").name); console.out("FTM_CONTROL: "+dim.state("FTM_CONTROL").name); console.out(""); if (dim.state("MCP").name!="Configuring3" || dim.state("FAD_CONTROL").name!="Configuring2") throw e; console.out(""); console.out("Waiting for fadctrl to get configured timed out... checking for in-run FAD loss."); var con = sub_connections.get(); var stat = con.obj['status']; console.out("Sending MCP/RESET"); dim.send("MCP/RESET"); dim.wait("FTM_CONTROL", "Idle", 3000); dim.wait("FAD_CONTROL", "Connected", 3000); dim.wait("MCP", "Idle", 3000); var reset = [ ]; for (var i=0; i<40; i++) if (stat[i]!=0x43) { console.out(" FAD %2d".$(i)+" not in Configured state."); reset.push(parseInt(i/10)); } reset = reset.filter(function(elem,pos){return reset.indexOf(elem)==pos;}); if (reset.length>0) { console.out(""); console.out(" FADs belong to crate(s): "+reset); console.out(""); } console.out(""); console.out("Trying automatic reconnect..."); for (var i=0; i<40; i++) if (stat[i]!=0x43) { console.out(" ...disconnect ", i); dim.send("FAD_CONTROL/DISCONNECT", i); } console.out(" ...waiting for 5s"); v8.sleep(5000); for (var i=0; i<40; i++) if (stat[i]!=0x43) { console.out(" ...reconnect ", i); dim.send("FAD_CONTROL/CONNECT", i); } console.out(" ...waiting for 1s"); v8.sleep(1000); console.out(""); throw e; } dim.wait("MCP", "Idle", time>0 ? time*1250 : undefined); // run time plus 25% if (incomplete) { console.out(""); console.out("Trying automatic reconnect..."); for (var i=0; i<40; i++) if (incomplete&(1<this.cnt+12) return; this.voltageStep = null; if (!evt.obj) return; var delta = evt.obj['DeltaBias']; var avg = 0; for (var i=0; i<320; i++) avg += delta[i]; avg /= 320; if (this.previous) this.voltageStep = Math.abs(avg-this.previous); this.previous = avg; console.out(" DeltaV="+this.voltageStep); } // DN: Why is voltageOff() implemented as // a method of a Subscription to a specific Service // I naively would think of voltageOff() as an unbound function. // I seems to me it has to be a method of a Subscription object, in order // to use the update counting method. But does it have to be // a Subscription to FEEDBACK/DEVIATION, or could it work with other services as well? service_feedback.voltageOff = function() { var state = dim.state("BIAS_CONTROL").name; // check of feedback has to be switched on var isOn = state=="VoltageOn" || state=="Ramping"; if (isOn) { console.out(" Voltage off: start"); // Supress the possibility that the bias control is // ramping and will reject the command to switch the // voltage off var isControl = dim.state("FEEDBACK").name=="CurrentControl"; if (isControl) { console.out(" Suspending feedback."); dim.send("FEEDBACK/ENABLE_OUTPUT", false); dim.wait("FEEDBACK", "CurrentCtrlIdle", 3000); } // Switch voltage off console.out(" Voltage on: switch off"); dim.send("BIAS_CONTROL/SET_ZERO_VOLTAGE"); // If the feedback was enabled, re-enable it if (isControl) { console.out(" Resuming feedback."); dim.send("FEEDBACK/ENABLE_OUTPUT", true); dim.wait("FEEDBACK", "CurrentControl", 3000); } } dim.wait("BIAS_CONTROL", "VoltageOff", 5000); // FEEDBACK stays in CurrentCtrl when Voltage is off but output enabled // dim.wait("FEEDBACK", "CurrentCtrlIdle", 1000); if (isOn) console.out(" Voltage off: end"); } // DN: The name of the method voltageOn() in the context of the method // voltageOff() is a little bit misleading, since when voltageOff() returns // the caller can be sure the voltage is off, but when voltageOn() return // this is not the case, in the sense, that the caller can now take data. // instead the caller of voltageOn() *must* call waitForVoltageOn() afterwards // in order to safely take good-quality data. // This could lead to nasty bugs in the sense, that the second call might // be forgotten by somebody // // so I suggest to rename voltageOn() --> prepareVoltageOn() // waitForVoltageOn() stays as it is // and one creates a third method called:voltageOn() like this /* service_feedback.voltageOn = function() * { * this.prepareVoltageOn(); * this.waitForVoltageOn(); * } * * */ // For convenience. service_feedback.voltageOn = function() { //if (Sun.horizon("FACT").isUp) // throw new Error("Sun is above FACT-horizon, voltage cannot be switched on."); var isOff = dim.state("BIAS_CONTROL").name=="VoltageOff"; if (isOff) { console.out(" Voltage on: switch on"); //console.out(JSON.stringify(dim.state("BIAS_CONTROL"))); dim.send("BIAS_CONTROL/SET_GLOBAL_DAC", 1); } // Wait until voltage on dim.wait("BIAS_CONTROL", "VoltageOn", 5000); // From now on the feedback waits for a valid report from the FSC // and than switchs to CurrentControl dim.wait("FEEDBACK", "CurrentControl", 60000); if (isOff) { console.out(" Voltage on: cnt="+this.cnt); this.previous = undefined; this.cnt = this.get().counter; this.voltageStep = undefined; } } service_feedback.waitForVoltageOn = function() { // waiting 45sec for the current control to stabilize... // v8.sleep(45000); // ----- Wait for at least three updates ----- // The feedback is started as if the camera where at 0deg // Then after the first temp update, the temperature will be set to the // correct value (this has already happened) // So we only have to wait for the current to get stable. // This should happen after three to five current updates. // So we want one recent temperature update // and three recent current updates // Avoid output if condition is already fulfilled if (this.cnt && this.get().counter>this.cnt+10) return; // FIXME: timeout missing console.out(" Feedback wait: start"); function func(service) { if ((service.cnt!=undefined && service.get().counter>service.cnt+10) || (service.voltageStep && service.voltageStep<0.02)) return true; } var now = new Date(); //v8.timeout(5*60000, func, this); while ((this.cnt==undefined || this.get().counter<=this.cnt+10) && (!this.voltageStep || this.voltageStep>0.02)) v8.sleep(); console.out(" Feedback wait: end [dV=%.3f, cnt=%d, %.2fs]".$(this.voltageStep, this.get().counter, (new Date()-now)/1000)); } // ================================================================ // Function to shutdown the system // ================================================================ function Shutdown() { /**** dummy ****/ //console.out(" => [SHUTDOWN] called."); console.out("Shutdown: start"); dim.send("FEEDBACK/STOP"); dim.send("FEEDBACK/ENABLE_OUTPUT", false); dim.send("FTM_CONTROL/STOP_TRIGGER"); service_feedback.voltageOff(); CloseLid(); dim.send("DRIVE_CONTROL/PARK"); console.out("Waiting for telescope to park. This may take a while."); // FIXME: This might not work is the drive is already close to park position dim.wait("DRIVE_CONTROL", "Locked", 3000); //dim.wait("DRIVE_CONTROL", "Moving", 3000); //dim.wait("DRIVE_CONTROL", "Armed", 120000); // dim.wait("DRIVE_CONTROL", "Locked", 3000); var sub = new Subscription("DRIVE_CONTROL/POINTING_POSITION"); sub.get(5000); // FIXME: Proper error message in case of failure function func() { var report = sub.get(); var zd = report.obj['Zd']; var az = report.obj['Az']; if (zd>100 && Math.abs(az)<1) return true; return undefined; } var now = new Date(); v8.timeout(150000, func); var report = sub.get(); console.out(""); console.out("Shutdown procedure seems to be finished..."); console.out(" Telescope at Zd=%.1fdeg Az=%.1fdeg".$(report.obj['Zd'], report.obj['Az'])); console.out(" Please make sure the park position was reached"); console.out(" and the telescope is not moving anymore."); console.out(" Please check that the lid is closed and the voltage switched off."); console.out(""); console.out("Shutdown: end ["+(new Date()-now)/1000+"s]"); sub.close(); } // ================================================================ // Crosscheck all states // ================================================================ // ---------------------------------------------------------------- // Do a standard startup to bring the system in into a well // defined state // ---------------------------------------------------------------- include('scripts/Startup.js'); // ---------------------------------------------------------------- // Check that everything we need is availabel to receive commands // (FIXME: Should that go to the general CheckState?) // ---------------------------------------------------------------- console.out("Checking send."); checkSend(["MCP", "DRIVE_CONTROL", "LID_CONTROL", "FAD_CONTROL", "FEEDBACK"]); console.out("Checking send: done"); // ---------------------------------------------------------------- // Bring feedback into the correct operational state // ---------------------------------------------------------------- console.out("Feedback init: start."); service_feedback.get(5000); dim.send("FEEDBACK/ENABLE_OUTPUT", true); dim.send("FEEDBACK/START_CURRENT_CONTROL", 0.); v8.timeout(3000, function() { var n = dim.state("FEEDBACK").name; if (n=="CurrentCtrlIdle" || n=="CurrentControl") return true; }); // ---------------------------------------------------------------- // Connect to the DRS_RUNS service // ---------------------------------------------------------------- console.out("Drs runs init: start."); var sub_drsruns = new Subscription("FAD_CONTROL/DRS_RUNS"); sub_drsruns.get(5000); // FIXME: Check if the last DRS calibration was complete? function getTimeSinceLastDrsCalib() { // ----- Time since last DRS Calibration [min] ------ var runs = sub_drsruns.get(0); var diff = (new Date()-runs.time)/60000; // Warning: 'roi=300' is a number which is not intrisically fixed // but can change depending on the taste of the observers var valid = runs.obj['run'][2]>0 && runs.obj['roi']==300; if (valid) console.out(" Last DRS calib: %.1fmin ago".$(diff)); else console.out(" No valid drs calibration available"); return valid ? diff : null; } // ---------------------------------------------------------------- // Make sure we will write files // ---------------------------------------------------------------- dim.send("FAD_CONTROL/SET_FILE_FORMAT", 2); // ---------------------------------------------------------------- // Print some information for the user about the // expected first oberservation // ---------------------------------------------------------------- var test = getObservation(); if (test!=undefined) { var n = new Date(); if (test==-1) console.out(n.toUTCString()+": First observation scheduled for "+observations[0].start.toUTCString()); if (test>=0 && testn+12*3600*1000) console.out(n.toUTCString()+": No observations scheduled for the next 12 hours!"); } // ---------------------------------------------------------------- // Start main loop // ---------------------------------------------------------------- console.out("Start main loop."); var run = -2; // getObservation never called var sub; var lastObs; var sun = Sun.horizon(-13); while (1) { // Check if observation position is still valid // If source position has changed, set run=0 var idxObs = getObservation(); if (idxObs===undefined) break; // we are still waiting for the first observation in the schedule if (idxObs==-1) { // flag that the first observation will be in the future run = -1; v8.sleep(1000); continue; } // Check if we have to take action do to sun-rise var was_up = sun.isUp; sun = Sun.horizon(-13); if (!was_up && sun.isUp) { console.out("Sun rise detected.... automatic shutdown initiated!"); // FIXME: State check? Shutdown(); continue; } // Current and next observation target var obs = observations[idxObs]; var nextObs = observations[idxObs+1]; // Check if observation target has changed if (lastObs!=idxObs) // !Object.isEqual(obs, nextObs) { console.out("--- "+idxObs+" ---"); console.out("Current time: "+new Date().toUTCString()); console.out("Current observation: "+obs.start.toUTCString()); if (nextObs!=undefined) console.out("Next observation: "+nextObs.start.toUTCString()); console.out(""); // This is the first source, but we do not come from // a scheduled 'START', so we have to check if the // telescop is operational already sub = 0; if (run<0) { //Startup(); // -> Bias On/Off?, Lid open/closed? //CloseLid(); } // The first observation had a start-time in the past... // In this particular case start with the last entry // in the list of measurements if (run==-2) sub = obs.length-1; run = 0; } lastObs = idxObs; if (nextObs==undefined && obs[obs.length-1].task!="SHUTDOWN") throw Error("Last scheduled measurement must be a shutdown."); // We are done with all measurement slots for this // observation... wait for next observation if (sub>=obs.length) { v8.sleep(1000); continue; } // Check if sun is still up... only DATA and RATESCAN must be suppressed if ((obs[sub].task=="DATA" || obs[sub].task=="RATESCAN") && sun.isUp) { var now = new Date(); var remaining = (sun.set - now)/60000; console.out(now.toUTCString()+" - "+obs[sub].task+": Sun above FACT-horizon: sleeping 1min, remaining %.1fmin".$(remaining)); v8.sleep(60000); continue; } // A scheduled task was found, lets check if all servers are // still only and in reasonable states. If this is not the case, // something unexpected must have happend and the script is aborted. //console.out(" Checking states [general]"); var table = [ [ "TNG_WEATHER" ], [ "MAGIC_WEATHER" ], [ "CHAT" ], [ "SMART_FACT" ], [ "DATA_LOGGER", [ "NightlyFileOpen", "WaitForRun", "Logging" ] ], [ "FSC_CONTROL", [ "Connected" ] ], [ "MCP", [ "Idle" ] ], [ "TIME_CHECK", [ "Valid" ] ], [ "PWR_CONTROL", [ "SystemOn" ] ], [ "AGILENT_CONTROL", [ "VoltageOn" ] ], [ "BIAS_CONTROL", [ "VoltageOff", "VoltageOn", "Ramping" ] ], [ "FEEDBACK", [ "CurrentControl", "CurrentCtrlIdle" ] ], [ "LID_CONTROL", [ "Open", "Closed" ] ], [ "DRIVE_CONTROL", [ "Armed", "Tracking", "OnTrack" ] ], [ "FTM_CONTROL", [ "Idle", "TriggerOn" ] ], [ "FAD_CONTROL", [ "Connected", "WritingData" ] ], [ "RATE_SCAN", [ "Connected" ] ], [ "RATE_CONTROL", [ "Connected", "GlobalThresholdSet", "InProgress" ] ], ]; if (!checkStates(table)) { throw new Error("Something unexpected has happened. One of the servers"+ "is in a state in which it should not be. Please,"+ "try to find out what happened..."); } console.out("\n"+(new Date()).toUTCString()+": Current measurement: "+obs[sub]); // Check if obs.task is one of the one-time-tasks switch (obs[sub].task) { case "STARTUP": console.out(" STARTUP", ""); CloseLid(); doDrsCalibration("startup"); // will switch the voltage off service_feedback.voltageOn(); service_feedback.waitForVoltageOn(); // Before we can switch to 3000 we have to make the right DRS calibration console.out(" Take single p.e. run."); while (!takeRun("pedestal", 5000)); // It is unclear what comes next, so we better switch off the voltage service_feedback.voltageOff(); break; case "SHUTDOWN": console.out(" SHUTDOWN", ""); Shutdown(); // FIXME: Avoid new observations after a shutdown until // the next startup (set run back to -2?) console.out(" Waiting for next startup.", ""); sub++; continue; case "IDLE": v8.sleep(1000); continue; case "DRSCALIB": console.out(" DRSCALIB", ""); doDrsCalibration("drscalib"); // will switch the voltage off break; case "SINGLEPE": console.out(" SINGLE-PE", ""); // The lid must be closes CloseLid(); // Check if DRS calibration is necessary var diff = getTimeSinceLastDrsCalib(); if (diff>30 || diff==null) doDrsCalibration("singlepe"); // will turn voltage off // The voltage must be on service_feedback.voltageOn(); service_feedback.waitForVoltageOn(); // Before we can switch to 3000 we have to make the right DRS calibration console.out(" Take single p.e. run."); while (!takeRun("pedestal", 5000)); // It is unclear what comes next, so we better switch off the voltage service_feedback.voltageOff(); break; case "RATESCAN": console.out(" RATESCAN", ""); var tm1 = new Date(); // This is a workaround to make sure that we really catch // the new state and not the old one dim.send("DRIVE_CONTROL/STOP"); dim.wait("DRIVE_CONTROL", "Armed", 5000); // The lid must be open OpenLid(); // The voltage must be switched on service_feedback.voltageOn(); if (obs.source != undefined) dim.send("DRIVE_CONTROL/TRACK_ON", obs[sub].source); else dim.send("DRIVE_CONTROL/TRACK", obs[sub].ra, obs[sub].dec); dim.wait("DRIVE_CONTROL", "OnTrack", 150000); // 110s for turning and 30s for stabilizing service_feedback.waitForVoltageOn(); var tm2 = new Date(); // Start rate scan dim.send("RATE_SCAN/START_THRESHOLD_SCAN", 50, 1000, -10); // Lets wait if the ratescan really starts... this might take a few // seconds because RATE_SCAN configures the ftm and is waiting for // it to be configured. dim.wait("RATE_SCAN", "InProgress", 10000); dim.wait("RATE_SCAN", "Connected", 2700000); // this line is actually some kind of hack. // after the Ratescan, no data is written to disk. I don't know why, but it happens all the time // So I decided to put this line here as a kind of patchwork.... //dim.send("FAD_CONTROL/SET_FILE_FORMAT", 2); console.out(" Ratescan done [%.1fs, %.1fs]".$((tm2-tm1)/1000, (new Date()-tm2)/1000)); break; // case "RATESCAN" case "DATA": // ========================== case "DATA" ============================ /* if (Sun.horizon("FACT").isUp) { console.out(" SHUTDOWN",""); Shutdown(); console.out(" Exit forced due to broken schedule", ""); exit(); } */ // Calculate remaining time for this observation in minutes var remaining = nextObs==undefined ? 0 : (nextObs.start-new Date())/60000; // ------------------------------------------------------------ console.out(" Run #"+run+" (remaining "+parseInt(remaining)+"min)"); // ----- Time since last DRS Calibration [min] ------ var diff = getTimeSinceLastDrsCalib(); // Changine pointing position and take calibration... // ...every four runs (every ~20min) // ...if at least ten minutes of observation time are left // ...if this is the first run on the source var point = (run%4==0 && remaining>10) || run==0; // Take DRS Calib... // ...every four runs (every ~20min) // ...at last every two hours // ...when DRS temperature has changed by more than 2deg (?) // ...when more than 15min of observation are left // ...no drs calibration was done yet var drscal = (run%4==0 && (remaining>15 && diff>70)) || diff==null; if (point) { // Change wobble position every four runs, // start with alternating wobble positions each day var wobble = (parseInt(run/4) + parseInt(new Date()/1000/3600/24-0.5))%2+1; //console.out(" Move telescope to '"+source+"' "+offset+" "+wobble); console.out(" Move telescope to '"+obs[sub].source+"' ["+wobble+"]"); //var offset = observations[obs][2]; //var wobble = observations[obs][3 + parseInt(run/4)%2]; //dim.send("DRIVE_CONTROL/TRACK_SOURCE", offset, wobble, source); dim.send("DRIVE_CONTROL/TRACK_WOBBLE", wobble, obs[sub].source); // Do we have to check if the telescope is really moving? // We can cross-check the SOURCE service later } if (drscal) doDrsCalibration("data"); // will turn voltage off OpenLid(); // voltage must be switched on after the lid is open for the // feedback to adapt the voltage properly to the night-sky // background light level. service_feedback.voltageOn(); // This is now th right time to wait for th drive to be stable dim.wait("DRIVE_CONTROL", "OnTrack", 150000); // 110s for turning and 30s for stabilizing // Now we have to be prepared for data-taking: // make sure voltage is on service_feedback.waitForVoltageOn(); // If pointing had changed, do calibration if (point) { console.out(" Calibration."); // Calibration (2% of 20') while (1) { if (!takeRun("pedestal", 1000)) // 80 Hz -> 10s continue; if (!takeRun("light-pulser-ext", 1000)) // 80 Hz -> 10s continue; break; } } console.out(" Taking data: start [5min]"); var len = 300; while (len>0) { var time = new Date(); if (takeRun("data", -1, len)) // Take data (5min) break; len -= parseInt((new Date()-time)/1000); } console.out(" Taking data: done"); run++; continue; // case "DATA" } if (nextObs!=undefined && sub==obs.length-1) console.out(" Waiting for next observation scheduled for "+nextObs.start.toUTCString(),""); sub++; } sub_drsruns.close(); // ================================================================ // Comments and ToDo goes here // ================================================================ // error handline : http://www.sitepoint.com/exceptional-exception-handling-in-javascript/ // classes: http://www.phpied.com/3-ways-to-define-a-javascript-class/ // // Arguments: TakeFirstDrsCalib // To be determined: How to stop the script without foreceful interruption?