package Bnc;

# Perl utility functions for BNC
#
# Revision: $Header: trunk/BNC/scripts/Bnc.pm 10809 2026-02-09 09:45:42Z stuerze $

use strict;
use warnings;
use File::Basename;
use File::Spec::Functions qw(catfile);
use File::Temp qw(tempfile);
use Exporter;
use Time::Piece 1.30;
use PDL::Lite;    # to avoid namespace pollution
use PDL::Primitive;
use Log::Log4perl qw(:easy);

# use List::MoreUtils qw(uniq); # Prototype mismatch with PDL (uniq)

# =============================================================================
# callBnc ($bnc_ini, %$opts_ref)
# =============================================================================
# Call BNC
#
# Param  : $bnc_ini  [optional] The BNC config file that should be used. If not set, the default config is ised.
#          $opts_ref [required] Hash with BNC config options as key. They will overwrite the settings from the config file.
# Return : BNC exit status
# =============================================================================
sub callBnc {
	my ( $bnc, $bnc_ini, $opts ) = @_;

	my $config_file = "";
	if ( $bnc_ini && -s $bnc_ini ) {
		$config_file = "--conf $bnc_ini";
	}
	else {
		DEBUG("callBnc: Use default bnc-ini file");
	}

	my $opts_str = "";
	if ($opts) { $opts_str = options2string($opts) }

	#    my @cmd = (
	#                'xvfb-run',
	#                "--server-args='-screen 0, 1280x1024x8'",    # 1024x768x24
	#                "$bnc",
	#                "--nw",
	#                $opts_str,
	#    );

#my $rc = call_system("xvfb-run -a -e /home/user/xvfb.err --server-args='-screen 0 1280x1024x8' $bnc --nw $config_file $opts_str");
	return Common::runCmd("$bnc --nw $config_file $opts_str");
}

# Converts options map to string used for calling BNC.
sub options2string {
	my ($opts) = shift;

	my $opts_str = "";
	if ($opts) {
		foreach my $key ( keys %{$opts} ) {
			$opts_str .= "--key $key" . ' "' . $opts->{$key} . '" ';
		}
	}
	return $opts_str;
}

# =============================================================================
# parseMessageTypesFromFile
# =============================================================================
# Parse Message-Types with repetition rate from a BNC/scanRTCM logfile
#
# Param  : $logfile     [required] Path of the BNC/scanRTCM logfile
#          $caAbbr      [optional] caster name or abbreviation
#          $tmpPath     [optional] write working files to this path
#
# Return : Message-types for each mountpoint (as Hash-Ref)
#          Hash with mp as key and the list of messTypes as value
#          Example: $VAR1 = { 'RIO10' => ['1004(1)','1006(15)','1008(15)',...],
#                             'CLIB0' => ['1004(1)','1006(10)','1008(10)',...],
#                           };
# =============================================================================
sub parseMessageTypesFromFile {
	my ( $logfile, $caAbbr, $mytmpPath ) = @_;

	unless ( -s $logfile ) {
		ERROR "File [$logfile] is empty or does not exist";
		return;
	}

	my $tmp = File::Temp->new( UNLINK => 1, SUFFIX => '.messtyps' );
	my $casterMessTypeFile = $tmp->filename;

	#my $caster = $logfilename =~ s/\.log\.messtyps//r;
	#INFO "Process caster '$caster'";

	INFO "Process $logfile";

# scan beginns with that line: 16-02-18 23:58:02 WTZ37: Get data in RTCM 3.x format

# First grep for message type lines in bnc-logfile and write them to local temp. directory
# NOTE: 'sort -u' because Ephemeries message-types 1019 (GPS), 1020 (GLONASS), 1045 (Galileo) come
#       for every sat. at the same time/second. This is done for getting the repetition rate
	system(
"grep \"Received message type\" $logfile | sort -u > $casterMessTypeFile"
	  ) == 0
	  or ERROR "Fehler: $!";

	# --------------------------------------------
	# Get message types foreach station/mountpoint
	# --------------------------------------------
	my $cmd = "cat $casterMessTypeFile | awk '{print \$3, \$7}' | sort -u";
	my @messTypes = `$cmd`;    # ['DARX1: 1004\n','DARX1: 1006\n',... ]
	if ( scalar @messTypes < 1 ) {
		ERROR "Could not retrieve message types from file $casterMessTypeFile";
		return;
	}

# Note: Skip first 1500 lines because BNC is weird here, all lines with same timestamp, buffer problem?

# ------------------------------------------------------
# Guess repetition rate for each mountpoint/message-type
# ------------------------------------------------------
# For that get the first $minSamples appearances of mountp with same message type
# and build the differences between them
	my $minSamples = 4;
	foreach my $mpType (@messTypes) {
		chomp $mpType;    # 'AUBG3: 1004'
		my ( $mp, $mt ) = split( /:\s*/, $mpType );
		my $cmd  = "grep \"$mp: Received message type $mt\" $logfile";
		my @rows = `$cmd`;
		if ( scalar @rows < 1 ) {
			ERROR
"Could not retrieve message types for $mpType from file $casterMessTypeFile";
			next;
		}

		if ( scalar @rows <= $minSamples ) {
			WARN
			  "Could not guess repetition rate for message type $mpType: only "
			  . scalar @rows
			  . " matches found";
			next;
		}

		my $repRate = _computeMessTypRepetitonRate( \@rows, $caAbbr );
		if ($repRate) {
			$mpType .= '(' . $repRate . ')';
		}
	}    # -----  end foreach messageType -----

#if ( unlink $casterMessTypeFile ) { TRACE "Removed file [$casterMessTypeFile]" }

	# Return an HASH OF ARRAYS with mp as key and the list of messTypes as value
	my %messTypesHash;
	foreach (@messTypes) {
		my @ele = split( ': ', $_ );
		my $mp  = shift @ele;
		push( @{ $messTypesHash{$mp} }, shift @ele );
	}

	return \%messTypesHash;
}

