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.

452 lines
16 KiB

function [gnssRaw,gnssAnalysis] = ReadGnssLogger(dirName,fileName,dataFilterIn,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
% dataFilter = cell array, dataFilter.{i}=string with a valid matlab expression
% e.g. dataFilter{1} = 'ConstellationType==1'
%
% 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
gnssAnalysis.GnssClockErrors = 'GnssClock Errors.';
gnssAnalysis.GnssMeasurementErrors = 'GnssMeasurement Errors.';
gnssAnalysis.ApiPassFail = '';
if nargin<3, dataFilterIn = []; 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
[dataFilter] = CheckDataFilter(dataFilterIn,header);
C = FilterData(C,dataFilter,header);
%% pack data into gnssRaw structure
[gnssRaw,missing] = PackGnssRaw(C,header);
%% check clock and measurements
[gnssRaw,gnssAnalysis] = CheckGnssClock(gnssRaw,gnssAnalysis);
gnssAnalysis = ReportMissingFields(gnssAnalysis,missing);
%TBD on any early return, return gnssAnalysis.ApiPassFail = 'explanation'
% so that reporting tool reports why
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,'prs.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')
error('Found "%s" in log file, expected "Platform: N"',line)
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
C = textscan(fid,formatSpec,'Delimiter',',','EmptyValue',NaN);
fclose(fid);
end% of function ReadRawCsv
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [dataFilter] = CheckDataFilter(dataFilterIn,header)
%% check that dataFilter has matching values in the header,
% extract this header value and add it as a second column for datafilter
dataFilter = dataFilterIn(:); %make dataFilter a column array
if isempty(dataFilterIn)
return
end
N = length(dataFilter);
%check that each value of dataFilter is valid: i.e. it contains one of the
%header types from the GnssLogger file
L = length(header);
for i=1:N
foundInHeader = zeros(1,L);
for j=1:L
foundInHeader(j) = any(strfind(dataFilter{i,1},header{j}));
end
if ~any(foundInHeader) %no match found (too coold)
error('dataFilter value ''%s'' has no matches in log file header',...
dataFilter{i,1});
elseif sum(foundInHeader)>1 % too many matches found (tooo hot)
error('dataFilter value ''%s'' has >1 match in log file header',...
dataFilter{i,1});
else %one match found (juust right)
k = find(foundInHeader);%index into where we found the matching header
dataFilter{i,2} = header{k};%save in second column
end
end
%dataFilter now has two columns: second one contains the matching header type
end% of function CheckDataFilter
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function C = FilterData(C,dataFilter,header)
%% filter C based on contents of dataFilter
iS = ones(size(C{1})); %initialize index into rows of C
for i=1:size(dataFilter,1)
j=find(strcmp(header,dataFilter{i,2}));%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}, 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}
ts = sprintf('iSi = %s;',dataFilter{i,1});
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, ')
dataFilter(:,1)
return
end
%Now remove all 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
%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] = CheckGnssClock(gnssRaw,gnssAnalysis)
%% check clock values in gnssRaw
N = length(gnssRaw.ReceivedSvTimeNanos);
%Insist on the presence of TimeNanos (time from hw clock)
if ~isfield(gnssRaw,'TimeNanos')
error('TimeNanos data missing from GnssLogger file\n');
end
if ~isfield(gnssRaw,'FullBiasNanos')
error('FullBiasNanos is missing or zero, we need it to get gnssRaw week\n')
%TBD change to fatal warning, so a report is still generated, with warning
end
if ~isfield(gnssRaw,'BiasNanos')
gnssRaw.BiasNanos = zeros(N,1);
end
if ~isfield(gnssRaw,'HardwareClockDiscontinuityCount')
gnssRaw.HardwareClockDiscontinuityCount = zeros(N,1);
fprintf('WARNING: Added HardwareClockDiscontinuityCount because it is missing from GNSS Logger file\n');
end
%auto-detect sign of FullBiasNanos, if it is positive, give warning and change
if ~all(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
%now all FullBiasNanos should be negative - if there are any are > 0 it means
%something is very wrong with the log file, because FullBiasNanos has changed
%sign from a large negative to large positive number, and we must assert
assert(all(gnssRaw.FullBiasNanos<=0),...
'FullBiasNanos changes sign within log file, this should never happen')
%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)
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 isempty(missing.ClockFields) && isempty(missing.MeasurementFields)
gnssAnalysis.ApiPassFail = 'PASS';
else
gnssAnalysis.ApiPassFail = 'FAIL BECAUSE OF MISSING FIELDS';
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.