You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

450 lines
16 KiB

function [gnssRaw,gnssAnalysis] = ReadGnssLogger(dirName,fileName,dataFilter,gnssAnalysis)
%% [gnssRaw,gnssAnalysis]=ReadGnssLogger(dirName,fileName,[dataFilter],[gnssAnalysis]);
% Read the log file created by Gnss Logger App in Android
% Compatible with Android release N
%
% Input:
% dirName = string with directory of fileName,
% e.g. '~/Documents/MATLAB/Pseudoranges/2016-03-28'
% fileName = string with filename
% optional inputs:
% [dataFilter], nx2 cell array of pairs of strings,
% dataFilter{i,1} is a string with one of 'Raw' header values from the
% GnssLogger log file e.g. 'ConstellationType'
% dataFilter{i,2} is a string with a valid matlab expression, containing
% the header value, e.g. 'ConstellationType==1'
% See SetDataFilter.m for full rules and examples of dataFilter.
% [gnssAnalysis] structure containing analysis, incl list of missing fields
%
% Output:
% gnssRaw, all GnssClock and GnssMeasurement fields from log file, including:
% .TimeNanos (int64)
% .FullBiasNanos (int64)
% ...
% .Svid
% .ReceivedSvTimeNanos (int64)
% .PseudorangeRateMetersPerSecond
% ...
% and data fields created by this function:
% .allRxMillis (int64), full cycle time of measurement (milliseconds)
% accurate to one millisecond, it is convenient for matching up time
% tags. For computing accurate location, etc, you must still use
% TimeNanos and gnssMeas.tRxSeconds
%
% gnssAnalysis, structure containing analysis, including list of missing fields
%
% see also: SetDataFilter, ProcessGnssMeas
%Author: Frank van Diggelen
%Open Source code for processing Android GNSS Measurements
%factored into a few main sub-functions:
% MakeCsv()
% ReadRawCsv()
% FilterData()
% PackGnssRaw()
% CheckGnssClock()
% ReportMissingFields()
%% Initialize outputs and inputs
gnssRaw = [];
gnssAnalysis.GnssClockErrors = 'GnssClock Errors.';
gnssAnalysis.GnssMeasurementErrors = 'GnssMeasurement Errors.';
gnssAnalysis.ApiPassFail = '';
if nargin<3, dataFilter = []; end
%% check we have the right kind of fileName
extension = fileName(end-3:end);
if ~any(strcmp(extension,{'.txt','.csv'}))
error('Expecting file name of the form "*.txt", or "*.csv"');
end
%% read log file into a numeric matrix 'S', and a cell array 'header'
rawCsvFile = MakeCsv(dirName,fileName);
[header,C] = ReadRawCsv(rawCsvFile);
%% apply dataFilter
[bOk] = CheckDataFilter(dataFilter,header);
if ~bOk, return, end
[bOk,C] = FilterData(C,dataFilter,header);
if ~bOk, return, end
%% pack data into gnssRaw structure
[gnssRaw,missing] = PackGnssRaw(C,header);
%% check clock and measurements
[gnssRaw,gnssAnalysis] = CheckGnssClock(gnssRaw,gnssAnalysis);
gnssAnalysis = ReportMissingFields(gnssAnalysis,missing);
end %end of function ReadGnssLogger
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function csvFileName = MakeCsv(dirName,fileName)
%% make csv file, if necessary.
%And return extended csv file name (i.e. with full path in the name)
%TBD, maybe, do this entirely with Matlab read/write functions, make independent
%from grep and sed
%make extended file name
if dirName(end)~='/'
dirName = [dirName,'/']; %add /
end
csvFileName = [dirName,'raw.csv'];
if strcmp(fileName(end-3:end),'.csv')
return %input file is a csv file, nothing more to do here
end
extendedFileName = [dirName,fileName];
fprintf('\nReading file %s\n',extendedFileName)
%% read version
txtfileID = fopen(extendedFileName,'r');
if txtfileID<0
error('file ''%s'' not found',extendedFileName);
end
line='';
while isempty(strfind(lower(line),'version'))
line = fgetl(txtfileID);
if ~ischar(line) %eof or error occurred
if isempty(line)
error('\nError occurred while reading file %s\n',fileName)
end
break
end
end
if line==-1
fprintf('\nCould not find "Version" in input file %s\n',fileName)
return
end
%look for the beginning of the version number, e.g. 1.4.0.0
iDigits = regexp(line,'\d'); %index into the first number found in line
v = sscanf(line(iDigits(1):end),'%d.%d.%d.%d',4);
if length(v)<4
v(end+1:4,1)=0; %make v into a length 4 column vector
end
%Now extract the platform
k = strfind(line,'Platform:');
if any(k)
sPlatform = line(k+9:end);
else
sPlatform = '';%set empty if 'Platform:' not found
end
if isempty(strfind(sPlatform,'N'))
%add || strfind(platform,'O') and so on for future platforms
fprintf('\nThis version of ReadGnssLogger supports Android N\n')
fprintf('WARNING: did not find "Platform" type in log file, expected "Platform: N"\n')
fprintf('Please Update GnssLogger\n')
sPlatform = 'N';%assume version N
end
v1 = [1;4;0;0];
sCompare = CompareVersions(v,v1);
%Note, we need to check both the logger version (e.g. v1.0.0.0) and the
%Platform version "e.g. Platform: N" for any logic based on version
if strcmp(sCompare,'before')
fprintf('\nThis version of ReadGnssLogger supports v1.4.0.0 onwards\n')
error('Found "%s" in log file',line)
end
%% write csv file with header and numbers
%We could use grep and sed to make a csv file
%fclose(txtfileID);
% system(['grep -e ''Raw,'' ',extendedFileName,...
% ' | sed -e ''s/true/1/'' -e ''s/false/0/'' -e ''s/# //'' ',...
% ' -e ''s/Raw,//'' ',... %replace "Raw," with nothing
% '-e ''s/(//g'' -e ''s/)//g'' > ',csvFileName]);
%
% On versions from v1.4.0.0 N:
% grep on "Raw," replace alpha characters amongst the numbers,
% remove parentheses in the header,
% note use of /g for "global" so sed acts on every occurrence in each line
% csv file "prs.csv" now contains a header row followed by numerical data
%
%But we'll do the same thing using Matlab, so people don't need grep/sed:
csvfileID = fopen(csvFileName,'w');
while ischar(line)
line = fgetl(txtfileID);
if isempty(strfind(line,'Raw,'))
continue %skip to next line
end
%Now 'line' contains the raw measurements header or data
line = strrep(line,'Raw,','');
line = strrep(line,'#',''); line = strrep(line,' ','');%remove '#' and spaces
%from versions v1.4.0.0 N we actually dont need to look for '(',')','true'
%or 'false' anymore. So we are done with replacing. That was easy.
fprintf(csvfileID,'%s\n',line);
end
fclose(txtfileID);
fclose(csvfileID);
if isempty(line) %line should be -1 at eof
error('\nError occurred while reading file %s\n',fileName)
end
end %end of function MakeCsv
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [header,C] = ReadRawCsv(rawCsvFile)
%% read data from csv file into a numerical matrix 'S' and cell array 'header'
S = csvread(rawCsvFile,1,0);%read numerical data from second row onwards
%Note csvread fills ,, with zero, so we will need a lower level read function to
%tell the difference between empty fields and valid zeros
%T = readtable(csvFileName,'FileType','text'); %use this to debug
%read header row:
fid = fopen(rawCsvFile);
if fid<0
error('file ''%s'' not found',rawCsvFile);
end
headerString = fgetl(fid);
if isempty(strfind(headerString,'TimeNanos'))
error('\n"TimeNanos" string not found in file %s\n',fileName)
end
header=textscan(headerString,'%s','Delimiter',',');
header = header{1}; %this makes header a numFieldsx1 cell array
numFields = size(header,1);
%check that numFields == size(S,2)
[~,M] = size(S); %M = number of columns
assert(numFields==M,...
'# of header names is different from # of columns of numerical data')
%read lines using formatSpec so we get TimeNanos and FullBiasNanos as
%int64, everything else as doubles, and empty values as NaN
formatSpec='';
for i=1:M
%lotsa || here, because we are comparing a vector, 'header'
%to a specific string each time. Not sure how to do this another way
%and still be able to easily read and debug. Better safe than too clever.
%longs
if i == find(strcmp(header,'TimeNanos')) || ...
i == find(strcmp(header,'FullBiasNanos')) || ...
i == find(strcmp(header,'ReceivedSvTimeNanos')) || ...
i == find(strcmp(header,'ReceivedSvTimeUncertaintyNanos')) || ...
i == find(strcmp(header,'CarrierCycles'))
formatSpec = sprintf('%s %%d64',formatSpec);
elseif 0
%ints
% TBD maybe %d32 for ints: AccumulatedDeltaRangeState, ...
% ConstellationType, MultipathIndicator, State, Svid
formatSpec = sprintf('%s %%d32',formatSpec);
else
%everything else doubles
formatSpec = sprintf('%s %%f',formatSpec);
end
end
%for empty fields, enter 'NaN' into csv
C = textscan(fid,formatSpec,'Delimiter',',','EmptyValue',NaN);
fclose(fid);
end% of function ReadRawCsv
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [bOk,C] = FilterData(C,dataFilter,header)
%% filter C based on contents of dataFilter
bOk = true;
iS = ones(size(C{1})); %initialize index into rows of C
for i=1:size(dataFilter,1)
j=find(strcmp(header,dataFilter{i,1}));%j = index into header
%we should always be a value of j, because checkDataFilter checks for this:
assert(any(j),'dataFilter{i} = %s not found in header\n',dataFilter{i,1})
%now we must evaluate the expression in dataFilter{i,2}, for example:
% 'BiasUncertaintyNanos < 1e7'
%assign the relevant cell of C to a variable with same name as the header
ts = sprintf('%s = C{%d};',header{j},j);
eval(ts);
%create an index vector from the expression in dataFilter{i,2}
ts = sprintf('iSi = %s;',dataFilter{i,2});
eval(ts);
%AND the iS index values on each iteration of i
iS = iS & iSi;
end
% Check if filter removes all values
if ~any(iS) %if all zeros
fprintf('\nAll measurements removed. Specify dataFilter less strictly than this:, ')
dataFilter(:,2)
bOk=false;
C=[];
return
end
% Keep only those values of C indexed by iS
for i=1:length(C)
C{i} = C{i}(iS);
end
end %end of function FilterDataS
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [gnssRaw,missing] = PackGnssRaw(C,header)
%% pack data into gnssRaw, and report missing fields
assert(length(C)==length(header),...
'length(C) ~= length(header). This should have been checked before here')
gnssRaw = [];
%report clock fields present/missing, based on:
gnssClockFields = {...
'TimeNanos'
'TimeUncertaintyNanos'
'LeapSecond'
'FullBiasNanos'
'BiasUncertaintyNanos'
'DriftNanosPerSecond'
'DriftUncertaintyNanosPerSecond'
'HardwareClockDiscontinuityCount'
'BiasNanos'
};
missing.ClockFields = {};
%report measurements fields present/missing, based on:
gnssMeasurementFields = {...
'Cn0DbHz'
'ConstellationType'
'MultipathIndicator'
'PseudorangeRateMetersPerSecond'
'PseudorangeRateUncertaintyMetersPerSecond'
'ReceivedSvTimeNanos'
'ReceivedSvTimeUncertaintyNanos'
'State'
'Svid'
'AccumulatedDeltaRangeMeters'
'AccumulatedDeltaRangeUncertaintyMeters'
};
%leave these out for now, 'cause we dont care (for now), or they're deprecated,
% or they could legitimately be left out (because they are not computed in
% a particular GNSS implementation)
% SnrInDb, TimeOffsetNanos, CarrierFrequencyHz, CarrierCycles, CarrierPhase,
% CarrierPhaseUncertainty
missing.MeasurementFields = {};
%pack data into vector variables, if the fields are not NaNs
for j = 1:length(header)
if any(isfinite(C{j})) %not all NaNs
%TBD what if there are some NaNs, but not all. i.e. some missing
%data in the log file - TBD deal with this
eval(['gnssRaw.',header{j}, '=C{j};']);
elseif any(strcmp(header{j},gnssClockFields))
missing.ClockFields{end+1} = header{j};
elseif any(strcmp(header{j},gnssMeasurementFields))
missing.MeasurementFields{end+1} = header{j};
end
end
%So, if a field is not reported, it will be all NaNs from makeCsv, and the above
%code will not load it into gnssRaw. So when we call 'CheckGnssClock' it can
%check for missing fields in gnssRaw.
%TBD look for all zeros that can not legitimately be all zero,
%e.g. AccumulatedDeltaRangeMeters, and report these as missing data
end %end of function PackGnssRaw
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [gnssRaw,gnssAnalysis,bOk] = CheckGnssClock(gnssRaw,gnssAnalysis)
%% check clock values in gnssRaw
bOk = true;
sFail = ''; %initialize string to record failure messafes
N = length(gnssRaw.ReceivedSvTimeNanos);
%Insist on the presence of TimeNanos (time from hw clock)
if ~isfield(gnssRaw,'TimeNanos')
s = ' TimeNanos missing from GnssLogger File.';
fprintf('WARNING: %s\n',s);
sFail = [sFail,s];
bOk = false;
end
if ~isfield(gnssRaw,'FullBiasNanos')
s = 'FullBiasNanos missing from GnssLogger file.';
fprintf('WARNING: %s, we need it to get the week number\n',s);
sFail = [sFail,s];
bOk = false;
end
if ~isfield(gnssRaw,'BiasNanos')
gnssRaw.BiasNanos = zeros(N,1);
end
if ~isfield(gnssRaw,'HardwareClockDiscontinuityCount')
%possibly non fatal error, we assume there is no hardware clock discontinuity
%so we set to zero and move on, but we print a warning
gnssRaw.HardwareClockDiscontinuityCount = zeros(N,1);
fprintf('WARNING: Added HardwareClockDiscontinuityCount=0 because it is missing from GNSS Logger file\n');
end
%check FullBiasNanos, it should be negative values
bChangeSign = any(gnssRaw.FullBiasNanos<0) & any(gnssRaw.FullBiasNanos>0);
assert(~bChangeSign,...
'FullBiasNanos changes sign within log file, this should never happen');
%Now we know FullBiasNanos doesnt change sign,auto-detect sign of FullBiasNanos,
%if it is positive, give warning and change
if any(gnssRaw.FullBiasNanos>0)
gnssRaw.FullBiasNanos = -1*gnssRaw.FullBiasNanos;
fprintf('WARNING: FullBiasNanos wrong sign. Should be negative. Auto changing inside ReadGpsLogger\n');
gnssAnalysis.GnssClockErrors = [gnssAnalysis.GnssClockErrors,...
sprintf(' FullBiasNanos wrong sign.')];
end
%compute full cycle time of measurement, in milliseonds
gnssRaw.allRxMillis = int64((gnssRaw.TimeNanos - gnssRaw.FullBiasNanos)*1e-6);
%allRxMillis is now accurate to one millisecond (because it's an integer)
if ~bOk
gnssAnalysis.ApiPassFail = ['FAIL ',sFail];
end
end %end of function CheckGnssClock
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function gnssAnalysis = ReportMissingFields(gnssAnalysis,missing)
%% report missing clock and measurement fields in gnssAnalysis
%report missing clock fields
if ~isempty(missing.ClockFields)
gnssAnalysis.GnssClockErrors = sprintf(...
'%s Missing Fields:',gnssAnalysis.GnssClockErrors);
for i=1:length(missing.ClockFields)
gnssAnalysis.GnssClockErrors = sprintf(...
'%s %s,',gnssAnalysis.GnssClockErrors,missing.ClockFields{i});
end
gnssAnalysis.GnssClockErrors(end) = '.';%replace final comma with period
end
%report missing measurement fields
if ~isempty(missing.MeasurementFields)
gnssAnalysis.GnssMeasurementErrors = sprintf(...
'%s Missing Fields:',gnssAnalysis.GnssMeasurementErrors);
for i=1:length(missing.MeasurementFields)
gnssAnalysis.GnssMeasurementErrors = sprintf(...
'%s %s,',gnssAnalysis.GnssMeasurementErrors,...
missing.MeasurementFields{i});
end
gnssAnalysis.GnssMeasurementErrors(end) = '.';%replace last comma with period
end
%assign pass/fail
if ~any(strfind(gnssAnalysis.ApiPassFail,'FAIL')) %didn't already fail
if isempty(missing.ClockFields) && isempty(missing.MeasurementFields)
gnssAnalysis.ApiPassFail = 'PASS';
else
gnssAnalysis.ApiPassFail = 'FAIL BECAUSE OF MISSING FIELDS';
end
end
end %end of function ReportMissingFields
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Copyright 2016 Google Inc.
%
% Licensed under the Apache License, Version 2.0 (the "License");
% you may not use this file except in compliance with the License.
% You may obtain a copy of the License at
%
%     http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS,
% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
% See the License for the specific language governing permissions and
% limitations under the License.