# =============================================================================
# _computeMessTypRepetitonRate
# =============================================================================
# Guess repetition rate of message types
#
# Param  : $firstMatches [required] Array-Ref with first appearances of station
#          and mess type. Complete logfile lines, e.g.
#          '13-07-04 17:38:26 BRUX0: Received message type 1004 '
#          $caAbbr       [optional] caster abbreviation
# Return : repetition rate in secs, (median value)
# =============================================================================
sub _computeMessTypRepetitonRate {
	my ( $rows, $caAbbr ) = @_;

	my $maxGap = 600;    # if gap > 10min then we guess it is a new scan

	$rows->[0] =~ /.{18}([a-x0-9]+): Received message type (\d+)/i;
	my $stat  = $1;
	my $mestp = $2;

	# Create list of unix timestamps
	my @scans;
	my $scan = 0;
	my ( $prevTime, $deltaT ) = ( 0, 0 );
	foreach (@$rows) {
		my $uxtime = date2unix( substr( $_, 0, 17 ) );
		if ( !$uxtime ) {
			ERROR "Could not parse date from line $_";
			next;
		}
		$deltaT = $uxtime - $prevTime;
		next if ( $deltaT == 0 ); # e.g. eph 1019,1020 one message for each sat.
		next if ( $deltaT <= 1 && $mestp =~ /10(19|20|42|43|44|45|46)|63/ );
		if ( $prevTime && $deltaT > $maxGap ) {
			$scan++;
		}
		push( @{ $scans[$scan] }, $uxtime );
		$prevTime = $uxtime;
	}

	my @repRates;
	my $highest_nof_diffs = 0;
	foreach (@scans) {
		my @timestamps = @{$_};

		# Compute the differences
		my @diffs;
		for ( my $i = 1 ; $i <= $#timestamps ; $i++ ) {
			push( @diffs, $timestamps[$i] - $timestamps[ $i - 1 ] );
		}
		my $nof_diffs = scalar @diffs;

		if ( $nof_diffs < 2 ) {
			WARN("$stat: $mestp: only $nof_diffs diffs");
			next;
		}

		my ( $mean, $prms, $median, $min, $max, $adev, $rms_n ) =
		  stats( pdl \@diffs );
		$mean  = sprintf( "%.0f",  $mean );
		$rms_n = sprintf( "%.02f", $rms_n );
		print $stat, ": ", $mestp, ": ", join( ' ', @diffs ),
		  "[Sig: $mean, $rms_n]\n";

		if ( $rms_n > 10 ) {
			WARN("$stat: $mestp: RMS too high: $rms_n");
			next;
		}

		# get the most frequent value
		my %counti = ();
		$counti{$_}++ foreach (@diffs);
		my ( $ni, $mfv ) = ( 0, 0 );
		while ( my ( $k, $v ) = each %counti ) {
			if ( $v > $ni ) {
				$ni  = $v;
				$mfv = $k;
			}
		}

		my $rounded_val = $mfv;    # init
		foreach ( ( 1, 5, 10, 15, 30, 60, 120, 150, 300 ) )
		{                          # most likely values
			my $mdiff = abs( $mfv - $_ );
			if ( $mdiff <= 2 ) {
				$rounded_val = $_;
			}
		}
		push( @repRates, [ $rounded_val, $nof_diffs ] );

		if ( $nof_diffs > $highest_nof_diffs ) {
			$highest_nof_diffs = $nof_diffs;
		}
	}    # -----  end foreach scan -----

	my @mostLikelyRates = grep { $_->[1] == $highest_nof_diffs } @repRates;
	my $mostLikelyRate  = $mostLikelyRates[0]->[0];
	foreach (@repRates) {
		if ( abs( $_->[0] - $mostLikelyRate ) > 2 ) {
			ERROR
"$stat: $caAbbr: $mestp: repetition rates from different scans differ: $mostLikelyRate $_->[0]";
			if ( scalar @repRates == 2 ) {
				return;
			}
		}
	}

	return $mostLikelyRate;
}

