1 | package Bnc;
|
---|
2 |
|
---|
3 | # Perl utility functions for BNC
|
---|
4 | #
|
---|
5 | # Revision: $Header: trunk/BNC/scripts/Bnc.pm 9597 2022-01-10 16:35:57Z wiese $
|
---|
6 |
|
---|
7 | use strict;
|
---|
8 | use warnings;
|
---|
9 | use File::Basename;
|
---|
10 | use File::Spec::Functions qw(catfile);
|
---|
11 | use File::Temp qw(tempfile);
|
---|
12 | use Exporter;
|
---|
13 | use Time::Piece 1.30;
|
---|
14 | use PDL::Lite; # to avoid namespace pollution
|
---|
15 | use PDL::Primitive;
|
---|
16 | use Log::Log4perl qw(:easy);
|
---|
17 |
|
---|
18 | # use List::MoreUtils qw(uniq); # Prototype mismatch with PDL (uniq)
|
---|
19 |
|
---|
20 | # =============================================================================
|
---|
21 | # callBnc ($bnc_ini, %$opts_ref)
|
---|
22 | # =============================================================================
|
---|
23 | # Call BNC
|
---|
24 | #
|
---|
25 | # Param : $bnc_ini [optional] The BNC config file that should be used. If not set, the default config is ised.
|
---|
26 | # $opts_ref [required] Hash with BNC config options as key. They will overwrite the settings from the config file.
|
---|
27 | # Return : BNC exit status
|
---|
28 | # =============================================================================
|
---|
29 | sub callBnc {
|
---|
30 | my ( $bnc, $bnc_ini, $opts ) = @_;
|
---|
31 |
|
---|
32 | my $config_file = "";
|
---|
33 | if ( $bnc_ini && -s $bnc_ini ) {
|
---|
34 | $config_file = "--conf $bnc_ini";
|
---|
35 | }
|
---|
36 | else {
|
---|
37 | DEBUG("callBnc: Use default bnc-ini file");
|
---|
38 | }
|
---|
39 |
|
---|
40 | my $opts_str = "";
|
---|
41 | if ($opts) { $opts_str = options2string($opts) }
|
---|
42 |
|
---|
43 | # my @cmd = (
|
---|
44 | # 'xvfb-run',
|
---|
45 | # "--server-args='-screen 0, 1280x1024x8'", # 1024x768x24
|
---|
46 | # "$bnc",
|
---|
47 | # "--nw",
|
---|
48 | # $opts_str,
|
---|
49 | # );
|
---|
50 |
|
---|
51 | #my $rc = call_system("xvfb-run -a -e /home/user/xvfb.err --server-args='-screen 0 1280x1024x8' $bnc --nw $config_file $opts_str");
|
---|
52 | return Common::runCmd("$bnc --nw $config_file $opts_str");
|
---|
53 | }
|
---|
54 |
|
---|
55 | # Converts options map to string used for calling BNC.
|
---|
56 | sub options2string {
|
---|
57 | my ($opts) = shift;
|
---|
58 |
|
---|
59 | my $opts_str = "";
|
---|
60 | if ($opts) {
|
---|
61 | foreach my $key ( keys %{$opts} ) {
|
---|
62 | $opts_str .= "--key $key" . ' "' . $opts->{$key} . '" ';
|
---|
63 | }
|
---|
64 | }
|
---|
65 | return $opts_str;
|
---|
66 | }
|
---|
67 |
|
---|
68 | # =============================================================================
|
---|
69 | # parseMessageTypesFromFile
|
---|
70 | # =============================================================================
|
---|
71 | # Parse Message-Types with repetition rate from a BNC/scanRTCM logfile
|
---|
72 | #
|
---|
73 | # Param : $logfile [required] Path to the BNC-logfile.
|
---|
74 | # $caAbbr [optional] caster name or abbreviation
|
---|
75 | #
|
---|
76 | # Return : Message-types for each mountpoint (as Hash-Ref)
|
---|
77 | # Hash with mp as key and the list of messTypes as value
|
---|
78 | # Example: $VAR1 = { 'RIO10' => ['1004(1)','1006(15)','1008(15)',...],
|
---|
79 | # 'CLIB0' => ['1004(1)','1006(10)','1008(10)',...],
|
---|
80 | # };
|
---|
81 | # =============================================================================
|
---|
82 | sub parseMessageTypesFromFile {
|
---|
83 | my ( $logfile, $caAbbr, $mytmpPath ) = @_;
|
---|
84 |
|
---|
85 | unless ( -s $logfile ) {
|
---|
86 | ERROR "File [$logfile] is empty or does not exist";
|
---|
87 | return;
|
---|
88 | }
|
---|
89 |
|
---|
90 | my $tmp = File::Temp->new( UNLINK => 1, SUFFIX => '.messtyps' );
|
---|
91 | my $casterMessTypeFile = $tmp->filename;
|
---|
92 |
|
---|
93 | #my $caster = $logfilename =~ s/\.log\.messtyps//r;
|
---|
94 | #INFO "Process caster '$caster'";
|
---|
95 |
|
---|
96 | INFO "Process $logfile";
|
---|
97 |
|
---|
98 | # scan beginns with that line: 16-02-18 23:58:02 WTZ37: Get data in RTCM 3.x format
|
---|
99 |
|
---|
100 | # First grep for message type lines in bnc-logfile and write them to local temp. directory
|
---|
101 | # NOTE: 'sort -u' because Ephemeries message-types 1019 (GPS), 1020 (GLONASS), 1045 (Galileo) come
|
---|
102 | # for every sat. at the same time/second. This is done for getting the repetition rate
|
---|
103 | system ("grep \"Received message type\" $logfile | sort -u > $casterMessTypeFile") == 0
|
---|
104 | or ERROR "Fehler: $!";
|
---|
105 |
|
---|
106 | # --------------------------------------------
|
---|
107 | # Get message types foreach station/mountpoint
|
---|
108 | # --------------------------------------------
|
---|
109 | my $cmd = "cat $casterMessTypeFile | awk '{print \$3, \$7}' | sort -u";
|
---|
110 | my @messTypes = `$cmd`; # ['DARX1: 1004\n','DARX1: 1006\n',... ]
|
---|
111 | if ( scalar @messTypes < 1 ) {
|
---|
112 | ERROR "Could not retrieve message types from file $casterMessTypeFile";
|
---|
113 | return;
|
---|
114 | }
|
---|
115 |
|
---|
116 | # Note: Skip first 1500 lines because BNC is weird here, all lines with same timestamp, buffer problem?
|
---|
117 |
|
---|
118 | # ------------------------------------------------------
|
---|
119 | # Guess repetition rate for each mountpoint/message-type
|
---|
120 | # ------------------------------------------------------
|
---|
121 | # For that get the first $minSamples appearances of mountp with same message type
|
---|
122 | # and build the differences between them
|
---|
123 | my $minSamples = 4;
|
---|
124 | foreach my $mpType (@messTypes) {
|
---|
125 | chomp $mpType; # 'AUBG3: 1004'
|
---|
126 | my ( $mp, $mt ) = split ( /:\s*/, $mpType );
|
---|
127 | my $cmd = "grep \"$mp: Received message type $mt\" $logfile";
|
---|
128 | my @rows = `$cmd`;
|
---|
129 | if ( scalar @rows < 1 ) {
|
---|
130 | ERROR "Could not retrieve message types for $mpType from file $casterMessTypeFile";
|
---|
131 | next;
|
---|
132 | }
|
---|
133 |
|
---|
134 | if ( scalar @rows <= $minSamples ) {
|
---|
135 | WARN "Could not guess repetition rate for message type $mpType: only " . scalar @rows . " matches found";
|
---|
136 | next;
|
---|
137 | }
|
---|
138 |
|
---|
139 | my $repRate = _computeMessTypRepetitonRate( \@rows, $caAbbr );
|
---|
140 | if ($repRate) {
|
---|
141 | $mpType .= '(' . $repRate . ')';
|
---|
142 | }
|
---|
143 | } # ----- end foreach messageType -----
|
---|
144 |
|
---|
145 | #if ( unlink $casterMessTypeFile ) { TRACE "Removed file [$casterMessTypeFile]" }
|
---|
146 |
|
---|
147 | # Return an HASH OF ARRAYS with mp as key and the list of messTypes as value
|
---|
148 | my %messTypesHash;
|
---|
149 | foreach (@messTypes) {
|
---|
150 | my @ele = split ( ': ', $_ );
|
---|
151 | my $mp = shift @ele;
|
---|
152 | push ( @{ $messTypesHash{$mp} }, shift @ele );
|
---|
153 | }
|
---|
154 |
|
---|
155 | return \%messTypesHash;
|
---|
156 | }
|
---|
157 |
|
---|
158 | # =============================================================================
|
---|
159 | # _computeMessTypRepetitonRate
|
---|
160 | # =============================================================================
|
---|
161 | # Guess repetition rate of message types
|
---|
162 | #
|
---|
163 | # Param : $firstMatches [required] Array-Ref with first appearances of station
|
---|
164 | # and mess type. Complete logfile lines, e.g.
|
---|
165 | # '13-07-04 17:38:26 BRUX0: Received message type 1004 '
|
---|
166 | # $caAbbr [optional] caster abbreviation
|
---|
167 | # Return : repetition rate in secs, (median value)
|
---|
168 | # =============================================================================
|
---|
169 | sub _computeMessTypRepetitonRate {
|
---|
170 | my ( $rows, $caAbbr ) = @_;
|
---|
171 |
|
---|
172 | my $maxGap = 600; # if gap > 10min then we guess it is a new scan
|
---|
173 |
|
---|
174 | $rows->[0] =~ /.{18}([a-x0-9]+): Received message type (\d+)/i;
|
---|
175 | my $stat = $1;
|
---|
176 | my $mestp = $2;
|
---|
177 |
|
---|
178 | # Create list of unix timestamps
|
---|
179 | my @scans;
|
---|
180 | my $scan = 0;
|
---|
181 | my ( $prevTime, $deltaT ) = ( 0, 0 );
|
---|
182 | foreach (@$rows) {
|
---|
183 | my $uxtime = date2unix( substr ( $_, 0, 17 ) );
|
---|
184 | if ( !$uxtime ) {
|
---|
185 | ERROR "Could not parse date from line $_";
|
---|
186 | next;
|
---|
187 | }
|
---|
188 | $deltaT = $uxtime - $prevTime;
|
---|
189 | next if ( $deltaT == 0 ); # e.g. eph 1019,1020 one message for each sat.
|
---|
190 | next if ( $deltaT <= 1 && $mestp =~ /10(19|20|42|43|44|45|46)|63/ );
|
---|
191 | if ( $prevTime && $deltaT > $maxGap ) {
|
---|
192 | $scan++;
|
---|
193 | }
|
---|
194 | push ( @{ $scans[$scan] }, $uxtime );
|
---|
195 | $prevTime = $uxtime;
|
---|
196 | }
|
---|
197 |
|
---|
198 | my @repRates;
|
---|
199 | my $highest_nof_diffs = 0;
|
---|
200 | foreach (@scans) {
|
---|
201 | my @timestamps = @{$_};
|
---|
202 |
|
---|
203 | # Compute the differences
|
---|
204 | my @diffs;
|
---|
205 | for ( my $i = 1; $i <= $#timestamps; $i++ ) {
|
---|
206 | push ( @diffs, $timestamps[$i] - $timestamps[ $i - 1 ] );
|
---|
207 | }
|
---|
208 | my $nof_diffs = scalar @diffs;
|
---|
209 |
|
---|
210 | if ( $nof_diffs < 2 ) {
|
---|
211 | WARN("$stat: $mestp: only $nof_diffs diffs");
|
---|
212 | next;
|
---|
213 | }
|
---|
214 |
|
---|
215 | my ( $mean, $prms, $median, $min, $max, $adev, $rms_n ) = stats( pdl \@diffs );
|
---|
216 | $mean = sprintf ( "%.0f", $mean );
|
---|
217 | $rms_n = sprintf ( "%.02f", $rms_n );
|
---|
218 | print $stat, ": ", $mestp, ": ", join ( ' ', @diffs ), "[Sig: $mean, $rms_n]\n";
|
---|
219 |
|
---|
220 | if ( $rms_n > 10 ) {
|
---|
221 | WARN("$stat: $mestp: RMS too high: $rms_n");
|
---|
222 | next;
|
---|
223 | }
|
---|
224 |
|
---|
225 | # get the most frequent value
|
---|
226 | my %counti = ();
|
---|
227 | $counti{$_}++ foreach (@diffs);
|
---|
228 | my ( $ni, $mfv ) = ( 0, 0 );
|
---|
229 | while ( my ( $k, $v ) = each %counti ) {
|
---|
230 | if ( $v > $ni ) {
|
---|
231 | $ni = $v;
|
---|
232 | $mfv = $k;
|
---|
233 | }
|
---|
234 | }
|
---|
235 |
|
---|
236 | my $rounded_val = $mfv; # init
|
---|
237 | foreach ( ( 1, 5, 10, 15, 30, 60, 120, 150, 300 ) ) { # most likely values
|
---|
238 | my $mdiff = abs ( $mfv - $_ );
|
---|
239 | if ( $mdiff <= 2 ) {
|
---|
240 | $rounded_val = $_;
|
---|
241 | }
|
---|
242 | }
|
---|
243 | push ( @repRates, [ $rounded_val, $nof_diffs ] );
|
---|
244 |
|
---|
245 | if ( $nof_diffs > $highest_nof_diffs ) {
|
---|
246 | $highest_nof_diffs = $nof_diffs;
|
---|
247 | }
|
---|
248 | } # ----- end foreach scan -----
|
---|
249 |
|
---|
250 | my @mostLikelyRates = grep { $_->[1] == $highest_nof_diffs } @repRates;
|
---|
251 | my $mostLikelyRate = $mostLikelyRates[0]->[0];
|
---|
252 | foreach (@repRates) {
|
---|
253 | if ( abs ( $_->[0] - $mostLikelyRate ) > 2 ) {
|
---|
254 | ERROR "$stat: $caAbbr: $mestp: repetition rates from different scans differ: $mostLikelyRate $_->[0]";
|
---|
255 | if ( scalar @repRates == 2 ) {
|
---|
256 | return;
|
---|
257 | }
|
---|
258 | }
|
---|
259 | }
|
---|
260 |
|
---|
261 | return $mostLikelyRate;
|
---|
262 | }
|
---|
263 |
|
---|
264 | # =============================================================================
|
---|
265 | # parseConfig ($confFile)
|
---|
266 | # =============================================================================
|
---|
267 | # Parse the BNC config file.
|
---|
268 | #
|
---|
269 | # Param : $confFile [required] BNC config file
|
---|
270 | # Return : Hash with configuration on success, otherwise undef
|
---|
271 | # Usage : $bncConf = parseConf($bncConfFile);
|
---|
272 | # $corrMount = $bncConf->{'PPP'}->{'corrMount'};
|
---|
273 | # =============================================================================
|
---|
274 | sub parseConfig {
|
---|
275 | my ($confFile) = @_;
|
---|
276 |
|
---|
277 | -s $confFile || LOGDIE "BNC config file \"$confFile\" does not exist\n";
|
---|
278 | TRACE "Parse BNC config file $confFile";
|
---|
279 | open ( my $INP, '<', $confFile ) || die "Could not open file '$confFile': $!";
|
---|
280 | my @confLines = <$INP>;
|
---|
281 | close ($INP);
|
---|
282 |
|
---|
283 | my %conf;
|
---|
284 | my $section; # [General], [PPP]
|
---|
285 | foreach (@confLines) {
|
---|
286 | chomp;
|
---|
287 | s/#.*//; # entfernt Kommentare
|
---|
288 | s/^\s*//; # whitespace am Anfang entfernen
|
---|
289 | s/\s+$//; # entfernt alle whitespaces am Ende
|
---|
290 | next unless length;
|
---|
291 | if ( $_ =~ /\[(\S+)\]/ ) { $section = $1 }
|
---|
292 | next if ( !$section );
|
---|
293 | my ( $key, $val ) = split ( /\s*=\s*/, $_, 2 );
|
---|
294 | if ( !defined $val ) { $val = "" }
|
---|
295 |
|
---|
296 | if ( $key eq "mountPoints" ) {
|
---|
297 |
|
---|
298 | # Simple parsing
|
---|
299 | $val =~ s/^\/\///;
|
---|
300 | my @mpts = split ( /,\s?\/{2}/, $val );
|
---|
301 | $conf{$section}->{$key} = \@mpts;
|
---|
302 |
|
---|
303 | # Extended parsing
|
---|
304 | my @mpts_def = ();
|
---|
305 | foreach (@mpts) {
|
---|
306 |
|
---|
307 | # user:passwd@igs-ip.net:2101/ASPA0 RTCM_3.0 ASM -14.33 189.28 no 1
|
---|
308 | if ( $_ =~
|
---|
309 | /^([\w-]+):(.+[^@])@([\w\.-]+):(\d{3,5})\/([-\w]+) ([\w\.]+) ?(\w{3})? ([\+\-\d\.]+) ([\+\-\d\.]+) no (\w+)/i
|
---|
310 | )
|
---|
311 | {
|
---|
312 | push ( @mpts_def, { caster => $3, port => $4, mp => $5, ntripVers => $10 } );
|
---|
313 | }
|
---|
314 | else { ERROR "$confFile: Could not parse mountPoints string $_" }
|
---|
315 | }
|
---|
316 | $conf{$section}->{'mountPoints_parsed'} = \@mpts_def;
|
---|
317 | }
|
---|
318 | elsif ( $key eq "cmbStreams" ) {
|
---|
319 | my @cmbStrs = split ( /\s*,\s*/, $val );
|
---|
320 | foreach (@cmbStrs) {
|
---|
321 | s/"//g;
|
---|
322 | s/\s+$//; # entfernt alle whitespaces am Ende
|
---|
323 | }
|
---|
324 | $conf{$section}->{$key} = \@cmbStrs;
|
---|
325 | }
|
---|
326 | else { $conf{$section}->{$key} = $val }
|
---|
327 | }
|
---|
328 |
|
---|
329 | my @nofPar = keys %conf;
|
---|
330 | if ( scalar @nofPar < 1 ) {
|
---|
331 | ERROR "No parameter found in BNC conf \"$confFile\"";
|
---|
332 | return;
|
---|
333 | }
|
---|
334 | return \%conf;
|
---|
335 | }
|
---|
336 |
|
---|
337 | # =============================================================================
|
---|
338 | # parseLogfile ($file, $sampling, $goBackSecs, $logMode )
|
---|
339 | # =============================================================================
|
---|
340 | # Parse BNCs' logfile
|
---|
341 | #
|
---|
342 | # Param : $file [required] BNC logfile
|
---|
343 | # $sampling [optional] sampling rate for logfile
|
---|
344 | # $logMode [optional] Flag. If set, remember the position of the file-read
|
---|
345 | # for the next read. Default: off
|
---|
346 | # Return : \%data
|
---|
347 | # =============================================================================
|
---|
348 | sub parseLogfile {
|
---|
349 | my $file = shift;
|
---|
350 | my $sampling = shift // 1;
|
---|
351 | my $logMode = shift // 0;
|
---|
352 |
|
---|
353 | open ( my $fh, "<", $file ) || LOGDIE "Could not open file $file: $!\n";
|
---|
354 |
|
---|
355 | # Goto last position from last read
|
---|
356 | #my $fPos = filePosition($file);
|
---|
357 | #TRACE "Current file pos: $fPos";
|
---|
358 | $logMode && seek ( $fh, filePosition($file), 0 );
|
---|
359 |
|
---|
360 | #$logMode && seek ( $fh, $fPos, 0 );
|
---|
361 | my $ln = "";
|
---|
362 | my ( @hlp, @epochs, @latencies, @restarts );
|
---|
363 | my $rec = {};
|
---|
364 | while (<$fh>) {
|
---|
365 | chomp ( $ln = $_ );
|
---|
366 | $rec = {};
|
---|
367 |
|
---|
368 | if ( $ln =~ /\bNEU/ ) { # NEU displacements
|
---|
369 | @hlp = split ( /\s+/, $ln );
|
---|
370 | my $tp = Time::Piece->strptime( substr ( $hlp[2], 0, 19 ), '%Y-%m-%d_%H:%M:%S' );
|
---|
371 |
|
---|
372 | if ( $hlp[14] eq '-nan' || $hlp[15] eq '-nan' || $hlp[16] eq '-nan' ) {
|
---|
373 | WARN("$hlp[2] $hlp[3]: NEU displacements are NAN");
|
---|
374 | next;
|
---|
375 | }
|
---|
376 |
|
---|
377 | #DEBUG ($tp->epoch, $hlp[3]);
|
---|
378 | push (
|
---|
379 | @epochs,
|
---|
380 | {
|
---|
381 | time => $tp->epoch,
|
---|
382 | site => $hlp[3],
|
---|
383 | dN => $hlp[14],
|
---|
384 | dE => $hlp[15],
|
---|
385 | dU => $hlp[16],
|
---|
386 | TRP => $hlp[18] + $hlp[19],
|
---|
387 | }
|
---|
388 | );
|
---|
389 | }
|
---|
390 | elsif ( index ( $ln, "latency", 25 ) > 1 ) {
|
---|
391 |
|
---|
392 | # DEBUG ($ln);
|
---|
393 | # altes format: 15-10-06 15:29:02 POTS0: Mean latency 2.34 sec, min 1.58, max 3, rms 0.48, 43 epochs, 17 gaps
|
---|
394 | # neu in BNC 2.12.4: 17-06-06 15:35:02 OHI37 Observations: Mean latency 1.51 sec, min 0.57, max 2.7, rms 0.5, 203 epochs, 73 gaps
|
---|
395 | @hlp = split ( /\s+/, $ln );
|
---|
396 |
|
---|
397 | # old latency log format
|
---|
398 | if ( $hlp[2] =~ /:$/ ) {
|
---|
399 | splice @hlp, 3, 0, 'Placeholder:';
|
---|
400 | $hlp[2] =~ s/:$//;
|
---|
401 | }
|
---|
402 | $hlp[3] =~ s/:$//;
|
---|
403 |
|
---|
404 | my $tp = Time::Piece->strptime( "$hlp[0] $hlp[1]", '%y-%m-%d %H:%M:%S' );
|
---|
405 | $rec = {
|
---|
406 | time => $tp->epoch,
|
---|
407 | mp => $hlp[2],
|
---|
408 | meanLat => $hlp[6] + 0.0,
|
---|
409 | epochs => int ( $hlp[14] ),
|
---|
410 | type => $hlp[3]
|
---|
411 | };
|
---|
412 |
|
---|
413 | # Unter bestimmten Bedingungen werden die gaps nicht rausgeschrieben!
|
---|
414 | if ( $ln =~ /gaps/ ) {
|
---|
415 | $rec->{'gaps'} = int ( $hlp[16] );
|
---|
416 | }
|
---|
417 |
|
---|
418 | push ( @latencies, $rec );
|
---|
419 | }
|
---|
420 | elsif ( index ( $ln, "Start BNC" ) > 1 ) {
|
---|
421 |
|
---|
422 | # 17-06-13 07:06:58 ========== Start BNC v2.12.3 (LINUX) ==========
|
---|
423 | @hlp = split ( /\s+/, $ln );
|
---|
424 | my $tp = Time::Piece->strptime( "$hlp[0] $hlp[1]", '%y-%m-%d %H:%M:%S' );
|
---|
425 | push (
|
---|
426 | @restarts,
|
---|
427 | {
|
---|
428 | time => $tp->epoch,
|
---|
429 | bncvers => $hlp[5]
|
---|
430 | }
|
---|
431 | );
|
---|
432 | }
|
---|
433 |
|
---|
434 | } # ----- next line -----
|
---|
435 |
|
---|
436 | $logMode && filePosition( $file, tell ($fh) ); # Remember pos for next read
|
---|
437 | close $fh;
|
---|
438 |
|
---|
439 | # Sampling must be done afterwords, for each station separated!
|
---|
440 | my @epochs_sampled;
|
---|
441 | my @sites = map { $_->{'site'} } @epochs;
|
---|
442 |
|
---|
443 | #@sites = uniq @sites;
|
---|
444 | my %hlp1 = ();
|
---|
445 | @sites = grep { !$hlp1{$_}++ } @sites;
|
---|
446 | foreach my $s (@sites) {
|
---|
447 | my $epoch_selected = 0;
|
---|
448 | foreach my $rec (@epochs) {
|
---|
449 | next if ( $rec->{'site'} ne $s );
|
---|
450 | if ( $rec->{'time'} - $epoch_selected >= $sampling ) {
|
---|
451 | push ( @epochs_sampled, $rec );
|
---|
452 | $epoch_selected = $rec->{'time'};
|
---|
453 | }
|
---|
454 | }
|
---|
455 | }
|
---|
456 |
|
---|
457 | my %data = (
|
---|
458 | EPOCHS => \@epochs_sampled,
|
---|
459 | LATENCIES => \@latencies,
|
---|
460 | RESTARTS => \@restarts
|
---|
461 | );
|
---|
462 |
|
---|
463 | return \%data;
|
---|
464 | }
|
---|
465 |
|
---|
466 | # =============================================================================
|
---|
467 | # parsePPPLogfile ($file, $sampling, $goBackSecs, $logMode )
|
---|
468 | # =============================================================================
|
---|
469 | # Parse BNCs' PPP station logfile
|
---|
470 | #
|
---|
471 | # Param : $file [required] BNC PPP station
|
---|
472 | # $sampling [optional] sampling rate for logfile
|
---|
473 | # $goBackSecs [optional] go back that seconds from now in logfile
|
---|
474 | # $logMode [optional] Flag. If set, remember the position of the file-read
|
---|
475 | # for the next read. Default: off
|
---|
476 | # Return : $station, \%data
|
---|
477 | # =============================================================================
|
---|
478 | sub parsePPPLogfile {
|
---|
479 | my $file = shift;
|
---|
480 | my $sampling = shift // 1;
|
---|
481 | my $goBackSecs = shift // 0;
|
---|
482 | my $logMode = shift // 0;
|
---|
483 |
|
---|
484 | if ($logMode) { $goBackSecs = 0 }
|
---|
485 |
|
---|
486 | my $startSec;
|
---|
487 | if ($goBackSecs) {
|
---|
488 | $startSec = time () - $goBackSecs;
|
---|
489 | }
|
---|
490 | my $isFreePPP = 1; # Free or closed PPP version
|
---|
491 | my $epo;
|
---|
492 | my $old_epochSec = 0;
|
---|
493 | my $epochSec = 0;
|
---|
494 | my $epochDiff = 0;
|
---|
495 | my (
|
---|
496 | @hlp, @EPOCHs, @N, @E, @U, %SATNUM, @TRPs, @CLKs,
|
---|
497 | @G_CLKs, @R_CLKs, @E_CLKs, @C_CLKs, @OGRs, @OGEs, @OGCs, @OFFGLOs
|
---|
498 | );
|
---|
499 | my ( @EPOCHs_OGE, @EPOCHs_OGR, @EPOCHs_OGC );
|
---|
500 | my ( @EPOCHs_G_CLK, @EPOCHs_R_CLK, @EPOCHs_E_CLK, @EPOCHs_C_CLK );
|
---|
501 | my ( %AMB, %RES, %ELE, %ION, %BIA );
|
---|
502 | my ( $station, $lki, $sys, $sat, $amb );
|
---|
503 | open ( my $fh, "<", $file ) || LOGDIE "Could not open file $file: $!\n";
|
---|
504 |
|
---|
505 | # Goto last position from last read
|
---|
506 | #my $fPos = filePosition($file);
|
---|
507 | #TRACE "Current file pos: $fPos";
|
---|
508 | $logMode && seek ( $fh, filePosition($file), 0 );
|
---|
509 |
|
---|
510 | #$logMode && seek ( $fh, $fPos, 0 );
|
---|
511 | my $ln = "";
|
---|
512 | while (<$fh>) {
|
---|
513 | chomp ( $ln = $_ );
|
---|
514 |
|
---|
515 | if ( $ln =~ /\bof Epoch\b/ ) {
|
---|
516 |
|
---|
517 | # PPP of Epoch 2015-08-27_14:00:15.000
|
---|
518 | if ( $ln =~ /PPP of Epoch (\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})\.\d+/ ) { # closed PPP
|
---|
519 | $isFreePPP = 0;
|
---|
520 | $epo = $1; #print "$epo\n";
|
---|
521 | }
|
---|
522 | elsif ( $ln =~ /Point Positioning of Epoch (\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})\.\d+/ ) { # free PPP
|
---|
523 | $isFreePPP = 1;
|
---|
524 | $epo = $1;
|
---|
525 | }
|
---|
526 | else { ERROR "strange line: \"$ln\""; next }
|
---|
527 |
|
---|
528 | #my $date = sprintf ( "%s %s", split ( /_/, $epo ) );
|
---|
529 | my $tp = Time::Piece->strptime( $epo, '%Y-%m-%d_%H:%M:%S' );
|
---|
530 | $epochSec = $tp->epoch();
|
---|
531 | $epochDiff = $epochSec - $old_epochSec;
|
---|
532 | next;
|
---|
533 | }
|
---|
534 |
|
---|
535 | next if ( !$epo );
|
---|
536 | next if ( defined $startSec && $epochSec < $startSec );
|
---|
537 | next if ( $epochDiff && $epochDiff < $sampling );
|
---|
538 |
|
---|
539 | @hlp = split ( /\s+/, $ln );
|
---|
540 |
|
---|
541 | if ( $ln =~ /\bOFFGLO\b/ ) { # ... OFFGLO 8.417 +- 28.479
|
---|
542 | push ( @OFFGLOs, $hlp[2] );
|
---|
543 | }
|
---|
544 |
|
---|
545 | elsif ( $ln =~ /\bdN\b/ ) {
|
---|
546 | push ( @EPOCHs, $epochSec ); # besser $epo ?
|
---|
547 | $old_epochSec = $epochSec;
|
---|
548 |
|
---|
549 | #2015-08-27_13:59:50.000 DIEP1 X = 3842152.9054 +- 0.0242 Y = 563402.0331 +- 0.0176 Z = 5042888.5182 +- 0.0319 dN = 0.0130 +- 0.0193 dE = -0.0032 +- 0.0178 dU = -0.0248 +- 0.0349
|
---|
550 | $station = $hlp[1];
|
---|
551 |
|
---|
552 | if ( $hlp[19] eq '-nan' || $hlp[24] eq '-nan' || $hlp[29] eq '-nan' ) {
|
---|
553 | WARN("$hlp[0] $station: NEU displacements are NAN");
|
---|
554 | }
|
---|
555 |
|
---|
556 | push @N, $hlp[19];
|
---|
557 | push @E, $hlp[24];
|
---|
558 | push @U, $hlp[29];
|
---|
559 | }
|
---|
560 | elsif ( ( $ln =~ /\bAMB\b/ ) && ( $ln !~ /RESET/ ) ) {
|
---|
561 | if ($isFreePPP) { # 2015-10... AMB G05 -6.754 +- 0.086 nEpo = 633
|
---|
562 | $sat = $hlp[2];
|
---|
563 | $sys = substr ( $sat, 0, 1 );
|
---|
564 | $amb = $hlp[4];
|
---|
565 | push @{ $AMB{$sys}{$sat}{EPOCH} }, $epochSec;
|
---|
566 | push @{ $AMB{$sys}{$sat}{DATA} }, $amb;
|
---|
567 | push @{ $AMB{$sys}{$sat}{NUMEPO} }, $hlp[9];
|
---|
568 | }
|
---|
569 | else { # 2015-08... AMB lIF G04 253.0000 -8.9924 +- 1.7825 el = 22.03 epo = 86
|
---|
570 | $lki = $hlp[2];
|
---|
571 | $sat = $hlp[3];
|
---|
572 | $sys = substr ( $sat, 0, 1 );
|
---|
573 | $amb = $hlp[4] + $hlp[5];
|
---|
574 | push @{ $AMB{$lki}{$sys}{$sat}{EPOCH} }, $epochSec;
|
---|
575 | push @{ $AMB{$lki}{$sys}{$sat}{DATA} }, $amb;
|
---|
576 | push @{ $AMB{$lki}{$sys}{$sat}{NUMEPO} }, $hlp[13];
|
---|
577 | push @{ $ELE{$sys}{$sat}{EPOCH} }, $epochSec;
|
---|
578 | push @{ $ELE{$sys}{$sat}{DATA} }, $hlp[10];
|
---|
579 | }
|
---|
580 | }
|
---|
581 | elsif ( $ln =~ /\bRES\b/ && $ln !~ /Neglected/ ) {
|
---|
582 | if ($isFreePPP) { # 2015-10... RES R08 L3 -0.0069
|
---|
583 | $sat = $hlp[2];
|
---|
584 | $lki = $hlp[3];
|
---|
585 | }
|
---|
586 | else { # 2015-08... RES lIF G30 -0.0076
|
---|
587 | $sat = $hlp[3];
|
---|
588 | $lki = $hlp[2];
|
---|
589 | }
|
---|
590 | $sys = substr ( $sat, 0, 1 );
|
---|
591 |
|
---|
592 | #print "$epo $lki $sys $sat $res\n";
|
---|
593 | push @{ $RES{$lki}{$sys}{$sat}{EPOCH} }, $epochSec;
|
---|
594 | push @{ $RES{$lki}{$sys}{$sat}{DATA} }, $hlp[4];
|
---|
595 | }
|
---|
596 | elsif ( ( $ln =~ /\bION\b/ ) && ( $ln !~ /RESET/ ) ) {
|
---|
597 |
|
---|
598 | # 2018-12-01_20:37:58.000 ION G02 0.0000 -0.3277 +- 2.4663
|
---|
599 | $sat = $hlp[2];
|
---|
600 | $sys = substr ( $sat, 0, 1 );
|
---|
601 | push @{ $ION{$sys}{$sat}{EPOCH} }, $epochSec;
|
---|
602 | push @{ $ION{$sys}{$sat}{DATA} }, $hlp[4];
|
---|
603 | }
|
---|
604 | elsif ( ( $ln =~ /\bBIA\b/ ) && ( $ln !~ /RESET/ ) ) {
|
---|
605 |
|
---|
606 | # 2020-12-09_00:55:19.000 BIA c1 G 0.0000 +2.5149 +- 9.6543
|
---|
607 | $lki = $hlp[2];
|
---|
608 | $sys = $hlp[3];
|
---|
609 | push @{ $BIA{$lki}{$sys}{EPOCH} }, $epochSec;
|
---|
610 | push @{ $BIA{$lki}{$sys}{DATA} }, $hlp[4] + $hlp[5];
|
---|
611 | }
|
---|
612 | elsif ( $ln =~ /\bCLK\b/ ) {
|
---|
613 | if ($isFreePPP) { push ( @CLKs, $hlp[2] ) }
|
---|
614 | else { push ( @CLKs, $hlp[2] + $hlp[3] ) }
|
---|
615 | }
|
---|
616 |
|
---|
617 | # REC_CLK in BNC 2.13
|
---|
618 | #elsif ( $ln =~ /\bREC_CLK\b/ ) {
|
---|
619 | elsif ( $ln =~ /\bREC_CLK\s{8}/ ) {
|
---|
620 | push ( @CLKs, $hlp[2] + $hlp[3] );
|
---|
621 | }
|
---|
622 | elsif ( $ln =~ /\bREC_CLK G\b/ ) {
|
---|
623 | push ( @EPOCHs_G_CLK, $epochSec );
|
---|
624 | push ( @G_CLKs, $hlp[3] + $hlp[4] );
|
---|
625 | }
|
---|
626 | elsif ( $ln =~ /\bREC_CLK R\b/ ) {
|
---|
627 | push ( @EPOCHs_R_CLK, $epochSec );
|
---|
628 | push ( @R_CLKs, $hlp[3] + $hlp[4] );
|
---|
629 | }
|
---|
630 | elsif ( $ln =~ /\bREC_CLK E\b/ ) {
|
---|
631 | push ( @EPOCHs_E_CLK, $epochSec );
|
---|
632 | push ( @E_CLKs, $hlp[3] + $hlp[4] );
|
---|
633 | }
|
---|
634 | elsif ( $ln =~ /\bREC_CLK C\b/ ) {
|
---|
635 | push ( @EPOCHs_C_CLK, $epochSec );
|
---|
636 | push ( @C_CLKs, $hlp[3] + $hlp[4] );
|
---|
637 | }
|
---|
638 | elsif ( $ln =~ /\bOGR\b/ ) { # 2015-08... OGR 52.6806 -3.8042 +- 9.0077
|
---|
639 | push ( @EPOCHs_OGR, $epochSec );
|
---|
640 | push ( @OGRs, $hlp[2] + $hlp[3] ); # only https so far
|
---|
641 | }
|
---|
642 | elsif ( $ln =~ /\bOGE\b/ ) { # 2015-08... OGE 52.6806 -3.8042 +- 9.0077
|
---|
643 | push ( @EPOCHs_OGE, $epochSec );
|
---|
644 | push ( @OGEs, $hlp[2] + $hlp[3] ); # only https so far
|
---|
645 | }
|
---|
646 | elsif ( $ln =~ /\bOGC\b/ ) { # 2015-08... OGC 52.6806 -3.8042 +- 9.0077
|
---|
647 | push ( @EPOCHs_OGC, $epochSec );
|
---|
648 | push ( @OGCs, $hlp[2] + $hlp[3] ); # only https so far
|
---|
649 | }
|
---|
650 | elsif ( $ln =~ /\bSATNUM\b/ ) { # 2015-09... SATNUM G 8
|
---|
651 | push ( @{ $SATNUM{ $hlp[2] } }, $hlp[3] );
|
---|
652 | }
|
---|
653 | elsif ( $ln =~ /\bTRP\b/ ) { # 2015-08... TRP 2.3803 +0.1009 +- 0.0324
|
---|
654 | push ( @TRPs, $hlp[2] + $hlp[3] );
|
---|
655 | }
|
---|
656 |
|
---|
657 | } # ----- next line -----
|
---|
658 |
|
---|
659 | $logMode && filePosition( $file, tell ($fh) ); # Remember pos for next read
|
---|
660 | close $fh;
|
---|
661 |
|
---|
662 | my $nof_epochs = scalar @EPOCHs;
|
---|
663 | DEBUG( "epochs:$nof_epochs, North displac.: "
|
---|
664 | . scalar @N
|
---|
665 | . ", East displac.: "
|
---|
666 | . scalar @E
|
---|
667 | . ", Up displac.: "
|
---|
668 | . scalar @U
|
---|
669 | . ", TRPs:"
|
---|
670 | . scalar @TRPs
|
---|
671 | . ", CLKs:"
|
---|
672 | . scalar @CLKs
|
---|
673 | . ", OFFGLOs:"
|
---|
674 | . scalar @OFFGLOs );
|
---|
675 | if ( $nof_epochs != scalar @N ) { LOGDIE "number of epochs and residuals not equal\n" }
|
---|
676 | if ( $nof_epochs != scalar @TRPs ) { LOGDIE "number of epochs and TRPs not equal\n" }
|
---|
677 | if ( @CLKs && $nof_epochs != scalar @CLKs ) { LOGDIE "number of epochs and CLKs not equal\n" }
|
---|
678 | if ( @G_CLKs && scalar @EPOCHs_G_CLK != scalar @G_CLKs ) { LOGDIE "number of epochs and G_CLKs not equal\n" }
|
---|
679 | if ( @R_CLKs && scalar @EPOCHs_R_CLK != scalar @R_CLKs ) { LOGDIE "number of epochs and R_CLKs not equal\n" }
|
---|
680 | if ( @E_CLKs && scalar @EPOCHs_E_CLK != scalar @E_CLKs ) { LOGDIE "number of epochs and E_CLKs not equal\n" }
|
---|
681 | if ( @C_CLKs && scalar @EPOCHs_C_CLK != scalar @C_CLKs ) { LOGDIE "number of epochs and C_CLKs not equal\n" }
|
---|
682 | if ( @OGRs && scalar @EPOCHs_OGR != scalar @OGRs ) { LOGDIE "number of epochs and OGRs not equal\n" }
|
---|
683 | if ( @OGEs && scalar @EPOCHs_OGE != scalar @OGEs ) { LOGDIE "number of epochs and OGEs not equal\n" }
|
---|
684 | if ( @OGCs && scalar @EPOCHs_OGC != scalar @OGCs ) { LOGDIE "number of epochs and OGCs not equal\n" }
|
---|
685 |
|
---|
686 | my %data = (
|
---|
687 | EPOCHS => \@EPOCHs,
|
---|
688 | N => \@N,
|
---|
689 | E => \@E,
|
---|
690 | U => \@U,
|
---|
691 | SATNUM => \%SATNUM,
|
---|
692 | TRPs => \@TRPs,
|
---|
693 | CLKs => \@CLKs,
|
---|
694 | G_CLKs => \@G_CLKs,
|
---|
695 | R_CLKs => \@R_CLKs,
|
---|
696 | E_CLKs => \@E_CLKs,
|
---|
697 | C_CLKs => \@C_CLKs,
|
---|
698 | OGRs => \@OGRs,
|
---|
699 | OGEs => \@OGEs,
|
---|
700 | OGCs => \@OGCs,
|
---|
701 | OFFGLOs => \@OFFGLOs,
|
---|
702 | RES => \%RES,
|
---|
703 | AMB => \%AMB,
|
---|
704 | ELE => \%ELE,
|
---|
705 | ION => \%ION,
|
---|
706 | BIA => \%BIA,
|
---|
707 | );
|
---|
708 |
|
---|
709 | return ( $station, \%data, $isFreePPP );
|
---|
710 | }
|
---|
711 |
|
---|
712 | # =============================================================================
|
---|
713 | # BncStillWorks ($bncConfFile)
|
---|
714 | # =============================================================================
|
---|
715 | # Checks if BNC is still working.
|
---|
716 | #
|
---|
717 | # BNC Jobs can still be alive (in processlist) but are not producing any more.
|
---|
718 | # This function checks if a BNC process is proper working.
|
---|
719 | #
|
---|
720 | # Param : $bncConfFile [required] path of BNC config file
|
---|
721 | # Return : true if BNC is still working otherwise false.
|
---|
722 | # =============================================================================
|
---|
723 | sub BncStillWorks {
|
---|
724 | my ($bncConfFile) = @_;
|
---|
725 |
|
---|
726 | my $timep = Time::Piece->new;
|
---|
727 |
|
---|
728 | # for safety if it is exatly at 00:00, add 30 sec
|
---|
729 | my $min_tmp = $timep->strftime("%M");
|
---|
730 | if ( $min_tmp =~ /00|15|30|45/ && $timep->strftime("%S") < 15 ) {
|
---|
731 | $timep += 30;
|
---|
732 | sleep 30;
|
---|
733 | }
|
---|
734 | my $yyyy = $timep->year;
|
---|
735 | my $yy = $timep->yy;
|
---|
736 | my $doy = sprintf "%03d", $timep->yday + 1;
|
---|
737 | my $hh = $timep->strftime("%H");
|
---|
738 | my $h = uc ( chr ( 65 + $hh ) );
|
---|
739 | my $min = $timep->min;
|
---|
740 | my $startmin;
|
---|
741 | if ( $min < 15 ) { $startmin = "00" }
|
---|
742 | elsif ( $min < 30 ) { $startmin = "15" }
|
---|
743 | elsif ( $min < 45 ) { $startmin = "30" }
|
---|
744 | elsif ( $min <= 59 ) { $startmin = "45" }
|
---|
745 | my $bncConf = parseConf($bncConfFile);
|
---|
746 | my $bncLogFileStub = $bncConf->{'General'}->{'logFile'};
|
---|
747 |
|
---|
748 | # BNC log file
|
---|
749 | # ------------
|
---|
750 | my $bncLogFile = "${bncLogFileStub}_" . $timep->strftime("%y%m%d"); # -> bnc.log_160425
|
---|
751 | unless ( -s $bncLogFile ) {
|
---|
752 | WARN("BNC logfile \"$bncLogFile\" is empty or does not exist");
|
---|
753 | return 0;
|
---|
754 | }
|
---|
755 |
|
---|
756 | # RINEX Obs Generation
|
---|
757 | # --------------------
|
---|
758 | if ( $bncConf->{'General'}->{'rnxPath'} ) {
|
---|
759 | my $rnxPath = $bncConf->{'General'}->{'rnxPath'};
|
---|
760 | $rnxPath =~ s/\/$//;
|
---|
761 |
|
---|
762 | # Write Rnx3 files (i.e. long Rnx3 filenames) 2: on ('rnxV3filenames' is deprecated since 2.12.8!!!)
|
---|
763 | my $writeRnxV3 = $bncConf->{'General'}->{'rnxV3'};
|
---|
764 | my $rnxIntr = $bncConf->{'General'}->{'rnxIntr'};
|
---|
765 | my $fileMask;
|
---|
766 |
|
---|
767 | if ($writeRnxV3) {
|
---|
768 | if ( $rnxIntr eq "1 hour" ) {
|
---|
769 | $fileMask = "*_S_${yyyy}${doy}${hh}??_01H_30S_?O.rnx";
|
---|
770 | }
|
---|
771 | elsif ( $rnxIntr eq "15 min" ) {
|
---|
772 | $fileMask = "*_S_${yyyy}${doy}${hh}${startmin}_15M_01S_?O.rnx";
|
---|
773 | }
|
---|
774 | else { # daily?
|
---|
775 | $fileMask = "*_S_${yyyy}${doy}????_01D_30S_?O.rnx"; # HRAG00ZAF_S_20191220000_01D_30S_MO.rnx
|
---|
776 | }
|
---|
777 | }
|
---|
778 | else { # Rnx2
|
---|
779 | if ( $rnxIntr eq "1 hour" ) {
|
---|
780 | $fileMask = "????${doy}${h}.${yy}O";
|
---|
781 | }
|
---|
782 | elsif ( $rnxIntr eq "15 min" ) {
|
---|
783 | $fileMask = "????${doy}${h}${startmin}.${yy}O";
|
---|
784 | }
|
---|
785 | else { # daily?
|
---|
786 | $fileMask = "????${doy}*.${yy}O";
|
---|
787 | }
|
---|
788 | }
|
---|
789 |
|
---|
790 | my @rnxFiles = glob "$rnxPath/$fileMask";
|
---|
791 | if ( scalar @rnxFiles < 1 ) {
|
---|
792 | ERROR("BNC does not create RINEX Obs files. (Filemask: \"$fileMask\" Path: $rnxPath)");
|
---|
793 |
|
---|
794 | #return 0;
|
---|
795 | }
|
---|
796 | }
|
---|
797 |
|
---|
798 | # RINEX Ephemerides Generation
|
---|
799 | # ----------------------------
|
---|
800 | if ( $bncConf->{'General'}->{'ephPath'} ) {
|
---|
801 | my $rnxPath = $bncConf->{'General'}->{'ephPath'};
|
---|
802 | $rnxPath =~ s/\/$//;
|
---|
803 | my $writeRnxV3 = $bncConf->{'General'}->{'ephV3'};
|
---|
804 | my $rnxIntr = $bncConf->{'General'}->{'ephIntr'};
|
---|
805 | my $fileMask;
|
---|
806 |
|
---|
807 | if ($writeRnxV3) {
|
---|
808 | if ( $rnxIntr eq "1 hour" ) {
|
---|
809 | $fileMask = "BRD?00WRD_S_${yyyy}${doy}${hh}00_01H_?N.rnx";
|
---|
810 | }
|
---|
811 | elsif ( $rnxIntr eq "15 min" ) {
|
---|
812 | $fileMask = "BRD?00WRD_S_${yyyy}${doy}${hh}${startmin}_15M_?N.rnx"; # BRDC00WRD_S_20191220900_15M_MN.rnx
|
---|
813 | }
|
---|
814 | else { # daily?
|
---|
815 | $fileMask = $fileMask = "BRD?00WRD_S_${yyyy}${doy}0000_01D_?N.rnx";
|
---|
816 | }
|
---|
817 | }
|
---|
818 | else { # Rnx2
|
---|
819 | $fileMask = "BRD?${doy}*.${yy}N";
|
---|
820 | }
|
---|
821 |
|
---|
822 | my @rnxFiles = glob "$rnxPath/$fileMask";
|
---|
823 | if ( scalar @rnxFiles < 1 ) {
|
---|
824 | ERROR("BNC does not create RINEX Nav files. (Filemask: \"$fileMask\" Path: $rnxPath)");
|
---|
825 |
|
---|
826 | #return 0;
|
---|
827 | }
|
---|
828 | }
|
---|
829 |
|
---|
830 | # Check jobs making PPP
|
---|
831 | # ---------------------
|
---|
832 | if ( $bncConf->{'PPP'}->{'corrMount'} && $bncConf->{'PPP'}->{'staTable'} ) {
|
---|
833 | my $timeOfLastCoo = `grep "NEU:" $bncLogFile | tail -1 | cut -d ' ' -f1,2`;
|
---|
834 | chomp $timeOfLastCoo;
|
---|
835 | if ( !$timeOfLastCoo ) {
|
---|
836 | ERROR "BNC does not compute coordinates";
|
---|
837 | return 0;
|
---|
838 | }
|
---|
839 |
|
---|
840 | my $tp = Time::Piece->strptime( $timeOfLastCoo, '%y-%m-%d %H:%M:%S' );
|
---|
841 | my $now = Time::Piece->new;
|
---|
842 | my $tdiff = $now - $tp;
|
---|
843 | if ( $tdiff > 1200 ) {
|
---|
844 | ERROR( "Last computed coordinates are " . $tdiff / 60 . " min old" );
|
---|
845 | return 0;
|
---|
846 | }
|
---|
847 | }
|
---|
848 |
|
---|
849 | # BNC works
|
---|
850 | return 1;
|
---|
851 | }
|
---|
852 |
|
---|
853 | 1; # End of Bnc
|
---|