source: trunk/FACT++/src/EventBuilderWrapper.h@ 12951

Last change on this file since 12951 was 12908, checked in by tbretz, 13 years ago
Fixed a bug which caused crashes for runs without run-types, e.g. self triggered events.
File size: 45.1 KB
Line 
1#ifndef FACT_EventBuilderWrapper
2#define FACT_EventBuilderWrapper
3
4#include <sstream>
5
6#if BOOST_VERSION < 104400
7#if (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ > 4))
8#undef BOOST_HAS_RVALUE_REFS
9#endif
10#endif
11#include <boost/thread.hpp>
12#include <boost/filesystem.hpp>
13#include <boost/date_time/posix_time/posix_time_types.hpp>
14
15#include "DimWriteStatistics.h"
16
17#include "DataCalib.h"
18#include "DataWriteRaw.h"
19
20#ifdef HAVE_FITS
21#include "DataWriteFits.h"
22#else
23#define DataWriteFits DataWriteRaw
24#endif
25
26namespace ba = boost::asio;
27namespace bs = boost::system;
28namespace fs = boost::filesystem;
29
30using ba::ip::tcp;
31
32using namespace std;
33
34// ========================================================================
35
36#include "EventBuilder.h"
37
38extern "C" {
39 extern void StartEvtBuild();
40 extern int CloseRunFile(uint32_t runId, uint32_t closeTime, uint32_t maxEvt);
41}
42
43// ========================================================================
44
45class EventBuilderWrapper
46{
47public:
48 // FIXME
49 static EventBuilderWrapper *This;
50
51 MessageImp &fMsg;
52
53private:
54 boost::thread fThread;
55
56 enum CommandStates_t // g_runStat
57 {
58 kAbort = -2, // quit as soon as possible ('abort')
59 kExit = -1, // stop reading, quit when buffered events done ('exit')
60 kInitialize = 0, // 'initialize' (e.g. dim not yet started)
61 kHybernate = 1, // do nothing for long time ('hybernate') [wakeup within ~1sec]
62 kSleep = 2, // do nothing ('sleep') [wakeup within ~10msec]
63 kModeFlush = 10, // read data from camera, but skip them ('flush')
64 kModeTest = 20, // read data and process them, but do not write to disk ('test')
65 kModeFlag = 30, // read data, process and write all to disk ('flag')
66 kModeRun = 40, // read data, process and write selected to disk ('run')
67 };
68
69 enum
70 {
71 kCurrent = 0,
72 kTotal = 1,
73 kEventId = 2,
74 kTriggerId = 3,
75 };
76
77 FAD::FileFormat_t fFileFormat;
78
79 uint32_t fMaxRun;
80 uint32_t fLastOpened;
81 uint32_t fLastClosed;
82 uint32_t fNumEvts[4];
83
84 DimWriteStatistics fDimWriteStats;
85 DimDescribedService fDimRuns;
86 DimDescribedService fDimEvents;
87 DimDescribedService fDimRawData;
88 DimDescribedService fDimEventData;
89 DimDescribedService fDimFeedbackData;
90 DimDescribedService fDimFwVersion;
91 DimDescribedService fDimRunNumber;
92 DimDescribedService fDimStatus;
93 DimDescribedService fDimDNA;
94 DimDescribedService fDimTemperature;
95 DimDescribedService fDimPrescaler;
96 DimDescribedService fDimRefClock;
97 DimDescribedService fDimRoi;
98 DimDescribedService fDimDac;
99 DimDescribedService fDimDrsCalibration;
100 DimDescribedService fDimStatistics1;
101 DimDescribedService fDimStatistics2;
102 DimDescribedService fDimFileFormat;
103
104 bool fDebugStream;
105 bool fDebugRead;
106 bool fDebugLog;
107
108 string fPath;
109 uint32_t fRunNumber;
110
111protected:
112 int64_t InitRunNumber()
113 {
114 fRunNumber = 1000;
115
116 // Ensure that the night doesn't change during our check
117 const int check = Time().NightAsInt();
118
119 while (--fRunNumber>0)
120 {
121 const string name = DataProcessorImp::FormFileName(fPath, fRunNumber, "");
122
123 if (access((name+"bin").c_str(), F_OK) == 0)
124 break;
125 if (access((name+"fits").c_str(), F_OK) == 0)
126 break;
127 if (access((name+"drs.fits").c_str(), F_OK) == 0)
128 break;
129
130 }
131
132 if (check != Time().NightAsInt())
133 return InitRunNumber();
134
135 fRunNumber++;
136
137 if (fRunNumber==1000)
138 {
139 fMsg.Error("You have a file with run-number 1000 in "+fPath);
140 return -1;
141 }
142
143 ostringstream str;
144 str << "Set next run-number to " << fRunNumber << " determined from " << fPath;
145 fMsg.Message(str);
146
147 fMsg.Info(" ==> TODO: Crosscheck with database!");
148
149 return check;
150 }
151
152 int64_t InitRunNumber(const string &path)
153 {
154 if (!DimWriteStatistics::DoesPathExist(path, fMsg))
155 {
156 fMsg.Error("Data path "+path+" does not exist!");
157 return -1;
158 }
159
160 //const fs::path fullPath = fs::system_complete(fs::path(path));
161
162 fPath = path;
163
164 fDimWriteStats.SetCurrentFolder(fPath);
165
166 return InitRunNumber();
167 }
168
169public:
170 EventBuilderWrapper(MessageImp &imp) : fMsg(imp),
171 fFileFormat(FAD::kNone), fMaxRun(0), fLastOpened(0), fLastClosed(0),
172 fDimWriteStats ("FAD_CONTROL", imp),
173 fDimRuns ("FAD_CONTROL/RUNS", "I:5;C",
174 "Run files statistics"
175 "|stats[int]:num. of open run files, min run num., max run num., lastest opened or closed run"
176 "|file[string]:filename of last opened file"),
177 fDimEvents ("FAD_CONTROL/EVENTS", "I:4",
178 "Event counts"
179 "|evtsCount[int]:Num evts cur. run, total (all run), evt ID, trig. Num"),
180 fDimRawData ("FAD_CONTROL/RAW_DATA", "S:1;I:1;S:1;I:1;I:2;I:40;S:1440;S:160;F", ""),
181 fDimEventData ("FAD_CONTROL/EVENT_DATA", "F:1440;F:1440;F:1440;F:1440", ""),
182 fDimFeedbackData("FAD_CONTROL/FEEDBACK_DATA", "F:1440", ""),
183 fDimFwVersion ("FAD_CONTROL/FIRMWARE_VERSION", "F:42",
184 "Firmware version number of fad boards"
185 "|firmware[float]:Version number of firmware, for each board. 40=min, 41=max"),
186 fDimRunNumber ("FAD_CONTROL/RUN_NUMBER", "I:42",
187 "Run numbers coming from FAD boards"
188 "|runNumbers[int]:current run number of each FAD board. 40=min, 41=max"),
189 fDimStatus ("FAD_CONTROL/STATUS", "S:42",
190 "Status of FAD boards"
191 "|status[bitpattern]:Status of each FAD board. Maybe buggy"),
192 fDimDNA ("FAD_CONTROL/DNA", "X:40",
193 "DNA of FAD boards"
194 "|DNA[hex]:Hex identifier of each FAD board"),
195 fDimTemperature ("FAD_CONTROL/TEMPERATURE", "F:82",
196 "FADs temperatures"
197 "|temp[deg. C]:0 global min, 1-40 min, 41 global max, 42-81 max"),
198 fDimPrescaler ("FAD_CONTROL/PRESCALER", "S:42",
199 "Trigger generator prescaler of fad boards"
200 "|prescaler[int]:Trigger generator prescaler value, for each board"),
201 fDimRefClock ("FAD_CONTROL/REFERENCE_CLOCK", "I:42",
202 "Reference clock of FAD boards"
203 "|refClocks[t]:ref clocks of FAD boards. 40=min, 41=max"),
204 fDimRoi ("FAD_CONTROL/REGION_OF_INTEREST", "S:2", ""),
205 fDimDac ("FAD_CONTROL/DAC", "S:336",
206 "DAC counts of each FAD board"
207 "|DAC[int]:DAC counts, sequentally DAC 0 board 0, DAC 0 board 1... (plus min max)"),
208 fDimDrsCalibration("FAD_CONTROL/DRS_CALIBRATION", "I:1;I:3;F:1474560;F:1474560;F:1474560;F:1474560;F:1474560;F:1474560;F:163840;F:163840", ""),
209 fDimStatistics1 ("FAD_CONTROL/STATISTICS1", "I:3;I:5;X:4;I:3;I:3;I:40;I:1;I:2;C:40;I:40;I:40;X:40",
210 "Event Builder status for GUI display"
211 "|threadInfo[int]:Number of read, proc and writes"
212 "|bufferInfo[int]:Events in buffer, incomp., comp., tot., max past cycle, total"
213 "|memInfo[int]:total buf. mem, used mem, max used, max past cycle"
214 "|EvtCnt[int]:Number of events skipped, written, with errors"
215 "|badRoi[int]:Num boards with wrong ROI in event, run or board"
216 "|badRoiBoard[int]:Num boards with wrong ROI"
217 "|deltaT[ms]:Time in ms for rates"
218 "|rateNew[int]:Number of new start events received"
219 "|numConn[int]:Number of connections per board"
220 "|errConn[int]:IO errors per board"
221 "|rateBytes[int]:Bytes read this cycle"
222 "|totBytes[int]:Bytes read (counter)"),
223 fDimStatistics2 ("FAD_CONTROL/STATISTICS2", "I:1;I:280;X:40;I:40;I:4;I:4;I:2;I:2;I:3;C:40",
224 "Event Builder status, events oriented"
225 "|reset[int]:If increased, reset all counters"
226 "|numRead[int]:How often sucessful read from N sockets per loop"
227 "|gotByte[int]:number of bytes read per board"
228 "|gotErr[int]:number of com. errors per board"
229 "|evtStat[int]:number of evts read, completed, with errors, incomplete"
230 "|procStat[int]:num. of evts proc., w probs, acc. or rej. by SW trigger"
231 "|feedStat[int]:number of evts used or rejected by feedback system"
232 "|wrtStat[int]:number of evts written to disk, with errors"
233 "|runStat[int]:number of run opened, closed, with open or close errors"
234 "|numConn[int]:number of sockets successfully opened per board"),
235 fDimFileFormat ("FAD_CONTROL/FILE_FORMAT", "S:1", ""),
236 fDebugStream(false), fDebugRead(false), fDebugLog(false)
237 {
238 if (This)
239 throw logic_error("EventBuilderWrapper cannot be instantiated twice.");
240
241 This = this;
242
243 memset(fNumEvts, 0, sizeof(fNumEvts));
244 fDimEvents.Update(fNumEvts);
245
246 for (size_t i=0; i<40; i++)
247 ConnectSlot(i, tcp::endpoint());
248 }
249 virtual ~EventBuilderWrapper()
250 {
251 Abort();
252
253 // FIXME: Used timed_join and abort afterwards
254 // What's the maximum time the eb need to abort?
255 fThread.join();
256 //ffMsg.Info("EventBuilder stopped.");
257
258 for (vector<DataProcessorImp*>::iterator it=fFiles.begin(); it!=fFiles.end(); it++)
259 delete *it;
260 }
261
262 set<uint32_t> fIsRunStarted;
263 map<uint32_t, FAD::RunDescription> fExpectedRuns;
264
265 uint32_t StartNewRun(int64_t maxtime, int64_t maxevt, const pair<string, FAD::Configuration> &ref)
266 {
267 if (maxtime<=0 || maxtime>24*60*60)
268 maxtime = 24*60*60;
269 if (maxevt<=0 || maxevt>INT32_MAX)
270 maxevt = INT32_MAX;
271
272 const FAD::RunDescription descr =
273 {
274 uint32_t(maxtime),
275 uint32_t(maxevt),
276 ref.first,
277 ref.second,
278 };
279
280 // FIMXE: Maybe reset an event counter so that the mcp can count events?
281
282 fMsg.Info(" ==> TODO: Set a limit on the size of fExpectedRuns!");
283
284 fExpectedRuns[fRunNumber] = descr;
285 fIsRunStarted.insert(fRunNumber);
286 return fRunNumber++;
287 }
288
289 bool IsThreadRunning()
290 {
291 return !fThread.timed_join(boost::posix_time::microseconds(0));
292 }
293
294 void SetMaxMemory(unsigned int mb) const
295 {
296 /*
297 if (mb*1000000<GetUsedMemory())
298 {
299 // ffMsg.Warn("...");
300 return;
301 }*/
302
303 g_maxMem = size_t(mb)*1000000;
304 }
305
306 void StartThread(const vector<tcp::endpoint> &addr)
307 {
308 if (IsThreadRunning())
309 {
310 fMsg.Warn("Start - EventBuilder still running");
311 return;
312 }
313
314 fLastMessage.clear();
315
316 for (size_t i=0; i<40; i++)
317 ConnectSlot(i, addr[i]);
318
319 g_runStat = kModeRun;
320 g_maxProc = 3;
321
322 fMsg.Message("Starting EventBuilder thread");
323
324 fThread = boost::thread(StartEvtBuild);
325 }
326 void ConnectSlot(unsigned int i, const tcp::endpoint &addr)
327 {
328 if (i>39)
329 return;
330
331 if (addr==tcp::endpoint())
332 {
333 DisconnectSlot(i);
334 return;
335 }
336
337 g_port[i].sockAddr.sin_family = AF_INET;
338 g_port[i].sockAddr.sin_addr.s_addr = htonl(addr.address().to_v4().to_ulong());
339 g_port[i].sockAddr.sin_port = htons(addr.port());
340 // In this order
341 g_port[i].sockDef = 1;
342 }
343 void DisconnectSlot(unsigned int i)
344 {
345 if (i>39)
346 return;
347
348 g_port[i].sockDef = 0;
349 // In this order
350 g_port[i].sockAddr.sin_family = AF_INET;
351 g_port[i].sockAddr.sin_addr.s_addr = 0;
352 g_port[i].sockAddr.sin_port = 0;
353 }
354 void IgnoreSlot(unsigned int i)
355 {
356 if (i>39)
357 return;
358 if (g_port[i].sockAddr.sin_port==0)
359 return;
360
361 g_port[i].sockDef = -1;
362 }
363
364
365 void Abort()
366 {
367 fMsg.Message("Signal abort to EventBuilder thread...");
368 g_runStat = kAbort;
369 }
370
371 void ResetThread(bool soft)
372 {
373 /*
374 if (g_reset > 0)
375
376 * suspend reading
377 * reset = g_reset;
378 * g_reset=0
379
380 * reset% 10
381 == 0 leave event Buffers as they are
382 == 1 let all buffers drain (write (incomplete) events)
383 > 1 flush all buffers (do not write buffered events)
384
385 * (reset/10)%10
386 > 0 close all sockets and destroy them (also free the
387 allocated read-buffers)
388 recreate before resuming operation
389 [ this is more than just close/open that can be
390 triggered by e.g. close/open the base-socket ]
391
392 * (reset/100)%10
393 > 0 close all open run-files
394
395 * (reset/1000)
396 sleep so many seconds before resuming operation
397 (does not (yet) take into account time left when waiting
398 for buffers getting empty ...)
399
400 * resume_reading
401
402 */
403 fMsg.Message("Signal reset to EventBuilder thread...");
404 g_reset = soft ? 101 : 102;
405 }
406
407 void Exit()
408 {
409 fMsg.Message("Signal exit to EventBuilder thread...");
410 g_runStat = kExit;
411 }
412
413 /*
414 void Wait()
415 {
416 fThread.join();
417 ffMsg.Message("EventBuilder stopped.");
418 }*/
419
420 void Hybernate() const { g_runStat = kHybernate; }
421 void Sleep() const { g_runStat = kSleep; }
422 void FlushMode() const { g_runStat = kModeFlush; }
423 void TestMode() const { g_runStat = kModeTest; }
424 void FlagMode() const { g_runStat = kModeFlag; }
425 void RunMode() const { g_runStat = kModeRun; }
426
427 // FIXME: To be removed
428 void SetMode(int mode) const { g_runStat = mode; }
429
430 bool IsConnected(int i) const { return gi_NumConnect[i]==7; }
431 bool IsConnecting(int i) const { return !IsConnected(i) && !IsDisconnected(i); }
432 bool IsDisconnected(int i) const { return gi_NumConnect[i]<=0 && g_port[i].sockDef==0; }
433 int GetNumConnected(int i) const { return gi_NumConnect[i]; }
434 int GetNumFilesOpen() const { return fFiles.size(); }
435
436 /*
437 bool IsConnected(int i) const { return gi_NumConnect[i]>0; }
438 bool IsConnecting(int i) const { return !IsConnected(i) && !IsDisconnected(i); }
439 bool IsDisconnected(int i) const { return gi_NumConnect[i]<=0 && g_port[i].sockDef==0; }
440 int GetNumConnected(int i) const { return gi_NumConnect[i]; }
441 */
442
443 void SetIgnore(int i, bool b) const { if (g_port[i].sockDef!=0) g_port[i].sockDef=b?-1:1; }
444 bool IsIgnored(int i) const { return g_port[i].sockDef==-1; }
445
446 void SetOutputFormat(FAD::FileFormat_t f)
447 {
448 fFileFormat = f;
449 fDimFileFormat.Update(uint16_t(f));
450 if (fFileFormat==FAD::kCalib)
451 {
452 DataCalib::Restart();
453 DataCalib::Update(fDimDrsCalibration);
454 fMsg.Message("Resetted DRS calibration.");
455 }
456 }
457
458 virtual int ResetSecondaryDrsBaseline()
459 {
460 if (DataCalib::ResetTrgOff(fDimDrsCalibration))
461 {
462 fFileFormat = FAD::kCalib;
463 fDimFileFormat.Update(uint16_t(fFileFormat));
464 fMsg.Message("Resetted DRS calibration for secondary baseline.");
465 }
466 else
467 fMsg.Warn("Could not reset DRS calibration of secondary baseline.");
468
469 return 0;
470 }
471
472 void SetDebugLog(bool b) { fDebugLog = b; }
473
474 void SetDebugStream(bool b)
475 {
476 fDebugStream = b;
477 if (b)
478 return;
479
480 for (int i=0; i<40; i++)
481 {
482 if (!fDumpStream[i].is_open())
483 continue;
484
485 fDumpStream[i].close();
486
487 ostringstream name;
488 name << "socket_dump-" << setfill('0') << setw(2) << i << ".bin";
489 fMsg.Message("Closed file '"+name.str()+"'");
490 }
491 }
492
493 void SetDebugRead(bool b)
494 {
495 fDebugRead = b;
496 if (b || !fDumpRead.is_open())
497 return;
498
499 fDumpRead.close();
500 fMsg.Message("Closed file 'socket_events.txt'");
501 }
502
503// size_t GetUsedMemory() const { return gi_usedMem; }
504
505 void LoadDrsCalibration(const char *fname)
506 {
507 if (!DataCalib::ReadFits(fname, fMsg))
508 return;
509 fMsg.Info("Successfully loaded DRS calibration from "+string(fname));
510 DataCalib::Update(fDimDrsCalibration);
511 }
512
513 virtual int CloseOpenFiles() { CloseRunFile(0, 0, 0); return 0; }
514
515
516 /*
517 struct OpenFileToDim
518 {
519 int code;
520 char fileName[FILENAME_MAX];
521 };
522
523 SignalRunOpened(runid, filename);
524 // Send num open files
525 // Send runid, (more info about the run?), filename via dim
526
527 SignalEvtWritten(runid);
528 // Send num events written of newest file
529
530 SignalRunClose(runid);
531 // Send new num open files
532 // Send empty file-name if no file is open
533
534 */
535
536 // -------------- Mapped event builder callbacks ------------------
537
538 void UpdateRuns(const string &fname="")
539 {
540 uint32_t values[5] =
541 {
542 static_cast<uint32_t>(fFiles.size()),
543 0xffffffff,
544 0,
545 fLastOpened,
546 fLastClosed
547 };
548
549 for (vector<DataProcessorImp*>::const_iterator it=fFiles.begin();
550 it!=fFiles.end(); it++)
551 {
552 const DataProcessorImp *file = *it;
553
554 if (file->GetRunId()<values[1])
555 values[1] = file->GetRunId();
556
557 if (file->GetRunId()>values[2])
558 values[2] = file->GetRunId();
559 }
560
561 fMaxRun = values[2];
562
563 vector<char> data(sizeof(values)+fname.size()+1);
564 memcpy(data.data(), values, sizeof(values));
565 strcpy(data.data()+sizeof(values), fname.c_str());
566
567 fDimRuns.Update(data);
568 }
569
570 vector<DataProcessorImp*> fFiles;
571
572 FileHandle_t runOpen(uint32_t runid, RUN_HEAD *h, size_t)
573 {
574 fMsg.Info(" ==> TODO: Update run configuration in database!");
575
576 map<uint32_t,FAD::RunDescription>::iterator it = fExpectedRuns.begin();
577 while (it!=fExpectedRuns.end())
578 {
579 if (it->first<runid)
580 {
581 ostringstream str;
582 str << "runOpen - Missed run " << it->first << ".";
583 fMsg.Info(str);
584
585 fExpectedRuns.erase(it++);
586 continue;
587 }
588 if (it->first==runid)
589 break;
590 it++;
591 }
592
593 FAD::RunDescription desc;
594
595 if (it==fExpectedRuns.end())
596 {
597 ostringstream str;
598 str << "runOpen - Run " << runid << " wasn't expected (maybe manual triggers)";
599 fMsg.Warn(str);
600 }
601 else
602 {
603 desc = it->second;
604 fExpectedRuns.erase(it);
605 }
606
607 // Check if file already exists...
608 DataProcessorImp *file = 0;
609 switch (fFileFormat)
610 {
611 case FAD::kNone: file = new DataDump(fPath, runid, fMsg); break;
612 case FAD::kDebug: file = new DataDebug(fPath, runid, fMsg); break;
613 case FAD::kFits: file = new DataWriteFits(fPath, runid, fMsg); break;
614 case FAD::kRaw: file = new DataWriteRaw(fPath, runid, fMsg); break;
615 case FAD::kCalib: file = new DataCalib(fPath, runid, fDimDrsCalibration, fMsg); break;
616 }
617
618 try
619 {
620 if (!file->Open(h, desc))
621 return 0;
622 }
623 catch (const exception &e)
624 {
625 fMsg.Error("Exception trying to open file: "+string(e.what()));
626 return 0;
627 }
628
629 fFiles.push_back(file);
630
631 ostringstream str;
632 str << "Opened: " << file->GetFileName() << " (" << file->GetRunId() << ")";
633 fMsg.Info(str);
634
635 fDimWriteStats.FileOpened(file->GetFileName());
636
637 fLastOpened = runid;
638 UpdateRuns(file->GetFileName());
639
640 fNumEvts[kEventId] = 0;
641 fNumEvts[kTriggerId] = 0;
642
643 fNumEvts[kCurrent] = 0;
644 fDimEvents.Update(fNumEvts);
645 // fDimCurrentEvent.Update(uint32_t(0));
646
647 return reinterpret_cast<FileHandle_t>(file);
648 }
649
650 int runWrite(FileHandle_t handler, EVENT *e, size_t /*sz*/)
651 {
652 DataProcessorImp *file = reinterpret_cast<DataProcessorImp*>(handler);
653
654 if (!file->WriteEvt(e))
655 return -1;
656
657 if (file->GetRunId()==fMaxRun)
658 {
659 fNumEvts[kCurrent]++;
660 fNumEvts[kEventId] = e->EventNum;
661 fNumEvts[kTriggerId] = e->TriggerNum;
662 }
663
664 fNumEvts[kTotal]++;
665
666 static Time oldt(boost::date_time::neg_infin);
667 Time newt;
668 if (newt>oldt+boost::posix_time::seconds(1))
669 {
670 fDimEvents.Update(fNumEvts);
671 oldt = newt;
672 }
673
674
675 // ===> SignalEvtWritten(runid);
676 // Send num events written of newest file
677
678 /* close run runId (all all runs if runId=0) */
679 /* return: 0=close scheduled / >0 already closed / <0 does not exist */
680 //CloseRunFile(file->GetRunId(), time(NULL)+2) ;
681
682 return 0;
683 }
684
685 virtual void CloseRun(uint32_t /*runid*/) { }
686
687 int runClose(FileHandle_t handler, RUN_TAIL *tail, size_t)
688 {
689 fMsg.Info(" ==> TODO: Update run configuration in database!");
690
691 DataProcessorImp *file = reinterpret_cast<DataProcessorImp*>(handler);
692
693 const vector<DataProcessorImp*>::iterator it = find(fFiles.begin(), fFiles.end(), file);
694 if (it==fFiles.end())
695 {
696 ostringstream str;
697 str << "File handler (" << handler << ") requested to close by event builder doesn't exist.";
698 fMsg.Fatal(str);
699 return -1;
700 }
701
702 fFiles.erase(it);
703
704 fLastClosed = file->GetRunId();
705 CloseRun(fLastClosed);
706 UpdateRuns();
707
708 fDimEvents.Update(fNumEvts);
709
710 const bool rc = file->Close(tail);
711 if (!rc)
712 {
713 // Error message
714 }
715
716 ostringstream str;
717 str << "Closed: " << file->GetFileName() << " (" << file->GetRunId() << ")";
718 fMsg.Info(str);
719
720 delete file;
721
722 // ==> SignalRunClose(runid);
723 // Send new num open files
724 // Send empty file-name if no file is open
725
726 return rc ? 0 : -1;
727 }
728
729 ofstream fDumpStream[40];
730
731 void debugStream(int isock, void *buf, int len)
732 {
733 if (!fDebugStream)
734 return;
735
736 const int slot = isock/7;
737 if (slot<0 || slot>39)
738 return;
739
740 if (!fDumpStream[slot].is_open())
741 {
742 ostringstream name;
743 name << "socket_dump-" << setfill('0') << setw(2) << slot << ".bin";
744
745 fDumpStream[slot].open(name.str().c_str(), ios::app);
746 if (!fDumpStream[slot])
747 {
748 ostringstream str;
749 str << "Open file '" << name << "': " << strerror(errno) << " (errno=" << errno << ")";
750 fMsg.Error(str);
751
752 return;
753 }
754
755 fMsg.Message("Opened file '"+name.str()+"' for writing.");
756 }
757
758 fDumpStream[slot].write(reinterpret_cast<const char*>(buf), len);
759 }
760
761 ofstream fDumpRead; // Stream to possibly dump docket events
762
763 void debugRead(int isock, int ibyte, uint32_t event, uint32_t ftmevt, uint32_t runno, int state, uint32_t tsec, uint32_t tusec)
764 {
765 // isock = socketID (0-279)
766 // ibyte = #bytes gelesen
767 // event = eventId (oder 0 wenn noch nicht bekannt)
768 // state : 1=finished reading data
769 // 0=reading data
770 // -1=start reading data (header)
771 // -2=start reading data,
772 // eventId not known yet (too little data)
773 // tsec, tusec = time when reading seconds, microseconds
774 //
775 if (!fDebugRead || ibyte==0)
776 return;
777
778 if (!fDumpRead.is_open())
779 {
780 fDumpRead.open("socket_events.txt", ios::app);
781 if (!fDumpRead)
782 {
783 ostringstream str;
784 str << "Open file 'socket_events.txt': " << strerror(errno) << " (errno=" << errno << ")";
785 fMsg.Error(str);
786
787 return;
788 }
789
790 fMsg.Message("Opened file 'socket_events.txt' for writing.");
791
792 fDumpRead << "# START: " << Time().GetAsStr() << endl;
793 fDumpRead << "# state time_sec time_usec socket slot runno event_id trigger_id bytes_received" << endl;
794 }
795
796 fDumpRead
797 << setw(2) << state << " "
798 << setw(8) << tsec << " "
799 << setw(9) << tusec << " "
800 << setw(3) << isock << " "
801 << setw(2) << isock/7 << " "
802 << runno << " "
803 << event << " "
804 << ftmevt << " "
805 << ibyte << endl;
806 }
807
808 array<uint16_t,2> fVecRoi;
809
810 int eventCheck(uint32_t runNr, PEVNT_HEADER *fadhd, EVENT *event, int /*iboard*/)
811 {
812 /*
813 fadhd[i] ist ein array mit den 40 fad-headers
814 (falls ein board nicht gelesen wurde, ist start_package_flag =0 )
815
816 event ist die Struktur, die auch die write routine erhaelt;
817 darin sind im header die 'soll-werte' fuer z.B. eventID
818 als auch die ADC-Werte (falls Du die brauchst)
819
820 Wenn die routine einen negativen Wert liefert, wird das event
821 geloescht (nicht an die write-routine weitergeleitet [mind. im Prinzip]
822 */
823
824 const array<uint16_t,2> roi = {{ event->Roi, event->RoiTM }};
825
826 if (roi!=fVecRoi)
827 {
828 Update(fDimRoi, roi);
829 fVecRoi = roi;
830 }
831
832 const FAD::EventHeader *beg = reinterpret_cast<FAD::EventHeader*>(fadhd);
833 const FAD::EventHeader *end = reinterpret_cast<FAD::EventHeader*>(fadhd)+40;
834
835 // FIMXE: Compare with target configuration
836
837 for (const FAD::EventHeader *ptr=beg; ptr!=end; ptr++)
838 {
839 // FIXME: Compare with expectations!!!
840 if (ptr->fStartDelimiter==0)
841 {
842 if (ptr==beg)
843 beg++;
844 continue;
845 }
846
847 if (beg->fStatus != ptr->fStatus)
848 {
849 fMsg.Error("Inconsistency in FAD status detected.... closing run.");
850 CloseRunFile(runNr, 0, 0);
851 return -1;
852 }
853
854 if (beg->fRunNumber != ptr->fRunNumber)
855 {
856 fMsg.Error("Inconsistent run number detected.... closing run.");
857 CloseRunFile(runNr, 0, 0);
858 return -1;
859 }
860
861 /*
862 if (beg->fVersion != ptr->fVersion)
863 {
864 Error("Inconsist firmware version detected.... closing run.");
865 CloseRunFile(runNr, 0, 0);
866 break;
867 }
868 if (beg->fEventCounter != ptr->fEventCounter)
869 {
870 Error("Inconsist run number detected.... closing run.");
871 CloseRunFile(runNr, 0, 0);
872 break;
873 }
874 if (beg->fTriggerCounter != ptr->fTriggerCounter)
875 {
876 Error("Inconsist trigger number detected.... closing run.");
877 CloseRunFile(runNr, 0, 0);
878 break;
879 }*/
880
881 if (beg->fAdcClockPhaseShift != ptr->fAdcClockPhaseShift)
882 {
883 fMsg.Error("Inconsistent phase shift detected.... closing run.");
884 CloseRunFile(runNr, 0, 0);
885 return -1;
886 }
887
888 if (memcmp(beg->fDac, ptr->fDac, sizeof(beg->fDac)))
889 {
890 fMsg.Error("Inconsistent DAC values detected.... closing run.");
891 CloseRunFile(runNr, 0, 0);
892 return -1;
893 }
894
895 if (beg->fTriggerType != ptr->fTriggerType)
896 {
897 fMsg.Error("Inconsistent trigger type detected.... closing run.");
898 CloseRunFile(runNr, 0, 0);
899 return -1;
900 }
901 }
902
903 // check REFCLK_frequency
904 // check consistency with command configuration
905 // how to log errors?
906 // need gotNewRun/closedRun to know it is finished
907
908 return 0;
909 }
910
911 void SendRawData(PEVNT_HEADER */*fadhd*/, EVENT *event)
912 {
913 // Currently we send any event no matter what its trigger id is...
914 // To be changed.
915 static Time oldt(boost::date_time::neg_infin);
916 Time newt;
917
918 // FIXME: Only send events if the have newer run-numbers
919 if (newt<oldt+boost::posix_time::seconds(1))
920 return;
921
922 oldt = newt;
923
924 vector<char> data(sizeof(EVENT)+event->Roi*sizeof(float)*(1440+160));
925 memcpy(data.data(), event, sizeof(EVENT));
926
927 float *vec = reinterpret_cast<float*>(data.data()+sizeof(EVENT));
928
929 DataCalib::Apply(vec, event->Adc_Data, event->StartPix, event->Roi);
930 fDimRawData.Update(data);
931
932 DrsCalibrate::RemoveSpikes(vec, event->Roi);
933
934 vector<float> data2(1440*4); // Mean, RMS, Max, Pos
935 DrsCalibrate::GetPixelStats(data2.data(), vec, event->Roi);
936
937 fDimEventData.Update(data2);
938 }
939
940 void SendFeedbackData(PEVNT_HEADER *fadhd, EVENT *event)
941 {
942 if (!DataCalib::IsValid())
943 return;
944
945 // Workaround to find a valid header.....
946 const FAD::EventHeader *beg = reinterpret_cast<FAD::EventHeader*>(fadhd);
947 const FAD::EventHeader *end = reinterpret_cast<FAD::EventHeader*>(fadhd)+40;
948
949 // FIMXE: Compare with target configuration
950
951 const FAD::EventHeader *ptr=beg;
952 for (; ptr!=end; ptr++)
953 {
954 if (ptr->fStartDelimiter==0)
955 continue;
956
957 if (!ptr->HasTriggerLPext() && !ptr->HasTriggerLPint())
958 return;
959 }
960
961 if (ptr->fStartDelimiter==0)
962 return;
963
964 vector<float> data(event->Roi*1440);
965 DataCalib::Apply(data.data(), event->Adc_Data, event->StartPix, event->Roi);
966
967 DrsCalibrate::RemoveSpikes(data.data(), event->Roi);
968
969 vector<float> data2(1440); // Mean, RMS, Max, Pos, first, last
970 DrsCalibrate::GetPixelMax(data2.data(), data.data(), event->Roi, 0, event->Roi-1);
971
972 fDimFeedbackData.Update(data2);
973 }
974
975 int subProcEvt(int threadID, PEVNT_HEADER *fadhd, EVENT *event, int16_t /*iboard*/, void */*buffer*/)
976 {
977 switch (threadID)
978 {
979 case 0:
980 SendRawData(fadhd, event);
981 return 1;
982 case 1:
983 SendFeedbackData(fadhd, event);
984 return 2;
985 }
986 return 100;
987 }
988
989
990 bool IsRunStarted() const
991 {
992 const set<uint32_t>::const_iterator it = fIsRunStarted.find(fRunNumber-1);
993 return it==fIsRunStarted.end();// ? true : it->second.started;
994 }
995
996 uint32_t GetRunNumber() const
997 {
998 return fRunNumber;
999 }
1000
1001 void IncreaseRunNumber(uint32_t run)
1002 {
1003 if (run>fRunNumber)
1004 fRunNumber = run;
1005 }
1006
1007 void gotNewRun(uint32_t runnr, PEVNT_HEADER */*headers*/)
1008 {
1009 // This function is called even when writing is switched off
1010 set<uint32_t>::iterator it = fIsRunStarted.begin();
1011 while (it!=fIsRunStarted.end())
1012 {
1013 if (*it<runnr)
1014 {
1015 ostringstream str;
1016 str << "gotNewRun - Missed run " << *it << ".";
1017 fMsg.Info(str);
1018
1019 fIsRunStarted.erase(it++);
1020 continue;
1021 }
1022 if (*it==runnr)
1023 break;
1024 it++;
1025 }
1026 if (it==fIsRunStarted.end())
1027 {
1028 ostringstream str;
1029 str << "gotNewRun - Not waiting for run " << runnr << ".";
1030 fMsg.Warn(str);
1031 return;
1032 }
1033
1034 map<uint32_t,FAD::RunDescription>::iterator i2 = fExpectedRuns.find(runnr);
1035 if (i2==fExpectedRuns.end())
1036 {
1037 ostringstream str;
1038 str << "gotNewRun - Run " << runnr << " wasn't expected.";
1039 fMsg.Warn(str);
1040 return;
1041 }
1042
1043 CloseRunFile(runnr, time(NULL)+i2->second.maxtime, i2->second.maxevt);
1044 // return: 0=close scheduled / >0 already closed / <0 does not exist
1045
1046 // FIXME: Move configuration from expected runs to runs which will soon
1047 // be opened/closed
1048
1049 fIsRunStarted.erase(it);
1050 }
1051
1052 map<boost::thread::id, string> fLastMessage;
1053
1054 void factOut(int severity, int err, const char *message)
1055 {
1056 if (!fDebugLog && severity==99)
1057 return;
1058
1059 ostringstream str;
1060 //str << boost::this_thread::get_id() << " ";
1061 str << "EventBuilder(";
1062 if (err<0)
1063 str << "---";
1064 else
1065 str << err;
1066 str << "): " << message;
1067
1068 string &old = fLastMessage[boost::this_thread::get_id()];
1069
1070 if (str.str()==old)
1071 return;
1072 old = str.str();
1073
1074 fMsg.Update(str, severity);
1075 }
1076/*
1077 void factStat(int64_t *stat, int len)
1078 {
1079 if (len!=7)
1080 {
1081 fMsg.Warn("factStat received unknown number of values.");
1082 return;
1083 }
1084
1085 vector<int64_t> data(1, g_maxMem);
1086 data.insert(data.end(), stat, stat+len);
1087
1088 static vector<int64_t> last(8);
1089 if (data==last)
1090 return;
1091 last = data;
1092
1093 fDimStatistics.Update(data);
1094
1095 // len ist die Laenge des arrays.
1096 // array[4] enthaelt wieviele bytes im Buffer aktuell belegt sind; daran
1097 // kannst Du pruefen, ob die 100MB voll sind ....
1098
1099 ostringstream str;
1100 str
1101 << "Wait=" << stat[0] << " "
1102 << "Skip=" << stat[1] << " "
1103 << "Del=" << stat[2] << " "
1104 << "Tot=" << stat[3] << " "
1105 << "Mem=" << stat[4] << "/" << g_maxMem << " "
1106 << "Read=" << stat[5] << " "
1107 << "Conn=" << stat[6];
1108
1109 fMsg.Info(str);
1110 }
1111 */
1112
1113 void factStat(const EVT_STAT &stat)
1114 {
1115 fDimStatistics2.Update(stat);
1116 }
1117
1118 void factStat(const GUI_STAT &stat)
1119 {
1120 fDimStatistics1.Update(stat);
1121 }
1122
1123
1124 array<FAD::EventHeader, 40> fVecHeader;
1125
1126 template<typename T, class S>
1127 array<T, 42> Compare(const S *vec, const T *t)
1128 {
1129 const int offset = reinterpret_cast<const char *>(t) - reinterpret_cast<const char *>(vec);
1130
1131 const T *min = NULL;
1132 const T *val = NULL;
1133 const T *max = NULL;
1134
1135 array<T, 42> arr;
1136
1137 // bool rc = true;
1138 for (int i=0; i<40; i++)
1139 {
1140 const char *base = reinterpret_cast<const char*>(vec+i);
1141 const T *ref = reinterpret_cast<const T*>(base+offset);
1142
1143 arr[i] = *ref;
1144
1145 if (gi_NumConnect[i]!=7)
1146 {
1147 arr[i] = 0;
1148 continue;
1149 }
1150
1151 if (!val)
1152 {
1153 min = ref;
1154 val = ref;
1155 max = ref;
1156 }
1157
1158 if (*ref<*min)
1159 min = ref;
1160
1161 if (*ref>*max)
1162 max = ref;
1163
1164 // if (*val!=*ref)
1165 // rc = false;
1166 }
1167
1168 arr[40] = val ? *min : 1;
1169 arr[41] = val ? *max : 0;
1170
1171 return arr;
1172 }
1173
1174 template<typename T>
1175 array<T, 42> CompareBits(const FAD::EventHeader *h, const T *t)
1176 {
1177 const int offset = reinterpret_cast<const char *>(t) - reinterpret_cast<const char *>(h);
1178
1179 T val = 0;
1180 T rc = 0;
1181
1182 array<T, 42> vec;
1183
1184 bool first = true;
1185
1186 for (int i=0; i<40; i++)
1187 {
1188 const char *base = reinterpret_cast<const char*>(&fVecHeader[i]);
1189 const T *ref = reinterpret_cast<const T*>(base+offset);
1190
1191 vec[i+2] = *ref;
1192
1193 if (gi_NumConnect[i]!=7)
1194 {
1195 vec[i+2] = 0;
1196 continue;
1197 }
1198
1199 if (first)
1200 {
1201 first = false;
1202 val = *ref;
1203 rc = 0;
1204 }
1205
1206 rc |= val^*ref;
1207 }
1208
1209 vec[0] = rc;
1210 vec[1] = val;
1211
1212 return vec;
1213 }
1214
1215 template<typename T, size_t N>
1216 void Update(DimDescribedService &svc, const array<T, N> &data, int n=N)
1217 {
1218// svc.setQuality(vec[40]<=vec[41]);
1219 svc.setData(const_cast<T*>(data.data()), sizeof(T)*n);
1220 svc.Update();
1221 }
1222
1223 template<typename T>
1224 void Print(const char *name, const pair<bool,array<T, 43>> &data)
1225 {
1226 cout << name << "|" << data.first << "|" << data.second[1] << "|" << data.second[0] << "<x<" << data.second[1] << ":";
1227 for (int i=0; i<40;i++)
1228 cout << " " << data.second[i+3];
1229 cout << endl;
1230 }
1231
1232 vector<uint> fNumConnected;
1233
1234 void debugHead(int /*socket*/, const FAD::EventHeader &h)
1235 {
1236 const uint16_t id = h.Id();
1237 if (id>39)
1238 return;
1239
1240 if (fNumConnected.size()!=40)
1241 fNumConnected.resize(40);
1242
1243 const vector<uint> con(gi_NumConnect, gi_NumConnect+40);
1244
1245 const bool changed = con!=fNumConnected || !IsThreadRunning();
1246
1247 fNumConnected = con;
1248
1249 const FAD::EventHeader old = fVecHeader[id];
1250 fVecHeader[id] = h;
1251
1252 if (old.fVersion != h.fVersion || changed)
1253 {
1254 const array<uint16_t,42> ver = Compare(&fVecHeader[0], &fVecHeader[0].fVersion);
1255
1256 array<float,42> data;
1257 for (int i=0; i<42; i++)
1258 {
1259 ostringstream str;
1260 str << (ver[i]>>8) << '.' << (ver[i]&0xff);
1261 data[i] = stof(str.str());
1262 }
1263 Update(fDimFwVersion, data);
1264 }
1265
1266 if (old.fRunNumber != h.fRunNumber || changed)
1267 {
1268 const array<uint32_t,42> run = Compare(&fVecHeader[0], &fVecHeader[0].fRunNumber);
1269 fDimRunNumber.Update(run);
1270 }
1271
1272 if (old.fTriggerGeneratorPrescaler != h.fTriggerGeneratorPrescaler || changed)
1273 {
1274 const array<uint16_t,42> pre = Compare(&fVecHeader[0], &fVecHeader[0].fTriggerGeneratorPrescaler);
1275 fDimPrescaler.Update(pre);
1276 }
1277
1278 if (old.fDNA != h.fDNA || changed)
1279 {
1280 const array<uint64_t,42> dna = Compare(&fVecHeader[0], &fVecHeader[0].fDNA);
1281 Update(fDimDNA, dna, 40);
1282 }
1283
1284 if (old.fStatus != h.fStatus || changed)
1285 {
1286 const array<uint16_t,42> sts = CompareBits(&fVecHeader[0], &fVecHeader[0].fStatus);
1287 Update(fDimStatus, sts);
1288 }
1289
1290 if (memcmp(old.fDac, h.fDac, sizeof(h.fDac)) || changed)
1291 {
1292 array<uint16_t, FAD::kNumDac*42> dacs;
1293
1294 for (int i=0; i<FAD::kNumDac; i++)
1295 {
1296 const array<uint16_t, 42> dac = Compare(&fVecHeader[0], &fVecHeader[0].fDac[i]);
1297 memcpy(&dacs[i*42], &dac[0], sizeof(uint16_t)*42);
1298 }
1299
1300 Update(fDimDac, dacs);
1301 }
1302
1303 // -----------
1304
1305 static Time oldt(boost::date_time::neg_infin);
1306 Time newt;
1307
1308 if (newt>oldt+boost::posix_time::seconds(1))
1309 {
1310 oldt = newt;
1311
1312 // --- RefClock
1313
1314 const array<uint32_t,42> clk = Compare(&fVecHeader[0], &fVecHeader[0].fFreqRefClock);
1315 Update(fDimRefClock, clk);
1316
1317 // --- Temperatures
1318
1319 const array<int16_t,42> tmp[4] =
1320 {
1321 Compare(&fVecHeader[0], &fVecHeader[0].fTempDrs[0]), // 0-39:val, 40:min, 41:max
1322 Compare(&fVecHeader[0], &fVecHeader[0].fTempDrs[1]), // 0-39:val, 40:min, 41:max
1323 Compare(&fVecHeader[0], &fVecHeader[0].fTempDrs[2]), // 0-39:val, 40:min, 41:max
1324 Compare(&fVecHeader[0], &fVecHeader[0].fTempDrs[3]) // 0-39:val, 40:min, 41:max
1325 };
1326
1327 vector<int16_t> data;
1328 data.reserve(82);
1329 data.push_back(tmp[0][40]); // min: 0
1330 data.insert(data.end(), tmp[0].data(), tmp[0].data()+40); // val: 1-40
1331 data.push_back(tmp[0][41]); // max: 41
1332 data.insert(data.end(), tmp[0].data(), tmp[0].data()+40); // val: 42-81
1333
1334 for (int j=1; j<=3; j++)
1335 {
1336 const array<int16_t,42> &ref = tmp[j];
1337
1338 // Gloabl min
1339 if (ref[40]<data[0]) // 40=min
1340 data[0] = ref[40];
1341
1342 // Global max
1343 if (ref[41]>data[41]) // 41=max
1344 data[41] = ref[41];
1345
1346 for (int i=0; i<40; i++)
1347 {
1348 // min per board
1349 if (ref[i]<data[i+1]) // data: 1-40
1350 data[i+1] = ref[i]; // ref: 0-39
1351
1352 // max per board
1353 if (ref[i]>data[i+42]) // data: 42-81
1354 data[i+42] = ref[i]; // ref: 0-39
1355 }
1356
1357
1358 }
1359
1360 vector<float> deg(82); // 0: global min, 1-40: min
1361 for (int i=0; i<82; i++) // 41: global max, 42-81: max
1362 deg[i] = data[i]/16.;
1363 fDimTemperature.Update(deg);
1364 }
1365 }
1366};
1367
1368EventBuilderWrapper *EventBuilderWrapper::This = 0;
1369
1370// ----------- Event builder callbacks implementation ---------------
1371extern "C"
1372{
1373 FileHandle_t runOpen(uint32_t irun, RUN_HEAD *runhd, size_t len)
1374 {
1375 return EventBuilderWrapper::This->runOpen(irun, runhd, len);
1376 }
1377
1378 int runWrite(FileHandle_t fileId, EVENT *event, size_t len)
1379 {
1380 return EventBuilderWrapper::This->runWrite(fileId, event, len);
1381 }
1382
1383 int runClose(FileHandle_t fileId, RUN_TAIL *runth, size_t len)
1384 {
1385 return EventBuilderWrapper::This->runClose(fileId, runth, len);
1386 }
1387
1388 // -----
1389
1390 //void *runStart(uint32_t /*irun*/, RUN_HEAD */*runhd*/, size_t /*len*/)
1391 //{
1392 // return NULL;
1393 //}
1394
1395 int subProcEvt(int threadID, PEVNT_HEADER *fadhd, EVENT *event, int16_t mboard, void *runPtr)
1396 {
1397 return EventBuilderWrapper::This->subProcEvt(threadID, fadhd, event, mboard, runPtr);
1398 }
1399
1400 int runEnd(uint32_t, void */*runPtr*/)
1401 {
1402 return 0;
1403 }
1404
1405 // -----
1406
1407 int eventCheck(uint32_t runNr, PEVNT_HEADER *fadhd, EVENT *event, int mboard)
1408 {
1409 return EventBuilderWrapper::This->eventCheck(runNr, fadhd, event, mboard);
1410 }
1411
1412 void gotNewRun(uint32_t runnr, PEVNT_HEADER *headers)
1413 {
1414 return EventBuilderWrapper::This->gotNewRun(runnr, headers);
1415 }
1416
1417 // -----
1418
1419 void factOut(int severity, int err, const char *message)
1420 {
1421 EventBuilderWrapper::This->factOut(severity, err, message);
1422 }
1423
1424 void factStat(GUI_STAT stat)
1425 {
1426 EventBuilderWrapper::This->factStat(stat);
1427 }
1428
1429 void factStatNew(EVT_STAT stat)
1430 {
1431 EventBuilderWrapper::This->factStat(stat);
1432 }
1433
1434 // ------
1435
1436 void debugHead(int socket, int/*board*/, void *buf)
1437 {
1438 const FAD::EventHeader &h = *reinterpret_cast<FAD::EventHeader*>(buf);
1439 EventBuilderWrapper::This->debugHead(socket, h);
1440 }
1441
1442 void debugStream(int isock, void *buf, int len)
1443 {
1444 return EventBuilderWrapper::This->debugStream(isock, buf, len);
1445 }
1446
1447 void debugRead(int isock, int ibyte, int32_t event, int32_t ftmevt, int32_t runno, int state, uint32_t tsec, uint32_t tusec)
1448 {
1449 EventBuilderWrapper::This->debugRead(isock, ibyte, event, ftmevt, runno, state, tsec, tusec);
1450 }
1451}
1452
1453#endif
Note: See TracBrowser for help on using the repository browser.