# =============================================================================
# parseConfig ($confFile)
# =============================================================================
# Parse the BNC config file.
#
# Param  : $confFile [required] BNC config file
# Return : Hash with configuration on success, otherwise undef
# Usage  : $bncConf = parseConf($bncConfFile);
#          $corrMount = $bncConf->{'PPP'}->{'corrMount'};
# =============================================================================
sub parseConfig {
	my ($confFile) = @_;

	-s $confFile || LOGDIE "BNC config file \"$confFile\" does not exist\n";
	TRACE "Parse BNC config file $confFile";
	open( my $INP, '<', $confFile )
	  || die "Could not open file '$confFile': $!";
	my @confLines = <$INP>;
	close($INP);

	my %conf;
	my $section;    # [General], [PPP]
	foreach (@confLines) {
		chomp;
		s/#.*//;     # entfernt Kommentare
		s/^\s*//;    # whitespace am Anfang entfernen
		s/\s+$//;    # entfernt alle whitespaces am Ende
		next unless length;
		if ( $_ =~ /\[(\S+)\]/ ) { $section = $1 }
		next if ( !$section );
		my ( $key, $val ) = split( /\s*=\s*/, $_, 2 );
		if ( !defined $val ) { $val = "" }

		if ( $key eq "mountPoints" ) {

			# Simple parsing
			$val =~ s/^\/\///;
			my @mpts = split( /,\s?\/{2}/, $val );
			$conf{$section}->{$key} = \@mpts;

			# Extended parsing
			my @mpts_def = ();
			foreach (@mpts) {

			#  user:passwd@igs-ip.net:2101/ASPA0 RTCM_3.0 ASM -14.33 189.28 no 1
				if ( $_ =~
/^([\w-]+):(.+[^@])@([\w\.-]+):(\d{3,5})\/([-\w]+) ([\w\.]+) ?(\w{3})? ([\+\-\d\.]+) ([\+\-\d\.]+) no (\w+)/i
				  )
				{
					push(
						@mpts_def,
						{
							caster    => $3,
							port      => $4,
							mp        => $5,
							ntripVers => $10
						}
					);
				}
				else {
					ERROR "$confFile: Could not parse mountPoints string $_";
				}
			}
			$conf{$section}->{'mountPoints_parsed'} = \@mpts_def;
		}
		elsif ( $key eq "cmbStreams" ) {
			my @cmbStrs = split( /\s*,\s*/, $val );
			foreach (@cmbStrs) {
				s/"//g;
				s/\s+$//;    # entfernt alle whitespaces am Ende
			}
			$conf{$section}->{$key} = \@cmbStrs;
		}
		else { $conf{$section}->{$key} = $val }
	}

	my @nofPar = keys %conf;
	if ( scalar @nofPar < 1 ) {
		ERROR "No parameter found in BNC conf \"$confFile\"";
		return;
	}
	return \%conf;
}

