|
|
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.
|
|
|
|