1 /**
  2  * @fileOverview This file has functions related to documenting JavaScript.
  3  * @author <a href="mailto:thomas.bretz@epfl.ch">Thomas Bretz</a>
  4  */
  5 'use strict';
  6 
  7 dim.log("Start: "+__FILE__+" ["+__DATE__+"]");
  8 
  9 //dimctrl.defineState(37, "TimeOutBeforeTakingData", "MCP took more than 5minutes to start TakingData");
 10 
 11 // ================================================================
 12 //  Code related to the schedule
 13 // ================================================================
 14 
 15 //this is just the class implementation of 'Observation'
 16 include('scripts/Observation_class.js');
 17 
 18 // this file just contains the definition of
 19 // the variable observations, which builds our nightly schedule, hence the filename
 20 include('scripts/schedule.js');
 21 
 22 // make Observation objects from user input and check if 'date' is increasing.
 23 for (var i=0; i<observations.length; i++)
 24 {
 25     observations[i] = new Observation(observations[i]);
 26 
 27     // check if the start date given by the user is increasing.
 28     if (i>0 && observations[i].start <= observations[i-1].start)
 29     {
 30         throw new Error("Start time '"+ observations[i].start.toUTCString()+
 31                         "' in row "+i+" exceeds start time in row "+(i-1));
 32     }
 33 }
 34 
 35 // Get the observation scheduled for 'now' from the table and
 36 // return its index
 37 function getObservation(now)
 38 {
 39     if (now==undefined)
 40         now = new Date();
 41 
 42     if (isNaN(now.valueOf()))
 43         throw new Error("Date argument in getObservation invalid.");
 44 
 45     for (var i=0; i<observations.length; i++)
 46         if (now<observations[i].start)
 47             return i-1;
 48 
 49     return observations.length-1;
 50 }
 51 
 52 // ================================================================
 53 //  Code to check whether observation is allowed
 54 // ================================================================
 55 /*
 56 function currentEst(source)
 57 {
 58     var moon = new Moon();
 59     if (!moon.isUp)
 60         return 7.7;
 61 
 62     var dist = Sky.dist(moon, source);
 63 
 64     var alt = 90-moon.toLocal().zd;
 65 
 66     var lc = dist*alt*pow(Moon.disk(), 6)/360/360;
 67 
 68     var cur = 7.7+4942*lc;
 69 
 70     return cur;
 71 }
 72 
 73 function thresholdEst(source) // relative threshold (ratio)
 74 {
 75     // Assumption:
 76     // atmosphere is 70km, shower taks place after 60km, earth radius 6400km
 77     // just using the cosine law
 78     // This fits very well with MC results: See Roger Firpo, p.45
 79     // "Study of the MAGIC telescope sensitivity for Large Zenith Angle observations"
 80 
 81     var c = Math.cos(Math.Pi-source.zd);
 82     var ratio = (10*sqrt(409600*c*c+9009) + 6400*c - 60)/10;
 83 
 84     // assumption: Energy threshold increases linearily with current
 85     // assumption: Energy threshold increases linearily with distance
 86 
 87     return ratio*currentEst(source)/7.7;
 88 }
 89 */
 90 
 91 // ----------------------------------------------------------------
 92 
 93 // ================================================================
 94 //  Code related to monitoring the fad system
 95 // ================================================================
 96 
 97 var sub_incomplete = new Subscription("FAD_CONTROL/INCOMPLETE");
 98 
 99 var incomplete = 0;