# =============================================================================
# parseLogfile ($file, $sampling, $goBackSecs, $logMode )
# =============================================================================
# Parse BNCs' logfile
#
# Param  : $file       [required] BNC logfile
#          $sampling   [optional] sampling rate for logfile
#          $logMode    [optional] Flag. If set, remember the position of the file-read
#                      for the next read. Default: off
# Return : \%data
# =============================================================================
sub parseLogfile {
	my $file     = shift;
	my $sampling = shift // 1;
	my $logMode  = shift // 0;

	open( my $fh, "<", $file ) || LOGDIE "Could not open file $file: $!\n";

	# Goto last position from last read
	#my $fPos = filePosition($file);
	#TRACE "Current file pos: $fPos";
	$logMode && seek( $fh, &Common::filePosition($file), 0 );

	#$logMode && seek ( $fh, $fPos, 0 );
	my $ln = "";
	my ( @hlp, @epochs, @latencies, @restarts );
	my $rec = {};
	while (<$fh>) {
		chomp( $ln = $_ );
		$rec = {};

		if ( $ln =~ /\bNEU/ ) {    # NEU displacements
			@hlp = split( /\s+/, $ln );
			my $tp = Time::Piece->strptime( substr( $hlp[2], 0, 19 ),
				'%Y-%m-%d_%H:%M:%S' );

			if (   $hlp[14] eq '-nan'
				|| $hlp[15] eq '-nan'
				|| $hlp[16] eq '-nan' )	{
				WARN("$hlp[2] $hlp[3]: NEU displacements are NAN");
				next;
			}

			#DEBUG ($tp->epoch, $hlp[3]);
			push(
				@epochs,
				{
					time => $tp->epoch,
					site => $hlp[3],
					dN   => $hlp[14],
					dE   => $hlp[15],
					dU   => $hlp[16],
					TRP  => $hlp[18] + $hlp[19],
				}
			);
		}
		elsif ( index( $ln, "latency", 25 ) > 1 ) {

# DEBUG ($ln);
# 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
# 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
			@hlp = split( /\s+/, $ln );

			# old latency log format
			if ( $hlp[2] =~ /:$/ ) {
				splice @hlp, 3, 0, 'Placeholder:';
				$hlp[2] =~ s/:$//;
			}
			$hlp[3] =~ s/:$//;

			my $tp =
			  Time::Piece->strptime( "$hlp[0] $hlp[1]", '%y-%m-%d %H:%M:%S' );
			$rec = {
				time    => $tp->epoch,
				mp      => $hlp[2],
				meanLat => $hlp[6] + 0.0,
				epochs  => int( $hlp[14] ),
				type    => $hlp[3]
			};

		   # Unter bestimmten Bedingungen werden die gaps nicht rausgeschrieben!
			if ( $ln =~ /gaps/ ) {
				$rec->{'gaps'} = int( $hlp[16] );
			}

			push( @latencies, $rec );
		}
		elsif ( index( $ln, "Start BNC" ) > 1 ) {

			# 17-06-13 07:06:58 ========== Start BNC v2.12.3 (LINUX) ==========
			@hlp = split( /\s+/, $ln );
			my $tp =
			  Time::Piece->strptime( "$hlp[0] $hlp[1]", '%y-%m-%d %H:%M:%S' );
			push(
				@restarts,
				{
					time    => $tp->epoch,
					bncvers => $hlp[5]
				}
			);
		}

	}    # -----  next line  -----

	$logMode
	  && &Common::filePosition( $file, tell($fh) ); # Remember pos for next read
	close $fh;

	# Sampling must be done afterwords, for each station separated!
	my @epochs_sampled;
	my @sites = map { $_->{'site'} } @epochs;

	#@sites = uniq @sites;
	my %hlp1 = ();
	@sites = grep { !$hlp1{$_}++ } @sites;
	foreach my $s (@sites) {
		my $epoch_selected = 0;
		foreach my $rec (@epochs) {
			next if ( $rec->{'site'} ne $s );
			if ( $rec->{'time'} - $epoch_selected >= $sampling ) {
				push( @epochs_sampled, $rec );
				$epoch_selected = $rec->{'time'};
			}
		}
	}

	my %data = (
		EPOCHS    => \@epochs_sampled,
		LATENCIES => \@latencies,
		RESTARTS  => \@restarts
	);

	return \%data;
}

