source: ntrip/trunk/BNC/src/latencychecker.cpp@ 10541

Last change on this file since 10541 was 10517, checked in by stuerze, 4 months ago

minor changes to allow re-connect check also if no latency check is enabled

File size: 18.3 KB
RevLine 
[1558]1// Part of BNC, a utility for retrieving decoding and
2// converting GNSS data streams from NTRIP broadcasters.
3//
4// Copyright (C) 2007
5// German Federal Agency for Cartography and Geodesy (BKG)
6// http://www.bkg.bund.de
7// Czech Technical University Prague, Department of Geodesy
8// http://www.fsv.cvut.cz
9//
10// Email: euref-ip@bkg.bund.de
11//
12// This program is free software; you can redistribute it and/or
13// modify it under the terms of the GNU General Public License
14// as published by the Free Software Foundation, version 2.
15//
16// This program is distributed in the hope that it will be useful,
17// but WITHOUT ANY WARRANTY; without even the implied warranty of
18// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19// GNU General Public License for more details.
20//
21// You should have received a copy of the GNU General Public License
22// along with this program; if not, write to the Free Software
23// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24
25/* -------------------------------------------------------------------------
26 * BKG NTRIP Client
27 * -------------------------------------------------------------------------
28 *
29 * Class: latencyChecker
30 *
31 * Purpose: Check incoming GNSS data for latencies, gaps etc.
32 *
33 * Author: G. Weber
34 *
35 * Created: 02-Feb-2009
36 *
[8011]37 * Changes:
[1558]38 *
39 * -----------------------------------------------------------------------*/
40
[1564]41#include <iostream>
[1578]42
[1577]43#ifdef WIN32
[9958]44#include <windows.h>
[1578]45#else
46#include <unistd.h>
[1577]47#endif
[1564]48
[1558]49#include "latencychecker.h"
[5070]50#include "bnccore.h"
[1558]51#include "bncutils.h"
52#include "bncsettings.h"
53
[1564]54using namespace std;
55
[1558]56// Constructor
57//////////////////////////////////////////////////////////////////////////////
58latencyChecker::latencyChecker(QByteArray staID) {
59
60 _staID = staID;
61
[8011]62 connect(this, SIGNAL(newMessage(QByteArray,bool)),
[5068]63 BNC_CORE, SLOT(slotMessage(const QByteArray,bool)));
[1558]64
65 bncSettings settings;
66
67 // Notice threshold
68 // ----------------
[7333]69 QString adviseObsRate = settings.value("adviseObsRate").toString();
[1558]70 _inspSegm = 0;
[8011]71 if ( adviseObsRate.isEmpty() ) {
72 _inspSegm = 0;
[1558]73 }
[8011]74 else if ( adviseObsRate.indexOf("5 Hz") != -1 ) {
75 _inspSegm = 20;
[1558]76 }
[8011]77 else if ( adviseObsRate.indexOf("1 Hz") != -1 ) {
78 _inspSegm = 10;
[1558]79 }
[8011]80 else if ( adviseObsRate.indexOf("0.5 Hz") != -1 ) {
81 _inspSegm = 20;
[1558]82 }
[8011]83 else if ( adviseObsRate.indexOf("0.2 Hz") != -1 ) {
84 _inspSegm = 40;
[1558]85 }
[8011]86 else if ( adviseObsRate.indexOf("0.1 Hz") != -1 ) {
87 _inspSegm = 50;
[1558]88 }
89 _adviseFail = settings.value("adviseFail").toInt();
90 _adviseReco = settings.value("adviseReco").toInt();
91 _adviseScript = settings.value("adviseScript").toString();
92 expandEnvVar(_adviseScript);
93
94 // Latency interval/average
95 // ------------------------
[10517]96 _miscIntr = 300;
[7422]97 QString miscIntr = settings.value("miscIntr").toString();
[8011]98 if ( miscIntr.isEmpty() ) {
[10517]99 _miscIntr = 300;
[1558]100 }
[8011]101 else if ( miscIntr.indexOf("2 sec") != -1 ) {
102 _miscIntr = 2;
[1558]103 }
[8011]104 else if ( miscIntr.indexOf("10 sec") != -1 ) {
105 _miscIntr = 10;
[1558]106 }
[8011]107 else if ( miscIntr.indexOf("1 min") != -1 ) {
108 _miscIntr = 60;
[1558]109 }
[8011]110 else if ( miscIntr.left(5).indexOf("5 min") != -1 ) {
111 _miscIntr = 300;
[1558]112 }
[8011]113 else if ( miscIntr.indexOf("15 min") != -1 ) {
114 _miscIntr = 900;
[1558]115 }
[8011]116 else if ( miscIntr.indexOf("1 hour") != -1 ) {
117 _miscIntr = 3600;
[1558]118 }
[8011]119 else if ( miscIntr.indexOf("6 hours") != -1 ) {
120 _miscIntr = 21600;
[1558]121 }
[8011]122 else if ( miscIntr.indexOf("1 day") != -1 ) {
123 _miscIntr = 86400;
[1558]124 }
125
126 // RTCM message types
127 // ------------------
128 _checkMountPoint = settings.value("miscMount").toString();
129
130 // Initialize private members
131 // --------------------------
132 _wrongEpoch = false;
[1568]133 _checkSeg = false;
[1558]134 _numSucc = 0;
135 _secSucc = 0;
136 _secFail = 0;
[1568]137 _initPause = 0;
[1558]138 _currPause = 0;
[8082]139 _endCorrupt = false;
140 _begCorrupt = false;
141 _fromCorrupt = false;
[1558]142
[1568]143 _checkTime = QDateTime::currentDateTime();
[6805]144 _decodeSucc = QDateTime::currentDateTime();
[6769]145
[6805]146 _decodeStop = QDateTime::currentDateTime();
147 _begDateTimeOut = QDateTime::currentDateTime();
148 _endDateTimeOut = QDateTime::currentDateTime();
[6820]149 _fromReconnect = false;
[1568]150
[6820]151 _decodeStopCorr = QDateTime::currentDateTime();
152 _begDateTimeCorr = QDateTime::currentDateTime();
153 _endDateTimeCorr = QDateTime::currentDateTime();
[1558]154}
155
156// Destructor
157//////////////////////////////////////////////////////////////////////////////
158latencyChecker::~latencyChecker() {
159}
160
[1568]161// Perform 'Begin outage' check
[1558]162//////////////////////////////////////////////////////////////////////////////
[1568]163void latencyChecker::checkReconnect() {
[1572]164
[6768]165 if (_inspSegm == 0) { return;}
[6758]166
[1572]167 // Begin outage threshold
168 // ----------------------
[6805]169 if (!_fromReconnect) {
170 _endDateTimeOut = QDateTime::currentDateTime();
[8011]171 }
[6805]172 _fromReconnect = true;
173
[1572]174 if ( _decodeStop.isValid() ) {
[6805]175 _begDateTimeOut = QDateTime::currentDateTime();
176 if ( _endDateTimeOut.secsTo(QDateTime::currentDateTime()) > _adviseFail * 60 ) {
177 _begDateOut = _endDateTimeOut.toUTC().date().toString("yy-MM-dd");
178 _begTimeOut = _endDateTimeOut.toUTC().time().toString("hh:mm:ss");
179 emit(newMessage((_staID + ": Failure threshold exceeded, outage since "
[8204]180 + _begDateOut + " " + _begTimeOut + " UTC").toLatin1(), true));
[6807]181 callScript(("Begin_Outage "
[8204]182 + _begDateOut + " " + _begTimeOut + " UTC").toLatin1());
[1572]183 _decodeStop.setDate(QDate());
184 _decodeStop.setTime(QTime());
[6805]185 _decodeStart = QDateTime::currentDateTime();
[1572]186 }
187 }
[1568]188}
189
190// Perform Corrupt and 'End outage' check
191//////////////////////////////////////////////////////////////////////////////
[1562]192void latencyChecker::checkOutage(bool decoded) {
[1558]193
[1568]194 if (_inspSegm == 0) { return;}
[1558]195
[1568]196 if (decoded) { _numSucc += 1; }
[1558]197
[1568]198 if (!_checkPause.isValid() || _checkPause.secsTo(QDateTime::currentDateTime()) >= _currPause ) {
199 if (!_checkSeg) {
200 if ( _checkTime.secsTo(QDateTime::currentDateTime()) > _inspSegm ) {
201 _checkSeg = true;
[1558]202 }
203 }
[1568]204
205 // Check - once per inspect segment
206 // --------------------------------
207 if (_checkSeg) {
208
209 _checkTime = QDateTime::currentDateTime();
210
211 if (_numSucc > 0) {
212 _secSucc += _inspSegm;
[6820]213 _secFail = 0;
[1568]214 _decodeSucc = QDateTime::currentDateTime();
215 if (_secSucc > _adviseReco * 60) {
216 _secSucc = _adviseReco * 60 + 1;
217 }
218 _numSucc = 0;
219 _currPause = _initPause;
220 _checkPause.setDate(QDate());
221 _checkPause.setTime(QTime());
[1558]222 }
223 else {
[1568]224 _secFail += _inspSegm;
225 _secSucc = 0;
[6805]226 if (_secFail > _adviseFail * 60) {
[1568]227 _secFail = _adviseFail * 60 + 1;
[1558]228 }
[1568]229 if (!_checkPause.isValid()) {
230 _checkPause = QDateTime::currentDateTime();
231 }
232 else {
233 _checkPause.setDate(QDate());
234 _checkPause.setTime(QTime());
235 _secFail = _secFail + _currPause - _inspSegm;
236 _currPause = _currPause * 2;
237 if (_currPause > 960) {
238 _currPause = 960;
239 }
240 }
[1558]241 }
[6805]242
[6820]243 // Begin corrupt threshold
244 // -----------------------
245 if (_secSucc > 0) {
246 _endDateTimeCorr = QDateTime::currentDateTime();
[6805]247 }
[1568]248
[6820]249 if (_secFail > 0) {
250 _begDateTimeCorr = QDateTime::currentDateTime();
251 }
252
253 if ( _decodeStopCorr.isValid() ) {
254 _begDateTimeCorr = QDateTime::currentDateTime();
255 if ( _endDateTimeCorr.secsTo(QDateTime::currentDateTime()) > _adviseFail * 60 ) {
256 _begDateCorr = _endDateTimeCorr.toUTC().date().toString("yy-MM-dd");
257 _begTimeCorr = _endDateTimeCorr.toUTC().time().toString("hh:mm:ss");
258 emit(newMessage((_staID + ": Failure threshold exceeded, corrupted since "
[8204]259 + _begDateCorr + " " + _begTimeCorr + " UTC").toLatin1(), true));
[6805]260 callScript(("Begin_Corrupted "
[8204]261 + _begDateCorr + " " + _begTimeCorr + " UTC").toLatin1());
[1568]262 _secSucc = 0;
263 _numSucc = 0;
[6820]264 _decodeStopCorr.setDate(QDate());
265 _decodeStopCorr.setTime(QTime());
266 _decodeStartCorr = QDateTime::currentDateTime();
[1568]267 }
[8011]268 }
[6820]269 else {
270
271 // End corrupt threshold
272 // ---------------------
273 if ( _decodeStartCorr.isValid() ) {
274 _endDateTimeCorr = QDateTime::currentDateTime();
275 if ( _begDateTimeCorr.secsTo(QDateTime::currentDateTime()) > _adviseReco * 60 ) {
276 _endDateCorr = _begDateTimeCorr.toUTC().date().toString("yy-MM-dd");
277 _endTimeCorr = _begDateTimeCorr.toUTC().time().toString("hh:mm:ss");
278 emit(newMessage((_staID + ": Recovery threshold exceeded, corruption ended "
[8204]279 + _endDateCorr + " " + _endTimeCorr + " UTC").toLatin1(), true));
[6820]280 callScript(("End_Corrupted "
281 + _endDateCorr + " " + _endTimeCorr + " UTC Begin was "
[8204]282 + _begDateCorr + " " + _begTimeCorr + " UTC").toLatin1());
[6820]283 _decodeStartCorr.setDate(QDate());
284 _decodeStartCorr.setTime(QTime());
285 _decodeStopCorr = QDateTime::currentDateTime();
286 _secFail = 0;
287 }
288 }
[1558]289 }
[1568]290 _checkSeg = false;
[1558]291 }
292 }
[1568]293
[6805]294 // End outage threshold
295 // --------------------
[6758]296 if (_fromReconnect) {
[6805]297 _begDateTimeOut = QDateTime::currentDateTime();
[8011]298 }
[6805]299 _fromReconnect = false;
[6758]300
[1568]301 if ( _decodeStart.isValid() ) {
[6805]302 _endDateTimeOut = QDateTime::currentDateTime();
303 if ( _begDateTimeOut.secsTo(QDateTime::currentDateTime()) > _adviseReco * 60 ) {
304 _endDateOut = _begDateTimeOut.toUTC().date().toString("yy-MM-dd");
305 _endTimeOut = _begDateTimeOut.toUTC().time().toString("hh:mm:ss");
306 emit(newMessage((_staID + ": Recovery threshold exceeded, outage ended "
[8204]307 + _endDateOut + " " + _endTimeOut + " UTC").toLatin1(), true));
[6805]308 callScript(("End_Outage "
309 + _endDateOut + " " + _endTimeOut + " UTC Begin was "
[8204]310 + _begDateOut + " " + _begTimeOut + " UTC").toLatin1());
[1568]311 _decodeStart.setDate(QDate());
312 _decodeStart.setTime(QTime());
[6805]313 _decodeStop = QDateTime::currentDateTime();
[1558]314 }
315 }
[1568]316}
317
[1562]318// Perform latency checks (observations)
319//////////////////////////////////////////////////////////////////////////////
[6137]320void latencyChecker::checkObsLatency(const QList<t_satObs>& obsList) {
[1562]321
[7422]322 if (_miscIntr > 0 ) {
[8271]323 t_latency& l = _lObs;
[9277]324 l._type = "Observations";
[6137]325 QListIterator<t_satObs> it(obsList);
[1978]326 while (it.hasNext()) {
[6137]327 const t_satObs& obs = it.next();
[8011]328 bool wrongObservationEpoch = checkForWrongObsEpoch(obs._time);
[8372]329 l._newSec = static_cast<int>(nint(obs._time.gpssec()*10));
[8082]330 if (l._newSec > l._oldSec && !wrongObservationEpoch) {
[8374]331 if (l._newSec % (_miscIntr * 10) < l._oldSec % (_miscIntr * 10)) {
[8082]332 if (l._numLat > 0) {
333 if (l._meanDiff > 0.0) {
[1978]334 if ( _checkMountPoint == _staID || _checkMountPoint == "ALL" ) {
[8082]335 emit( newMessage(QString("%1 %2: Mean latency %3 sec, min %4, max %5, rms %6, %7 epochs, %8 gaps")
[1558]336 .arg(_staID.data())
[8082]337 .arg(l._type.data())
[9277]338 .arg(int(l._sumLat/l._numLat*100)/100., 4, 'f', 2)
339 .arg(int(l._minLat*100)/100., 4, 'f', 2)
340 .arg(int(l._maxLat*100)/100., 4, 'f', 2)
341 .arg(int((sqrt(l._sumLatQ / l._numLat))*100)/100., 4, 'f', 2)
[8082]342 .arg(l._numLat)
343 .arg(l._numGaps)
[8204]344 .toLatin1(), true) );
[1978]345 }
346 } else {
347 if ( _checkMountPoint == _staID || _checkMountPoint == "ALL" ) {
[8082]348 emit( newMessage(QString("%1 %2: Mean latency %3 sec, min %4, max %5, rms %6, %7 epochs")
[1558]349 .arg(_staID.data())
[8082]350 .arg(l._type.data())
[9277]351 .arg(int(l._sumLat/l._numLat*100)/100., 4, 'f', 2)
352 .arg(int(l._minLat*100)/100., 4, 'f', 2)
353 .arg(int(l._maxLat*100)/100., 4, 'f', 2)
354 .arg(int((sqrt(l._sumLatQ / l._numLat))*100)/100., 4, 'f', 2)
[8082]355 .arg(l._numLat)
[8204]356 .toLatin1(), true) );
[1558]357 }
358 }
[8673]359 l._meanDiff = l._diffSec / l._numLat;
360 l.init();
[1558]361 }
[1978]362 }
[8082]363 if (l._followSec) {
364 l._diffSec += l._newSec - l._oldSec;
[8673]365 if (l._meanDiff > 0.0) {
[8082]366 if (l._newSec - l._oldSec > 1.5 * l._meanDiff) {
367 l._numGaps += 1;
[1558]368 }
369 }
[1978]370 }
[1558]371
[1978]372 // Compute the observations latency
373 // --------------------------------
[6137]374 int week;
375 double sec;
[1978]376 currentGPSWeeks(week, sec);
377 const double secPerWeek = 7.0 * 24.0 * 3600.0;
[6137]378 if (week < int(obs._time.gpsw())) {
[1978]379 week += 1;
380 sec -= secPerWeek;
[1558]381 }
[6137]382 if (week > int(obs._time.gpsw())) {
[1978]383 week -= 1;
384 sec += secPerWeek;
385 }
[8082]386 l._curLat = sec - obs._time.gpssec();
387 l._sumLat += l._curLat;
388 l._sumLatQ += l._curLat * l._curLat;
389 if (l._curLat < l._minLat) {
390 l._minLat = l._curLat;
[1978]391 }
[8082]392 if (l._curLat >= l._maxLat) {
393 l._maxLat = l._curLat;
[1978]394 }
[8082]395 l._numLat += 1;
396 l._followSec = true;
[1558]397 }
[8271]398 l._oldSec = l._newSec;
[1558]399 }
[8082]400 _lObs = l;
401 setCurrentLatency(l._curLat);
[1558]402 }
403}
404
[1562]405// Perform latency checks (corrections)
406//////////////////////////////////////////////////////////////////////////////
[8082]407void latencyChecker::checkCorrLatency(int corrGPSEpochTime, int type) {
[1566]408 if (corrGPSEpochTime < 0) {
[8271]409 return;
[1562]410 }
[8082]411 t_latency& l = _lOrb; // init
412 switch (type) {
413 case 1057: case 1063: case 1240: case 1246: case 1252: case 1258:
[9124]414 l = _lOrb;
[9277]415 l._type = "RtcmSsrOrbit";
[8082]416 break;
417 case 1058: case 1064: case 1241: case 1247: case 1253: case 1259:
[9124]418 l = _lClk;
[9277]419 l._type = "RtcmSsrClock";
[8082]420 break;
421 case 1060: case 1066: case 1243: case 1249: case 1255: case 1261:
[9124]422 l = _lClkOrb;
423 l._type = "RtcmSsrClock&Orbit";
[8082]424 break;
425 case 1059: case 1065: case 1242: case 1248: case 1254: case 1260:
[9124]426 l = _lCb;
[9277]427 l._type = "RtcmSsrCodeBiases";
[8082]428 break;
[9124]429 case 1265: case 1266: case 1267: case 1268: case 1269: case 1270:
430 l = _lPb;
431 l._type = "RtcmSsrPhaseBiases";
[8082]432 break;
433 case 1264:
[9124]434 l = _lVtec;
[9277]435 l._type = "RtcmSsrVTEC";
[8082]436 break;
437 case 1061: case 1067: case 1244: case 1250: case 1256: case 1262:
[9124]438 l = _lUra;
[9277]439 l._type = "RtcmSsrURA";
[8082]440 break;
441 case 1062: case 1068: case 1245: case 1251: case 1257: case 1263:
[9124]442 l = _lHr;
[9277]443 l._type = "RtcmSsrHrClock";
[8082]444 break;
[9124]445 case 4076:
446 l = _lSsrIgs;
[9277]447 l._type = "RtcmSsrIgs4076";
[9124]448 break;
[8082]449 default:
450 return;
451 }
[1562]452
[7422]453 if (_miscIntr > 0) {
[8082]454 l._newSec = corrGPSEpochTime;
455 if (l._newSec > l._oldSec) {
456 if (int(l._newSec) % _miscIntr < int(l._oldSec) % _miscIntr) {
457 if (l._numLat>0) {
[1978]458 QString late;
[8082]459 if (l._meanDiff>0.) {
460 late = QString(" %1: Mean latency %2 sec, min %3, max %4, rms %5, %6 epochs, %7 gaps")
461 .arg(l._type.data())
[9277]462 .arg(int(l._sumLat/l._numLat*100)/100., 4, 'f', 2)
463 .arg(int(l._minLat*100)/100., 4, 'f', 2)
464 .arg(int(l._maxLat*100)/100., 4, 'f', 2)
465 .arg(int((sqrt(l._sumLatQ / l._numLat))*100)/100., 4, 'f', 2)
[8082]466 .arg(l._numLat)
467 .arg(l._numGaps);
[1978]468 if ( _checkMountPoint == _staID || _checkMountPoint == "ALL" ) {
[8204]469 emit(newMessage(QString(_staID + late ).toLatin1(), true) );
[1562]470 }
[8011]471 }
[1978]472 else {
[8082]473 late = QString(" %1: Mean latency %2 sec, min %3, max %4, rms %5, %6 epochs")
474 .arg(l._type.data())
[9277]475 .arg(int(l._sumLat/l._numLat*100)/100., 4, 'f', 2)
476 .arg(int(l._minLat*100)/100., 4, 'f', 2)
477 .arg(int(l._maxLat*100)/100., 4, 'f', 2)
478 .arg(int((sqrt(l._sumLatQ / l._numLat))*100)/100., 4, 'f', 2)
[8082]479 .arg(l._numLat);
[1978]480 if ( _checkMountPoint == _staID || _checkMountPoint == "ALL" ) {
[8204]481 emit(newMessage(QString(_staID + late ).toLatin1(), true) );
[1978]482 }
[1564]483 }
[1566]484 }
[8082]485 l._meanDiff = int(l._diffSec)/l._numLat;
486 l.init();
[1978]487 }
[8082]488 if (l._followSec) {
489 l._diffSec += l._newSec - l._oldSec;
490 if (l._meanDiff>0.) {
491 if (l._newSec - l._oldSec > 1.5 * l._meanDiff) {
492 l._numGaps += 1;
[1562]493 }
494 }
495 }
[8271]496
497 // Compute the observations latency
498 // --------------------------------
499 int week;
500 double sec;
501 currentGPSWeeks(week, sec);
502 double dt = fabs(sec - l._newSec);
503 const double secPerWeek = 7.0 * 24.0 * 3600.0;
504 if (dt > 0.5 * secPerWeek) {
505 if (sec > l._newSec) {
506 sec -= secPerWeek;
507 } else {
508 sec += secPerWeek;
509 }
510 }
[8082]511 l._curLat = sec - l._newSec;
512 l._sumLat += l._curLat;
513 l._sumLatQ += l._curLat * l._curLat;
514 if (l._curLat < l._minLat) {
515 l._minLat = l._curLat;
[1978]516 }
[8082]517 if (l._curLat >= l._maxLat) {
518 l._maxLat = l._curLat;
[1978]519 }
[8082]520 l._numLat += 1;
521 l._followSec = true;
522 setCurrentLatency(l._curLat);
[1562]523 }
[8271]524 l._oldSec = l._newSec;
[1562]525 }
[8271]526 switch (type) {
527 case 1057: case 1063: case 1240: case 1246: case 1252: case 1258:
528 _lOrb = l;
529 break;
530 case 1058: case 1064: case 1241: case 1247: case 1253: case 1259:
531 _lClk = l;
532 break;
533 case 1060: case 1066: case 1243: case 1249: case 1255: case 1261:
534 _lClkOrb = l;
535 break;
536 case 1059: case 1065: case 1242: case 1248: case 1254: case 1260:
537 _lCb = l;
538 break;
539 case 1265: case 1266: case 1267: case 1268: case 1269: case 1270:
540 _lPb = l;
541 break;
542 case 1264:
543 _lVtec = l;
544 break;
545 case 1061: case 1067: case 1244: case 1250: case 1256: case 1262:
546 _lUra = l;
547 break;
548 case 1062: case 1068: case 1245: case 1251: case 1257: case 1263:
549 _lHr = l;
550 break;
[9124]551 case 4076:
552 _lSsrIgs = l;
553 break;
[8271]554 }
[1562]555}
556
[8011]557// Call advisory notice script
[1558]558////////////////////////////////////////////////////////////////////////////
559void latencyChecker::callScript(const char* comment) {
560 if (!_adviseScript.isEmpty()) {
561#ifdef WIN32
[1577]562 Sleep(1);
[1558]563 QProcess::startDetached(_adviseScript, QStringList() << _staID << comment) ;
564#else
[1577]565 sleep(1);
[1558]566 QProcess::startDetached("nohup", QStringList() << _adviseScript << _staID << comment) ;
567#endif
568 }
569}
Note: See TracBrowser for help on using the repository browser.