100 
101 sub_incomplete.onchange = function(evt)
102 {
103     if (!evt.data)
104         return;
105 
106     var inc = evt.obj['incomplete'];
107     if (!inc || inc>0xffffffffff)
108         return;
109 
110     if (incomplete>0)
111         return;
112 
113     if (dim.state("MCP").name!="TakingData")
114         return;
115 
116     incomplete = inc;
117 
118     console.out("Sending MCP/STOP");
119     dim.send("MCP/STOP");
120 }
121 
122 var sub_connections = new Subscription("FAD_CONTROL/CONNECTIONS");
123 
124 /**
125  * call-back function of FAD_CONTROL/CONNECTIONS
126  * store IDs of problematic FADs 
127  *
128  */
129 /*
130 sub_connections.onchange = function(evt)
131 {
132     // This happens, but why?
133     if (!evt.obj['status'])
134         return;
135 
136     this.reset = [ ];
137 
138     for (var x=0; x<40; x++)
139         if (evt.obj['status'][x]!=66 && evt.obj['status'][x]!=67)
140             this.reset.push(x);
141 
142     if (this.reset.length==0)
143         return;
144 
145     //m.alarm("FAD board loss detected...");
146     dim.send("MCP/RESET");
147     dim.send("FAD_CONTROL/CLOSE_OPEN_FILES");
148 }
149 */
150 
151 /**
152  * reconnect to problematic FADs
153  *
154  * Dis- and Reconnects to FADs, found to be problematic by call-back function
155  * onchange() to have a different CONNECTION value than 66 or 67. 
156  * 
157  * @returns
158  *      a boolean is returned. 
159  *      reconnect returns true if:
160  *          * nothing needed to be reset --> no problems found by onchange()
161  *          * the reconnection went fine.
162  *      
163  *      reconnect *never returns false* so far.
164  *
165  * @example
166  *      if (!sub_connections.reconnect())
167  *          exit();
168  */
169 sub_connections.reconnect = function()
170 {
171     // this.reset is a list containing the IDs of FADs, 
172     // which have neither CONNECTION==66 nor ==67, whatever this means :-)
173     if (this.reset.length==0)
174         return true;
175 
176     console.out("  Reconnect: start ["+this.reset.length+"]");
177 
178     for (var i=0; i<this.reset.length; i++)
179         dim.send("FAD_CONTROL/DISCONNECT", this.reset[i]);
180 
181     v8.sleep(3000);
182 
183     while (this.reset.length)
184         dim.send("FAD_CONTROL/CONNECT", this.reset.pop());
185 
186     v8.sleep(1000);
187     dim.wait("FAD_CONTROL", "Connected", 3000);
188 
189     console.out("  Reconnect: end");
190 
191     return true;
192 }
193 
194 // ================================================================
195 //  Code related to taking data
196 // ================================================================
197 
198 var startrun = new Subscription("FAD_CONTROL/START_RUN");
199 startrun.get(5000);
200 
201 function reconnect(list, txt)
202 { /*
203     var reset = [ ];
204 
205     for (var i=0; i<list.length; i++)
206         {
207             console.out("  FAD %2d".$(list[i])+" lost during "+txt);
208             reset.push(parseInt(list[i]/10));
209         }
210 
211     reset = reset.filter(function(elem,pos){return reset.indexOf(elem)==pos;});
212 
213     console.out("");
214     console.out("  FADs belong to crate(s): "+reset);
215     console.out("");
216 */
217     console.out("");
218     console.out("Trying automatic reconnect ["+txt+"]...");
219 
220     for (var i=0; i<list.length; i++)
221     {
222         console.out("   ...disconnect "+list[i]);
223         dim.send("FAD_CONTROL/DISCONNECT", list[i]);
224     }
225 
226     console.out("   ...waiting for 5s");
227     v8.sleep(5000);
228 
229     for (var i=0; i<list.length; i++)
230     {
231         console.out("   ...reconnect "+list[i]);
232         dim.send("FAD_CONTROL/CONNECT", list[i]);
233     }
234 
235     console.out("   ...waiting for 1s");
236     v8.sleep(1000);
237     console.out("");
238 }
239 
240 function takeRun(type, count, time)
241 {
242     if (!count)
243         count = -1;
244     if (!time)
245         time = -1;
246 
247     var nextrun = startrun.get().obj['next'];
248     console.out("  Take run %3d".$(nextrun)+": N="+count+" T="+time+"s ["+type+"]");
249 
250     incomplete = 0;
251     dim.send("MCP/START", time?time:-1, count?count:-1, type);
252 
253     // FIXME: Replace by callback?
254     //
255     // DN: I believe instead of waiting for 'TakingData' one could split this
256     // up into two checks with an extra condition:
257     //  if type == 'data':
258     //      wait until ThresholdCalibration starts:
259     //          --> this time should be pretty identical for each run
260     //      if this takes longer than say 3s:
261     //          there might be a problem with one/more FADs
262     //    
263     //      wait until "TakingData":
264     //          --> this seems to take even some minutes sometimes... 
265     //              (might be optimized rather soon, but still in the moment...)
266     //      if this takes way too long: 
267     //          there might be something broken, 
268     //          so maybe a very high time limit is ok here.
269     //          I think there is not much that can go wrong, 
270     //          when the Thr-Calib has already started. Still it might be nice 
271     //          If in the future RateControl is written so to find out that 
272     //          in case the threshold finding algorithm does 
273     //          *not converge as usual*
274     //          it can complain, and in this way give a hint, that the weather
275     //          might be a little bit too bad.
276     //  else:
277     //      wait until "TakingData":
278     //          --> in a non-data run this time should be pretty short again
279     //      if this takes longer than say 3s:
280     //          there might be a problem with one/more FADs
281     //  
282 
283     // Use this if you use the rate control to calibrate by rates
284     //if (!dim.wait("MCP", "TakingData", -300000) )
285     //{
286     //    throw new Error("MCP took longer than 5 minutes to start TakingData"+
287     //                    "maybe this idicates a problem with one of the FADs?");
288     //}
289 
290     // Here we could check and handle fad losses
291 
292     try
293     {
294         dim.wait("MCP", "TakingData", 15000);
295     }
296     catch (e)
297     {
298         console.out("");
299         console.out("MCP:         "+dim.state("MCP").name);
300         console.out("FAD_CONTROL: "+dim.state("FAD_CONTROL").name);
301         console.out("FTM_CONTROL: "+dim.state("FTM_CONTROL").name);
302         console.out("");
303 
304         if (dim.state("MCP").name!="Configuring3" ||
305             dim.state("FAD_CONTROL").name!="Configuring2")
306             throw e;
307 
308         console.out("");
309         console.out("Waiting for fadctrl to get configured timed out... checking for in-run FAD loss.");
310 
311         var con  = sub_connections.get();
312         var stat = con.obj['status'];
313 
314         console.out("Sending MCP/RESET");
315         dim.send("MCP/RESET");
316 
317         dim.wait("FTM_CONTROL", "Idle",      3000);
318         dim.wait("FAD_CONTROL", "Connected", 3000);
319         dim.wait("MCP",         "Idle",      3000);
320 
321         /*** FOR REMOVE ***/
322         var reset = [ ];
323 
324         for (var i=0; i<40; i++)
325             if (stat[i]!=0x43)
326             {
327                 console.out("  FAD %2d".$(i)+" not in Configured state.");
328                 reset.push(parseInt(i/10));
329             }
330 
331         reset = reset.filter(function(elem,pos){return reset.indexOf(elem)==pos;});
332 
333         if (reset.length>0)
334         {
335             console.out("");
336             console.out("  FADs belong to crate(s): "+reset);
337             console.out("");
338         }
339         /**** FOR REMOVE ****/
340 
341         var list = [];
342         for (var i=0; i<40; i++)
343             if (stat[i]!=0x43)
344                 list.push(i);
345 
346         reconnect(list, "configuration");
347 
348         throw e;
349     }
350 
351     dim.wait("MCP", "Idle", time>0 ? time*1250 : undefined); // run time plus 25%
352 
353     if (incomplete)
354     {
355         console.out("Incomplete: "+incomplete);
356 
357         console.out("");
358         console.out("MCP:         "+dim.state("MCP").name);
359         console.out("FAD_CONTROL: "+dim.state("FAD_CONTROL").name);
360         console.out("FTM_CONTROL: "+dim.state("FTM_CONTROL").name);
361         console.out("");
362 
363         dim.wait("MCP",         "Idle", 3000);
364         dim.wait("FTM_CONTROL", "Idle", 3000);
365 
366         // Necessary to allow the disconnect, reconnect
367         dim.send("FAD_CONTROL/CLOSE_OPEN_FILES");
368         dim.wait("FAD_CONTROL", "Connected", 3000);
369 
370         var list = [];
371         for (var i=0; i<40; i++)
372             if (incomplete&(1<<i))
373                 list.push(i);
374 
375         reconnect(list, "data taking");
376 
377         throw new Error("In-run FAD loss detected.");
378     }
379 
380     //console.out("  Take run: end");
381 
382     // DN: currently reconnect() never returns false 
383     //     .. but it can fail of course.
384     //if (!sub_connections.reconnect())
385     //    exit();
386 
387     return true;//sub_connections.reconnect();
388 }
389 
390 // ----------------------------------------------------------------
391 
392 function doDrsCalibration(where)
393 {
394     console.out("  Take DRS calibration ["+where+"]");
395 
396     service_feedback.voltageOff();
397 
398     var tm = new Date();
399 
400     while (1)
401     {
402         dim.send("FAD_CONTROL/START_DRS_CALIBRATION");
403         if (!takeRun("drs-pedestal", 1000))     // 40 / 20s     (50Hz)
404             continue;
405 
406         // Does that fix the runopen before runclose problem?
407         //dim.wait("FAD_CONTROL", "Connected", 3000);
408         //v8.sleep(1000);
409 
410         if (!takeRun("drs-gain",     1000))     // 40 / 20s     (50Hz)
411             continue;
412 
413         // Does that fix the runopen before runclose problem?
414         //dim.wait("FAD_CONTROL", "Connected", 3000);
415         //v8.sleep(1000);
416 
417         if (!takeRun("drs-pedestal", 1000))     // 40 / 20s     (50Hz)
418             continue;
419 
420         dim.send("FAD_CONTROL/SET_FILE_FORMAT", 2);
421         if (!takeRun("drs-pedestal", 1000))     // 40 / 20s     (50Hz)
422             continue;
423         if (!takeRun("drs-time",     1000))     // 40 / 20s     (50Hz)
424             continue;
425 
426         dim.send("FAD_CONTROL/RESET_SECONDARY_DRS_BASELINE");
427         if (!takeRun("pedestal",     1000))     // 40 / 10s     (80Hz)
428             continue;
429 
430         dim.send("FAD_CONTROL/SET_FILE_FORMAT", 2);
431         if (!takeRun("pedestal",     1000))     // 40 / 10s     (80Hz)
432             continue;
433         //                                       -----------
434         //                                       4'40 / 2'00
435 
436         break;
437     }
438 
439     console.out("  DRS calibration done [%.1f]".$((new Date()-tm)/1000));
440 }
441 
442 // ================================================================
443 //  Code related to the lid
444 // ================================================================
445 
446 function OpenLid()
447 {
448     /*
449     while (Sun.horizon(-13).isUp)
450     {
451         var now = new Date();
452         var minutes_until_sunset = (Sun.horizon(-13).set - now)/60000;
453         console.out(now.toUTCString()+": Sun above FACT-horizon, lid cannot be opened: sleeping 1min, remaining %.1fmin".$(minutes_until_sunset));
454         v8.sleep(60000);
455     }*/
456 
457     var isClosed = dim.state("LID_CONTROL").name=="Closed";
458 
459     var tm = new Date();
460 
461     // Wait for lid to be open
462     if (isClosed)
463     {
464         console.out("  Open lid: start");
465         dim.send("LID_CONTROL/OPEN");
466     }
467     dim.wait("LID_CONTROL", "Open", 30000);
468 
469     if (isClosed)
470         console.out("  Open lid: done [%.1fs]".$((new Date()-tm)/1000));
471 }
472 
473 function CloseLid()
474 {
475     var isOpen = dim.state("LID_CONTROL").name=="Open";
476 
477     var tm = new Date();
478 
479     // Wait for lid to be open
480     if (isOpen)
481     {
482         console.out("  Close lid: start");
483         dim.send("LID_CONTROL/CLOSE");
484     }
485     dim.wait("LID_CONTROL", "Closed", 30000);
486 
487     if (isOpen)
488         console.out("  Close lid: end [%.1fs]".$((new Date()-tm)/1000));
489 }
490 
491 // ================================================================
492 //  Code related to switching bias voltage on and off
493 // ================================================================
494 
495 var service_feedback = new Subscription("FEEDBACK/DEVIATION");
496 
497 service_feedback.onchange = function(evt)
498 {
499     if (this.cnt && evt.counter>this.cnt+12)
500         return;
501 
502     this.voltageStep = null;
503     if (!evt.data)
504         return;
505 
506     var delta = evt.obj['DeltaBias'];
507 
508     var avg = 0;
509     for (var i=0; i<320; i++)
510         avg += delta[i];
511     avg /= 320;
512 
513     if (this.previous)
514         this.voltageStep = Math.abs(avg-this.previous);
515 
516     this.previous = avg;
517 
518     console.out("  DeltaV="+this.voltageStep);
519 }
520 
521 // DN:  Why is voltageOff() implemented as 
522 //      a method of a Subscription to a specific Service
523 //      I naively would think of voltageOff() as an unbound function.
524 //      I seems to me it has to be a method of a Subscription object, in order
525 //      to use the update counting method. But does it have to be
526 //      a Subscription to FEEDBACK/DEVIATION, or could it work with other services as well?
527 service_feedback.voltageOff = function()
528 {
529     var state = dim.state("BIAS_CONTROL").name;
530 
531     // check of feedback has to be switched on
532     var isOn = state=="VoltageOn" || state=="Ramping";
533     if (isOn)
534     {
535         console.out("  Voltage off: start");
536 
537         // Supress the possibility that the bias control is
538         // ramping and will reject the command to switch the
539         // voltage off
540         var isControl = dim.state("FEEDBACK").name=="CurrentControl";
541         if (isControl)
542         {
543             console.out("  Suspending feedback.");
544             dim.send("FEEDBACK/ENABLE_OUTPUT", false);
545             dim.wait("FEEDBACK", "CurrentCtrlIdle", 3000);
546         }
547 
548         // Switch voltage off
549         console.out("  Voltage on: switch off");
550         dim.send("BIAS_CONTROL/SET_ZERO_VOLTAGE");
551 
552         // If the feedback was enabled, re-enable it
553         if (isControl)
554         {
555             console.out("  Resuming feedback.");
556             dim.send("FEEDBACK/ENABLE_OUTPUT", true);
557             dim.wait("FEEDBACK", "CurrentControl", 3000);
558         }
559     }
560 
561     dim.wait("BIAS_CONTROL", "VoltageOff", 5000);
562 
563     // FEEDBACK stays in CurrentCtrl when Voltage is off but output enabled
564     // dim.wait("FEEDBACK", "CurrentCtrlIdle", 1000);
565 
566     if (isOn)
567         console.out("  Voltage off: end");
568 }
569 
570 // DN:  The name of the method voltageOn() in the context of the method
571 //      voltageOff() is a little bit misleading, since when voltageOff() returns
572 //      the caller can be sure the voltage is off, but when voltageOn() return
573 //      this is not the case, in the sense, that the caller can now take data.
574 //      instead the caller of voltageOn() *must* call waitForVoltageOn() afterwards
575 //      in order to safely take good-quality data.
576 //      This could lead to nasty bugs in the sense, that the second call might 
577 //      be forgotten by somebody
578 //      
579 //      so I suggest to rename voltageOn() --> prepareVoltageOn()
580 //      waitForVoltageOn() stays as it is
581 //      and one creates a third method called:voltageOn() like this
582 /*      service_feedback.voltageOn = function()
583  *      {
584  *          this.prepareVoltageOn();
585  *          this.waitForVoltageOn();
586  *      }
587  * 
588  * */
589 //      For convenience.
590 
591 service_feedback.voltageOn = function()
592 {
593     //if (Sun.horizon("FACT").isUp)
594     //    throw new Error("Sun is above FACT-horizon, voltage cannot be switched on.");
595 
596     var isOff = dim.state("BIAS_CONTROL").name=="VoltageOff";
597     if (isOff)
598     {
599         console.out("  Voltage on: switch on");
600         //console.out(JSON.stringify(dim.state("BIAS_CONTROL")));
601 
602         dim.send("BIAS_CONTROL/SET_GLOBAL_DAC", 1);
603     }
604 
605     // Wait until voltage on
606     dim.wait("BIAS_CONTROL", "VoltageOn", 5000);
607 
608     // From now on the feedback waits for a valid report from the FSC
609     // and than switchs to CurrentControl
610     dim.wait("FEEDBACK", "CurrentControl", 60000);
611 
612     if (isOff)
613     {
614         console.out("  Voltage on: cnt="+this.cnt);
615 
616         this.previous = undefined;
617         this.cnt = this.get().counter;
618         this.voltageStep = undefined;
619     }
620 }
621 
622 service_feedback.waitForVoltageOn = function()
623 {
624     // waiting 45sec for the current control to stabilize...
625     // v8.sleep(45000);
626 
627     // ----- Wait for at least three updates -----
628     // The feedback is started as if the camera where at 0deg
629     // Then after the first temp update, the temperature will be set to the
630     // correct value (this has already happened)
631     // So we only have to wait for the current to get stable.
632     // This should happen after three to five current updates.
633     // So we want one recent temperature update
634     //  and three recent current updates
635 
636     // Avoid output if condition is already fulfilled
637     if (this.cnt && this.get().counter>this.cnt+10)
638         return;
639 
640     // FIXME: timeout missing
641     console.out("  Feedback wait: start");
642 
643     function func(service)
644     {
645         if ((service.cnt!=undefined && service.get().counter>service.cnt+10) ||
646             (service.voltageStep && service.voltageStep<0.02))
647             return true;
648     }
649 
650     var now = new Date();
651     //v8.timeout(5*60000, func, this);
652     while ((this.cnt==undefined || this.get().counter<=this.cnt+10) && (!this.voltageStep || this.voltageStep>0.02))
653         v8.sleep();
654 
655     console.out("  Feedback wait: end [dV=%.3f, cnt=%d, %.2fs]".$(this.voltageStep, this.get().counter, (new Date()-now)/1000));
656 }
657 
658 // ================================================================
659 //  Function to shutdown the system
660 // ================================================================
661 
662 function Shutdown()
663 {
664     console.out("Shutdown: start");
665 
666     service_feedback.voltageOff();
667     CloseLid(); 
668     dim.send("DRIVE_CONTROL/PARK");
669 
670     console.out("Waiting for telescope to park. This may take a while.");
671 
672     // FIXME: This might not work is the drive is already close to park position
673     dim.wait("DRIVE_CONTROL", "Locked", 3000);
674 
675     var sub = new Subscription("DRIVE_CONTROL/POINTING_POSITION");
676     sub.get(5000);  // FIXME: Proper error message in case of failure
677 
678     function func()
679     {
680         var report = sub.get();
681 
682         var zd = report.obj['Zd'];
683         var az = report.obj['Az'];
684 
685         if (zd>100 && Math.abs(az)<1)
686             return true;
687 
688         return undefined;
689     }
690 
691     var now = new Date();
692     v8.timeout(150000, func);
693 
694     //dim.send("FEEDBACK/STOP");
695     dim.send("FEEDBACK/ENABLE_OUTPUT", false);
696     dim.send("FTM_CONTROL/STOP_TRIGGER");
697 
698     dim.wait("FEEDBACK", "CurrentCtrlIdle", 3000);
699     dim.wait("FTM_CONTROL", "Idle", 3000);
700 
701     var report = sub.get();
702 
703     console.out("");
704     console.out("Shutdown procedure seems to be finished...");
705     console.out("  Telescope at Zd=%.1fdeg Az=%.1fdeg".$(report.obj['Zd'], report.obj['Az']));
706     console.out("  Please make sure the park position was reached");
707     console.out("  and the telescope is not moving anymore.");
708     console.out("  Please check that the lid is closed and the voltage switched off.");
709     console.out("");
710     console.out("Shutdown: end ["+(new Date()-now)/1000+"s]");
711 
712     sub.close();
713 }
714 
715 // ================================================================
716 // Check datalogger subscriptions
717 // ================================================================
718 
719 var datalogger_subscriptions = new Subscription("DATA_LOGGER/SUBSCRIPTIONS");
720 datalogger_subscriptions.get(3000, false);
721 
722 datalogger_subscriptions.check = function()
723 {
724     var obj = this.get();
725     if (!obj.data)
726         throw new Error("DATA_LOGGER/SUBSCRIPTIONS not available.");
727 
728     var expected =
729         [
730          "BIAS_CONTROL/CURRENT",
731          "BIAS_CONTROL/DAC",
732          "BIAS_CONTROL/NOMINAL",
733          "BIAS_CONTROL/VOLTAGE",
734          "DRIVE_CONTROL/POINTING_POSITION",
735          "DRIVE_CONTROL/SOURCE_POSITION",
736          "DRIVE_CONTROL/STATUS",
737          "DRIVE_CONTROL/TRACKING_POSITION",
738          "FAD_CONTROL/CONNECTIONS",
739          "FAD_CONTROL/DAC",
740          "FAD_CONTROL/DNA",
741          "FAD_CONTROL/DRS_RUNS",
742          "FAD_CONTROL/EVENTS",
743          "FAD_CONTROL/FEEDBACK_DATA",
744          "FAD_CONTROL/FILE_FORMAT",
745          "FAD_CONTROL/FIRMWARE_VERSION",
746          "FAD_CONTROL/INCOMPLETE",
747          "FAD_CONTROL/PRESCALER",
748          "FAD_CONTROL/REFERENCE_CLOCK",
749          "FAD_CONTROL/REGION_OF_INTEREST",
750          "FAD_CONTROL/RUNS",
751          "FAD_CONTROL/RUN_NUMBER",
752          "FAD_CONTROL/START_RUN",
753          "FAD_CONTROL/STATISTICS1",
754          "FAD_CONTROL/STATISTICS2",
755          "FAD_CONTROL/STATS",
756          "FAD_CONTROL/STATUS",
757          "FAD_CONTROL/TEMPERATURE",
758          "FEEDBACK/CALIBRATED_CURRENTS",
759          "FEEDBACK/CALIBRATION",
760          "FEEDBACK/DEVIATION",
761          "FEEDBACK/REFERENCE",
762          "FSC_CONTROL/CURRENT",
763          "FSC_CONTROL/HUMIDITY",
764          "FSC_CONTROL/TEMPERATURE",
765          "FSC_CONTROL/VOLTAGE",
766          "FTM_CONTROL/COUNTER",
767          "FTM_CONTROL/DYNAMIC_DATA",
768          "FTM_CONTROL/ERROR",
769          "FTM_CONTROL/FTU_LIST",
770          "FTM_CONTROL/PASSPORT",
771          "FTM_CONTROL/STATIC_DATA",
772          "FTM_CONTROL/TRIGGER_RATES",
773          "LID_CONTROL/DATA",
774          "MAGIC_LIDAR/DATA",
775          "MAGIC_WEATHER/DATA",
776          "MCP/CONFIGURATION",
777          "PWR_CONTROL/DATA",
778          "RATE_CONTROL/THRESHOLD",
779          "RATE_SCAN/DATA",
780          "RATE_SCAN/PROCESS_DATA",
781          "TEMPERATURE/DATA",
782          "TIME_CHECK/OFFSET",
783          "TNG_WEATHER/DATA",
784          "TNG_WEATHER/DUST",
785         ];
786 
787     function map(entry)
788     {
789         if (entry.length==0)
790             return undefined;
791 
792         var rc = entry.split(',');
793         if (rc.length!=2)
794             throw new Error("Subscription list entry '"+entry+"' has wrong number of elements.");
795         return rc;
796     }
797 
798     var list = obj.data.split('\n').map(map);
799 
800     function check(name)
801     {
802         if (list.every(function(el){return el[0]!=name;}))
803             throw new Error("Subscription to '"+name+"' not available.");
804     }
805 
806     expected.forEach(check);
807 }
808 
809 
810 
811 // ================================================================
812 // Crosscheck all states
813 // ================================================================
814 
815 // ----------------------------------------------------------------
816 // Do a standard startup to bring the system in into a well
817 // defined state
818 // ----------------------------------------------------------------
819 include('scripts/Startup.js');
820 
821 // ----------------------------------------------------------------
822 // Check that everything we need is availabel to receive commands
823 // (FIXME: Should that go to the general CheckState?)
824 // ----------------------------------------------------------------
825 console.out("Checking send.");
826 checkSend(["MCP", "DRIVE_CONTROL", "LID_CONTROL", "FAD_CONTROL", "FEEDBACK"]);
827 console.out("Checking send: done");
828 
829 // ----------------------------------------------------------------
830 // Bring feedback into the correct operational state
831 // ----------------------------------------------------------------
832 console.out("Feedback init: start.");
833 service_feedback.get(5000);
834 
835 dim.send("FEEDBACK/ENABLE_OUTPUT", true);
836 dim.send("FEEDBACK/START_CURRENT_CONTROL", 0.);
837 
838 v8.timeout(3000, function() { var n = dim.state("FEEDBACK").name; if (n=="CurrentCtrlIdle" || n=="CurrentControl") return true; });
839 
840 // ----------------------------------------------------------------
841 // Connect to the DRS_RUNS service
842 // ----------------------------------------------------------------
843 console.out("Drs runs init: start.");
844 
845 var sub_drsruns = new Subscription("FAD_CONTROL/DRS_RUNS");
846 sub_drsruns.get(5000);
847 // FIXME: Check if the last DRS calibration was complete?
848 
849 function getTimeSinceLastDrsCalib()
850 {
851     // ----- Time since last DRS Calibration [min] ------
852     var runs = sub_drsruns.get(0);
853     var diff = (new Date()-runs.time)/60000;
854 
855     // Warning: 'roi=300' is a number which is not intrisically fixed
856     //          but can change depending on the taste of the observers
857     var valid = runs.obj['run'][2]>0 && runs.obj['roi']==300;
858 
859     if (valid)
860         console.out("  Last DRS calib: %.1fmin ago".$(diff));
861     else
862         console.out("  No valid drs calibration available");
863 
864     return valid ? diff : null;
865 }
866 
867 // ----------------------------------------------------------------
868 // Make sure we will write files
869 // ----------------------------------------------------------------
870 dim.send("FAD_CONTROL/SET_FILE_FORMAT", 2);
871 
872 // ----------------------------------------------------------------
873 // Print some information for the user about the
874 // expected first oberservation
875 // ----------------------------------------------------------------
876 var test = getObservation();
877 if (test!=undefined)
878 {
879     var n = new Date();
880     if (test==-1)
881         console.out(n.toUTCString()+": First observation scheduled for "+observations[0].start.toUTCString());
882     if (test>=0 && test<observations.length)
883         console.out(n.toUTCString()+": First observation should start immediately.");
884     if (observations[0].start>n+12*3600*1000)
885         console.out(n.toUTCString()+": No observations scheduled for the next 12 hours!");
886 }
887 
888 // ----------------------------------------------------------------
889 // Start main loop
890 // ----------------------------------------------------------------
891 console.out("Start main loop.");
892 
893 var run = -2; // getObservation never called
894 var sub;
895 var lastObs;
896 var sun = Sun.horizon(-13);
897 var system_on;  // undefined
898 
899 while (1)
900 {
901     // Check if observation position is still valid
902     // If source position has changed, set run=0
903     var idxObs = getObservation();
904     if (idxObs===undefined)
905         break;
906 
907     // we are still waiting for the first observation in the schedule
908     if (idxObs==-1)
909     {
910         // flag that the first observation will be in the future
911         run = -1; 
912         v8.sleep(1000);
913         continue;
914     }
915 
916     // Check if we have to take action do to sun-rise
917     var was_up = sun.isUp;
918     sun = Sun.horizon(-13);
919     if (!was_up && sun.isUp)
920     {
921         console.out("", "Sun rise detected.... automatic shutdown initiated!");
922         // FIXME: State check?
923         Shutdown();
924         system_on = false;
925         continue;
926     }
927 
928     // Current and next observation target
929     var obs     = observations[idxObs];
930     var nextObs = observations[idxObs+1];
931 
932     // Check if observation target has changed
933     if (lastObs!=idxObs) // !Object.isEqual(obs, nextObs)
934     {
935         console.out("--- "+idxObs+" ---");
936         console.out("Current time:        "+new Date().toUTCString());
937         console.out("Current observation: "+obs.start.toUTCString());
938         if (nextObs!=undefined)
939             console.out("Next    observation: "+nextObs.start.toUTCString());
940         console.out("");
941 
942         // This is the first source, but we do not come from
943         // a scheduled 'START', so we have to check if the
944         // telescop is operational already
945         sub = 0;
946         if (run<0)
947         {
948             //Startup();   // -> Bias On/Off?, Lid open/closed?
949             //CloseLid();
950         }
951 
952         // The first observation had a start-time in the past...
953         // In this particular case start with the last entry
954         // in the list of measurements
955         if (run==-2)
956             sub = obs.length-1;
957 
958         run = 0;
959     }
960     lastObs = idxObs;
961 
962     if (nextObs==undefined && obs[obs.length-1].task!="SHUTDOWN")
963         throw Error("Last scheduled measurement must be a shutdown.");
964 
965     // We are done with all measurement slots for this
966     // observation... wait for next observation
967     if (sub>=obs.length)
968     {
969         v8.sleep(1000);
970         continue;
971     }
972 
973     var task = obs[sub].task;
974 
975     if (system_on===false && task!="STARTUP")
976     {
977         v8.sleep(1000);
978         continue;
979     }
980 
981     // Check if sun is still up... only DATA and RATESCAN must be suppressed
982     if ((task=="DATA" || task=="RATESCAN") && sun.isUp)
983     {
984         var now = new Date();
985         var remaining = (sun.set - now)/60000;
986         console.out(now.toUTCString()+" - "+obs[sub].task+": Sun above FACT-horizon: sleeping 1min, remaining %.1fmin".$(remaining));
987         v8.sleep(60000);
988         continue;
989     }
990 
991     console.out("\n"+(new Date()).toUTCString()+": Current measurement: "+obs[sub]);
992 
993     var power_states = sun.isUp || system_on===false ? [ "DriveOff" ] : [ "SystemOn" ];
994     var drive_states = sun.isUp || system_on===false ?   undefined    : [ "Armed", "Tracking", "OnTrack" ];
995 
996     // A scheduled task was found, lets check if all servers are
997     // still only and in reasonable states. If this is not the case,
998     // something unexpected must have happend and the script is aborted.
999     //console.out("  Checking states [general]");
1000     var table =
1001         [
1002          [ "TNG_WEATHER"   ],
1003          [ "MAGIC_WEATHER" ],
1004          [ "CHAT"          ],
1005          [ "SMART_FACT"    ],
1006          [ "TEMPERATURE"   ],
1007          [ "DATA_LOGGER",     [ "NightlyFileOpen", "WaitForRun", "Logging" ] ],
1008          [ "FSC_CONTROL",     [ "Connected"                ] ],
1009          [ "MCP",             [ "Idle"                     ] ],
1010          [ "TIME_CHECK",      [ "Valid"                    ] ],
1011          [ "PWR_CONTROL",     power_states/*[ "SystemOn"                 ]*/ ],
1012 //         [ "AGILENT_CONTROL", [ "VoltageOn"                ] ],
1013          [ "BIAS_CONTROL",    [ "VoltageOff", "VoltageOn", "Ramping" ] ],
1014          [ "FEEDBACK",        [ "CurrentControl", "CurrentCtrlIdle" ] ],
1015          [ "LID_CONTROL",     [ "Open", "Closed"           ] ],
1016          [ "DRIVE_CONTROL",   drive_states/*[ "Armed", "Tracking", "OnTrack" ]*/ ],
1017          [ "FTM_CONTROL",     [ "Idle", "TriggerOn"        ] ],
1018          [ "FAD_CONTROL",     [ "Connected", "WritingData" ] ],
1019          [ "RATE_SCAN",       [ "Connected"                ] ],
1020          [ "RATE_CONTROL",    [ "Connected", "GlobalThresholdSet", "InProgress"  ] ],
1021         ];
1022 
1023     if (!checkStates(table))
1024     {
1025         throw new Error("Something unexpected has happened. One of the servers"+
1026                         "is in a state in which it should not be. Please,"+
1027                         "try to find out what happened...");
1028     }
1029 
1030     datalogger_subscriptions.check();
1031 
1032     // Check if obs.task is one of the one-time-tasks
1033     switch (obs[sub].task)
1034     {
1035     case "STARTUP":
1036         console.out("  STARTUP", "");
1037         CloseLid();
1038 
1039         doDrsCalibration("startup");  // will switch the voltage off
1040 
1041         service_feedback.voltageOn();
1042         service_feedback.waitForVoltageOn();
1043 
1044         // Before we can switch to 3000 we have to make the right DRS calibration
1045         console.out("  Take single p.e. run.");
1046         while (!takeRun("pedestal", 5000));
1047 
1048         // It is unclear what comes next, so we better switch off the voltage
1049         service_feedback.voltageOff();
1050         system_on = true;
1051         break;
1052 
1053     case "SHUTDOWN":
1054         console.out("  SHUTDOWN", "");
1055         Shutdown();
1056         system_on = false;
1057 
1058         // FIXME: Avoid new observations after a shutdown until
1059         //        the next startup (set run back to -2?)
1060         console.out("  Waiting for next startup.", "");
1061         sub++;
1062         continue;
1063 
1064     case "IDLE":
1065         v8.sleep(1000);
1066         continue;
1067 
1068     case "DRSCALIB":
1069         console.out("  DRSCALIB", "");
1070         doDrsCalibration("drscalib");  // will switch the voltage off
1071         break;
1072 
1073     case "SINGLEPE":
1074         console.out("  SINGLE-PE", "");
1075 
1076         // The lid must be closes
1077         CloseLid();
1078 
1079         // Check if DRS calibration is necessary
1080         var diff = getTimeSinceLastDrsCalib();
1081         if (diff>30 || diff==null)
1082             doDrsCalibration("singlepe");  // will turn voltage off
1083 
1084         // The voltage must be on
1085         service_feedback.voltageOn();
1086         service_feedback.waitForVoltageOn();
1087 
1088         // Before we can switch to 3000 we have to make the right DRS calibration
1089         console.out("  Take single p.e. run.");
1090         while (!takeRun("pedestal", 5000));
1091 
1092         // It is unclear what comes next, so we better switch off the voltage
1093         service_feedback.voltageOff();
1094         break;
1095 
1096     case "RATESCAN":
1097         console.out("  RATESCAN", "");
1098 
1099         var tm1 = new Date();
1100 
1101         // This is a workaround to make sure that we really catch
1102         // the new state and not the old one
1103         dim.send("DRIVE_CONTROL/STOP");
1104         dim.wait("DRIVE_CONTROL", "Armed", 5000);
1105 
1106         // The lid must be open
1107         OpenLid();
1108 
1109         // The voltage must be switched on
1110         service_feedback.voltageOn();
1111 
1112         if (obs.source != undefined)
1113             dim.send("DRIVE_CONTROL/TRACK_ON", obs[sub].source);
1114         else
1115             dim.send("DRIVE_CONTROL/TRACK", obs[sub].ra, obs[sub].dec);
1116 
1117         dim.wait("DRIVE_CONTROL", "OnTrack", 150000); // 110s for turning and 30s for stabilizing
1118 
1119         service_feedback.waitForVoltageOn();
1120 
1121         var tm2 = new Date();
1122 
1123         // Start rate scan
1124         dim.send("RATE_SCAN/START_THRESHOLD_SCAN", 50, 1000, -10);
1125 
1126         // Lets wait if the ratescan really starts... this might take a few
1127         // seconds because RATE_SCAN configures the ftm and is waiting for
1128         // it to be configured.
1129         dim.wait("RATE_SCAN", "InProgress", 10000);
1130         dim.wait("RATE_SCAN", "Connected", 2700000);
1131 
1132         // this line is actually some kind of hack. 
1133         // after the Ratescan, no data is written to disk. I don't know why, but it happens all the time
1134         // So I decided to put this line here as a kind of patchwork....
1135         //dim.send("FAD_CONTROL/SET_FILE_FORMAT", 2);
1136 
1137         console.out("  Ratescan done [%.1fs, %.1fs]".$((tm2-tm1)/1000, (new Date()-tm2)/1000));
1138         break; // case "RATESCAN"
1139 
1140     case "DATA":
1141 
1142         // ========================== case "DATA" ============================
1143     /*
1144         if (Sun.horizon("FACT").isUp)
1145         {
1146             console.out("  SHUTDOWN","");
1147             Shutdown();
1148             console.out("  Exit forced due to broken schedule", "");
1149             exit();
1150         }
1151     */
1152         // Calculate remaining time for this observation in minutes
1153         var remaining = nextObs==undefined ? 0 : (nextObs.start-new Date())/60000;
1154 
1155         // ------------------------------------------------------------
1156 
1157         console.out("  Run #"+run+"  (remaining "+parseInt(remaining)+"min)");
1158 
1159         // ----- Time since last DRS Calibration [min] ------
1160         var diff = getTimeSinceLastDrsCalib();
1161 
1162         // Changine pointing position and take calibration...
1163         //  ...every four runs (every ~20min)
1164         //  ...if at least ten minutes of observation time are left
1165         //  ...if this is the first run on the source
1166         var point  = (run%4==0 && remaining>10) || run==0;
1167 
1168         // Take DRS Calib...
1169         //  ...every four runs (every ~20min)
1170         //  ...at last  every two hours
1171         //  ...when DRS temperature has changed by more than 2deg (?)
1172         //  ...when more than 15min of observation are left
1173         //  ...no drs calibration was done yet
1174         var drscal = (run%4==0 && (remaining>15 && diff>70)) || diff==null;
1175 
1176         if (point)
1177         {
1178             // Change wobble position every four runs,
1179             // start with alternating wobble positions each day
1180             var wobble = (parseInt(run/4) + parseInt(new Date()/1000/3600/24-0.5))%2+1;
1181 
1182             //console.out("  Move telescope to '"+source+"' "+offset+" "+wobble);
1183             console.out("  Move telescope to '"+obs[sub].source+"' ["+wobble+"]");
1184 
1185             //var offset = observations[obs][2];
1186             //var wobble = observations[obs][3 + parseInt(run/4)%2];
1187 
1188             //dim.send("DRIVE_CONTROL/TRACK_SOURCE", offset, wobble, source);
1189 
1190             dim.send("DRIVE_CONTROL/TRACK_WOBBLE", wobble, obs[sub].source);
1191 
1192             // Do we have to check if the telescope is really moving?
1193             // We can cross-check the SOURCE service later
1194         }
1195 
1196         if (drscal)
1197             doDrsCalibration("data");  // will turn voltage off
1198 
1199         OpenLid();
1200 
1201         // voltage must be switched on after the lid is open for the
1202         // feedback to adapt the voltage properly to the night-sky
1203         // background light level.
1204         service_feedback.voltageOn();
1205 
1206         // This is now th right time to wait for th drive to be stable
1207         dim.wait("DRIVE_CONTROL", "OnTrack", 150000); // 110s for turning and 30s for stabilizing
1208 
1209         // Now we have to be prepared for data-taking:
1210         // make sure voltage is on
1211         service_feedback.waitForVoltageOn();
1212 
1213         // If pointing had changed, do calibration
1214         if (point)
1215         {
1216             console.out("  Calibration.");
1217 
1218             // Calibration (2% of 20')
1219             while (1)
1220             {
1221                 if (!takeRun("pedestal",         1000))  // 80 Hz  -> 10s
1222                     continue;
1223                 if (!takeRun("light-pulser-ext", 1000))  // 80 Hz  -> 10s
1224                     continue;
1225                 break;
1226             }
1227         }
1228 
1229         console.out("  Taking data: start [5min]");
1230 
1231         var len = 300;
1232         while (len>0)
1233         {
1234             var time = new Date();
1235             if (takeRun("data", -1, len)) // Take data (5min)
1236                 break;
1237 
1238             len -= parseInt((new Date()-time)/1000);
1239         }
1240 
1241         console.out("  Taking data: done");
1242         run++;
1243 
1244         continue; // case "DATA"
1245     }
1246 
1247     if (nextObs!=undefined && sub==obs.length-1)
1248         console.out("  Waiting for next observation scheduled for "+nextObs.start.toUTCString(),"");
1249 
1250     sub++;
1251 }
1252 
1253 sub_drsruns.close();
1254 
1255 // ================================================================
1256 // Comments and ToDo goes here
1257 // ================================================================
1258 
1259 // error handline : http://www.sitepoint.com/exceptional-exception-handling-in-javascript/
1260 // classes: http://www.phpied.com/3-ways-to-define-a-javascript-class/
1261 //
1262 // Arguments: TakeFirstDrsCalib
1263 // To be determined: How to stop the script without foreceful interruption?
1264