# =============================================================================
# parsePPPLogfile ($file, $sampling, $goBackSecs, $logMode )
# =============================================================================
# Parse BNCs' PPP station logfile
#
# Param  : $file       [required] BNC PPP station
#          $sampling   [optional] sampling rate for logfile
#          $goBackSecs [optional] go back that seconds from now in logfile
#          $logMode    [optional] Flag. If set, remember the position of the file-read
#                      for the next read. Default: off
# Return : $dateStr, $station, $ssrData \%data
# =============================================================================
sub parsePPPLogfile {
	my $file       = shift;
	my $sampling   = shift // 1;
	my $goBackSecs = shift // 0;
	my $logMode    = shift // 0;

	if ($logMode) { $goBackSecs = 0 }

	my $startSec;
	if ($goBackSecs) {
		$startSec = time() - $goBackSecs;
	}
	my $epo_ppp;
	my $old_epochSec = 0;
	my $epochSec     = 0;
	my $epochDiff    = 0;

    my ( @EPOCH_PPP);
	my ( @hlp, @N, @E, @U, @ISFIX, @NUMFIX, @TRP );
	my ( %SATNUM, %RECCLK, %AMB, %RES, %ELE, %ION, %BIA );
	my ( $date, $station, $lki, $sys, $sat, $amb );

	my ( @EPOCH_SSR);
	my ( %CLKCORR);
	my ( %YAW_SSR);
	my ( %YAW_DEF);
	my ( %CODEBIAS);
	my ( %PHASEBIAS);
	my ( %JUMPCOUNT);
	my $epochSsrSec = 0;
	my $ssrData;
	my $dateStr;

	open( my $fh, "<", $file ) || LOGDIE "Could not open file $file: $!\n";

	# Goto last position from last read
	#my $fPos = filePosition($file);
	#TRACE "Current file pos: $fPos";
	$logMode && seek( $fh, filePosition($file), 0 );

	#$logMode && seek ( $fh, $fPos, 0 );
	my $ln = "";
	while (<$fh>) {
		chomp( $ln = $_ );
		if ( $ln =~ /\bof Epoch\b/ ) {
			# PPP of Epoch 2015-08-27_14:00:15.000
			if ( $ln =~ /PPP of Epoch (\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})\.\d+/ ) {
				$epo_ppp = $1;    #print "$epo_ppp\n";
				@hlp = split( /\s+/, $ln );
                $ssrData = $hlp[5]; #print "$ssrData\n";
                $dateStr = substr( $hlp[3], 0, 10 );

			}
			else {
				ERROR "strange ppp line: \"$ln\""; next
			}

			my $tp = Time::Piece->strptime( $epo_ppp, '%Y-%m-%d_%H:%M:%S' );
			$epochSec  = $tp->epoch();
			$epochDiff = $epochSec - $old_epochSec;
			next;
		}

		next if ( !$epo_ppp );
		next if ( defined $startSec && $epochSec < $startSec );
		next if ( $epochDiff && $epochDiff < $sampling );

		@hlp = split( /\s+/, $ln );

		if ( $ln =~ /\bdN\b/ ) {
			#2025-11-29_16:55:09.000 WTZR00DEU0 X = 4075580.2482 +- 0.0500 Y = 931854.1736 +- 0.0314 Z = 4801568.3044 +- 0.0591 dN = 0.0074 +- 0.0407 dE = 0.0895 +- 0.0284 dU = -0.0072 +- 0.0672 fix 96 %
			push( @EPOCH_PPP, $epochSec );
			$old_epochSec = $epochSec;
			$date = substr( $hlp[0], 0, 9 );
			$station = $hlp[1];
		    my $numFix = 0;
		    my $isFix;
		    my $strFix = "fix";
			if (   $hlp[19] eq '-nan'
				|| $hlp[24] eq '-nan'
				|| $hlp[29] eq '-nan' )	{
				WARN("$hlp[0] $station: NEU displacements are NAN");
			}
			push @N, $hlp[19];
			push @E, $hlp[24];
			push @U, $hlp[29];

			if ( $hlp[32] eq $strFix ) {
				$isFix  = 1;
				$numFix = int( $hlp[33] );
			}
			else {
				$isFix  = 0;
				$numFix = 0;
			}
			push @ISFIX,  $isFix;
			push @NUMFIX, $numFix;
		}
		elsif ( ( $ln =~ /\bAMB\b/ ) && ( $ln !~ /RESET/ ) ) {		 # 2015-08... AMB lIF G04 253.0000 -8.9924 +- 1.7825 el = 22.03 epo = 86
			$lki = $hlp[2];
			$sat = $hlp[3];
			$sys = substr( $sat, 0, 1 );
			$amb = $hlp[4] + $hlp[5];
			push @{ $AMB{$lki}{$sys}{$sat}{EPOCH_PPP} },  $epochSec;
			push @{ $AMB{$lki}{$sys}{$sat}{DATA} },   $amb;
			push @{ $AMB{$lki}{$sys}{$sat}{NUMEPO} }, $hlp[13];
			push @{ $ELE{$sys}{$sat}{EPOCH_PPP} }, $epochSec;
			push @{ $ELE{$sys}{$sat}{DATA} },  $hlp[10];
		}
		elsif ( $ln =~ /\bRES\b/ && $ln !~ /Neglected/ ) {			# 2015-08... RES lIF G30  -0.0076
			$sat = $hlp[3];
			$lki = $hlp[2];
			$sys = substr( $sat, 0, 1 );

			#print "$epo $lki $sys $sat $res\n";
			push @{ $RES{$lki}{$sys}{$sat}{EPOCH_PPP} }, $epochSec;
			push @{ $RES{$lki}{$sys}{$sat}{DATA} },  $hlp[4];
		}
		elsif ( ( $ln =~ /\bION\b/ ) && ( $ln !~ /RESET/ ) ) {		# 2018-12-01_20:37:58.000 ION      G02     0.0000    -0.3277 +-   2.4663
			$sat = $hlp[2];
			$sys = substr( $sat, 0, 1 );
			push @{ $ION{$sys}{$sat}{EPOCH_PPP} }, $epochSec;
			push @{ $ION{$sys}{$sat}{DATA} },  $hlp[4];
		}
		elsif ( ( $ln =~ /\bBIA\b/ ) && ( $ln !~ /RESET/ ) ) {		# 2020-12-09_00:55:19.000 BIA  c1  G       0.0000    +2.5149 +-   9.6543
			$lki = $hlp[2];
			$sys = $hlp[3];
			push @{ $BIA{$lki}{$sys}{EPOCH_PPP} }, $epochSec;
			push @{ $BIA{$lki}{$sys}{DATA} },  $hlp[4] + $hlp[5];
		}
		elsif ( $ln =~ /\bSATNUM\b/ ) {    # 2015-09... SATNUM G  8
			push( @{ $SATNUM{ $hlp[2] } }, $hlp[3] );
		}
		elsif ( $ln =~ /\bTRP\b/ ) {  # 2015-08... TRP  2.3803 +0.1009 +- 0.0324
			push( @TRP, $hlp[2] + $hlp[3] );
		}
		elsif ( $ln =~ /\bREC_CLK\b/ ) {		 # 2024-10-20_03:57:30.000 RECCLK  G       0.0000    -1.7861 +-   0.5268
			$sys = $hlp[2];
			push @{ $RECCLK{$sys}{EPOCH_PPP} }, $epochSec;
			push @{ $RECCLK{$sys}{DATA} },  $hlp[3] + $hlp[4];
		}
		##################
		# SSR parameters #
		##################
	    elsif ( $ln =~ /\bclkCorr\b/ && @hlp > 3) {
	    	# clkCorr 2025-11-17_13:50:45.000 C45 -18.576
	    	if ( $ln =~ /clkCorr (\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})\.\d+/ ) {
				$epochSsrSec = $1;    #print "$epochSsrSec\n";
			}
			else {
				ERROR "strange ssr line: \"$ln\""; next
			}
			my $tp = Time::Piece->strptime( $epo_ppp, '%Y-%m-%d_%H:%M:%S' );
			$epochSsrSec  = $tp->epoch();
			$date = substr( $hlp[0], 0, 9 );
			$sat = $hlp[2];
			$sys = substr( $sat, 0, 1 );
			push @{ $CLKCORR{$sys}{$sat}{EPOCH_SSR} }, $epochSsrSec;
			push @{ $CLKCORR{$sys}{$sat}{DATA} },  $hlp[3];
		}
	    elsif ( $ln =~ /\byawAngle\b/&& @hlp > 3 ) {
	    	# yawAngle 2025-11-17_13:50:40.000 G06 218.672 136.115
	    	if ( $ln =~ /yawAngle (\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})\.\d+/ ) {
				$epochSsrSec = $1;    #print "$epochSsrSec\n";
			}
			else {
				ERROR "strange ssr line: \"$ln\""; next
			}
			my $tp = Time::Piece->strptime( $epo_ppp, '%Y-%m-%d_%H:%M:%S' );
			$epochSsrSec  = $tp->epoch();
			$sat = $hlp[2];
			$sys = substr( $sat, 0, 1 );
			push @{ $YAW_SSR{$sat}{EPOCH_SSR} }, $epochSsrSec;
			push @{ $YAW_SSR{$sat}{DATA} },  $hlp[3];
			push @{ $YAW_DEF{$sat}{EPOCH_SSR} }, $epochSsrSec;
			push @{ $YAW_DEF{$sat}{DATA} },  $hlp[4];
		}
	    elsif ( $ln =~ /\bcodeBias\b/ && @hlp > 3) {
	    	#codeBias 2025-11-17_13:50:35.000 C45  1P  12.140  2I  13.330  5P  21.740  6I  20.190
	    	if ( $ln =~ /codeBias (\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})\.\d+/ ) {
				$epochSsrSec = $1;    #print "$epochSsrSec\n";
			}
			else {
				ERROR "strange ssr line: \"$ln\""; next
			}
			my $tp = Time::Piece->strptime( $epo_ppp, '%Y-%m-%d_%H:%M:%S' );
			my ($num_bias, $bias_type, $bias_value);
			$epochSsrSec  = $tp->epoch();
			$sat = $hlp[2];
			$sys = substr( $sat, 0, 1 );
			my $hlpSize = @hlp;
			for (my $i=3; $i < $hlpSize; $i++) {
              $bias_type  = $hlp[$i++];
              $bias_value = $hlp[$i];
              push @{ $CODEBIAS{$bias_type}{$sys}{$sat}{EPOCH_SSR} }, $epochSsrSec;
			  push @{ $CODEBIAS{$bias_type}{$sys}{$sat}{DATA} },  $bias_value;
            }
		}
	    elsif ( $ln =~ /\bphaseBias\b/ && @hlp > 3) {
	    	#phaseBias 2025-11-17_13:50:55.000 G02  315.00   0.000  1C  1 x  0.291  2W  1 x  0.408
	    	if ( $ln =~ /phaseBias (\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})\.\d+/ ) {
				$epochSsrSec = $1;    #print "$epochSsrSec\n";
			}
			else {
				ERROR "strange ssr line: \"$ln\""; next
			}
			my $tp = Time::Piece->strptime( $epo_ppp, '%Y-%m-%d_%H:%M:%S' );
			my ($num_bias, $bias_type, $bias_value, $jump_count, $int_flag);
			$epochSsrSec  = $tp->epoch();
			$sat = $hlp[2];
			$sys = substr( $sat, 0, 1 );
			my $hlpSize = @hlp;
			for (my $i=5; $i < $hlpSize; $i++) {
              $bias_type  = $hlp[$i++]; #print "$sat type: $bias_type";
              $jump_count = $hlp[$i++]; #print " count: $jump_count";
              $int_flag   = $hlp[$i++]; #print " flag: $int_flag";
              $bias_value = $hlp[$i];	#print " bias: $bias_value \n";
              push @{ $PHASEBIAS{$bias_type}{$sys}{$sat}{EPOCH_SSR} }, $epochSsrSec;
              push @{ $JUMPCOUNT{$bias_type}{$sys}{$sat}{EPOCH_SSR} }, $epochSsrSec;
			  push @{ $PHASEBIAS{$bias_type}{$sys}{$sat}{DATA} },  $bias_value;
			  push @{ $JUMPCOUNT{$bias_type}{$sys}{$sat}{DATA} },  $jump_count;
            }
		}
	}    # -----  next line  -----

	$logMode && filePosition( $file, tell($fh) );   # Remember pos for next read
	close $fh;

	my $nof_epochs = scalar @EPOCH_PPP;
	DEBUG(  "$station: epochs:$nof_epochs, North displac.: "
		  . scalar @N
		  . ", East displac.: "
		  . scalar @E
		  . ", Up displac.: "
		  . scalar @U
		  . ", isfix: "
		  . scalar @ISFIX
		  . ", numfix: "
		  . scalar @NUMFIX
		  . ", TRP:"
		  . scalar @TRP );
	if ( $nof_epochs != scalar @N ) {
		LOGDIE "number of epochs and residuals not equal\n";
	}
	if ( $nof_epochs != scalar @TRP ) {
		LOGDIE "number of epochs and TRP not equal\n";
	}

	if ( !$station ) { WARN "could not grep stationname from file: $file\n" }

	my %data = (
		EPOCH_PPP  => \@EPOCH_PPP,
		N          => \@N,
		E          => \@E,
		U          => \@U,
		ISFIX      => \@ISFIX,
		NUMFIX     => \@NUMFIX,
		TRP        => \@TRP,
		SATNUM     => \%SATNUM,
		RECCLK     => \%RECCLK,
		RES        => \%RES,
		AMB        => \%AMB,
		ELE        => \%ELE,
		ION        => \%ION,
		BIA        => \%BIA,
		EPOCH_SSR  => \@EPOCH_SSR,
		CLKCORR    => \%CLKCORR,
		YAW_SSR    => \%YAW_SSR,
		YAW_DEF    => \%YAW_DEF,
		CODEBIAS   => \%CODEBIAS,
		PHASEBIAS  => \%PHASEBIAS,
		JUMPCOUNT  => \%JUMPCOUNT,
	);

	return ($dateStr, $station, $ssrData, \%data, 0 );
}

# =============================================================================
# BncStillWorks ($bncConfFile)
# =============================================================================
# Checks if BNC is still working.
#
# BNC Jobs can still be alive (in processlist) but are not producing any more.
# This function checks if a BNC process is proper working.
#
# Param  : $bncConfFile [required] path of BNC config file
# Return : true if BNC is still working otherwise false.
# =============================================================================
sub BncStillWorks {
	my ($bncConfFile) = @_;

	my $timep = Time::Piece->new;

	# for safety if it is exatly at 00:00, add 30 sec
	my $min_tmp = $timep->strftime("%M");
	if ( $min_tmp =~ /00|15|30|45/ && $timep->strftime("%S") < 15 ) {
		$timep += 30;
		sleep 30;
	}
	my $yyyy = $timep->year;
	my $yy   = $timep->yy;
	my $doy  = sprintf "%03d", $timep->yday + 1;
	my $hh   = $timep->strftime("%H");
	my $h    = uc( chr( 65 + $hh ) );
	my $min  = $timep->min;
	my $startmin;
	if    ( $min < 15 )  { $startmin = "00" }
	elsif ( $min < 30 )  { $startmin = "15" }
	elsif ( $min < 45 )  { $startmin = "30" }
	elsif ( $min <= 59 ) { $startmin = "45" }
	my $bncConf        = parseConf($bncConfFile);
	my $bncLogFileStub = $bncConf->{'General'}->{'logFile'};

	# BNC log file
	# ------------
	my $bncLogFile =
	  "${bncLogFileStub}_" . $timep->strftime("%y%m%d");    # -> bnc.log_160425
	unless ( -s $bncLogFile ) {
		WARN("BNC logfile \"$bncLogFile\" is empty or does not exist");
		return 0;
	}

	# RINEX Obs Generation
	# --------------------
	if ( $bncConf->{'General'}->{'rnxPath'} ) {
		my $rnxPath = $bncConf->{'General'}->{'rnxPath'};
		$rnxPath =~ s/\/$//;

# Write Rnx3 files (i.e. long Rnx3 filenames) 2: on ('rnxV3filenames' is deprecated since 2.12.8!!!)
		my $writeRnxV3 = $bncConf->{'General'}->{'rnxV3'};
		my $rnxIntr    = $bncConf->{'General'}->{'rnxIntr'};
		my $fileMask;

		if ($writeRnxV3) {
			if ( $rnxIntr eq "1 hour" ) {
				$fileMask = "*_S_${yyyy}${doy}${hh}??_01H_30S_?O.rnx";
			}
			elsif ( $rnxIntr eq "15 min" ) {
				$fileMask = "*_S_${yyyy}${doy}${hh}${startmin}_15M_01S_?O.rnx";
			}
			else {    # daily?
				$fileMask = "*_S_${yyyy}${doy}????_01D_30S_?O.rnx"
				  ;    # HRAG00ZAF_S_20191220000_01D_30S_MO.rnx
			}
		}
		else {         # Rnx2
			if ( $rnxIntr eq "1 hour" ) {
				$fileMask = "????${doy}${h}.${yy}O";
			}
			elsif ( $rnxIntr eq "15 min" ) {
				$fileMask = "????${doy}${h}${startmin}.${yy}O";
			}
			else {     # daily?
				$fileMask = "????${doy}*.${yy}O";
			}
		}

		my @rnxFiles = glob "$rnxPath/$fileMask";
		if ( scalar @rnxFiles < 1 ) {
			ERROR(
"BNC does not create RINEX Obs files. (Filemask: \"$fileMask\" Path: $rnxPath)"
			);

			#return 0;
		}
	}

	# RINEX Ephemerides Generation
	# ----------------------------
	if ( $bncConf->{'General'}->{'ephPath'} ) {
		my $rnxPath = $bncConf->{'General'}->{'ephPath'};
		$rnxPath =~ s/\/$//;
		my $writeRnxV3 = $bncConf->{'General'}->{'ephV3'};
		my $rnxIntr    = $bncConf->{'General'}->{'ephIntr'};
		my $fileMask;

		if ($writeRnxV3) {
			if ( $rnxIntr eq "1 hour" ) {
				$fileMask = "BRD?00WRD_S_${yyyy}${doy}${hh}00_01H_?N.rnx";
			}
			elsif ( $rnxIntr eq "15 min" ) {
				$fileMask =
				  "BRD?00WRD_S_${yyyy}${doy}${hh}${startmin}_15M_?N.rnx"
				  ;    # BRDC00WRD_S_20191220900_15M_MN.rnx
			}
			else {     # daily?
				$fileMask = $fileMask =
				  "BRD?00WRD_S_${yyyy}${doy}0000_01D_?N.rnx";
			}
		}
		else {         # Rnx2
			$fileMask = "BRD?${doy}*.${yy}N";
		}

		my @rnxFiles = glob "$rnxPath/$fileMask";
		if ( scalar @rnxFiles < 1 ) {
			ERROR(
"BNC does not create RINEX Nav files. (Filemask: \"$fileMask\" Path: $rnxPath)"
			);

			#return 0;
		}
	}

	# Check jobs making PPP
	# ---------------------
	if ( $bncConf->{'PPP'}->{'corrMount'} && $bncConf->{'PPP'}->{'staTable'} ) {
		my $timeOfLastCoo =
		  `grep "NEU:" $bncLogFile | tail -1 | cut -d ' ' -f1,2`;
		chomp $timeOfLastCoo;
		if ( !$timeOfLastCoo ) {
			ERROR "BNC does not compute coordinates";
			return 0;
		}

		my $tp  = Time::Piece->strptime( $timeOfLastCoo, '%y-%m-%d %H:%M:%S' );
		my $now = Time::Piece->new;
		my $tdiff = $now - $tp;
		if ( $tdiff > 1200 ) {
			ERROR(
				"Last computed coordinates are " . $tdiff / 60 . " min old" );
			return 0;
		}
	}

	# BNC works
	return 1;
}

1;    # End of Bnc
