diff --git a/.gitignore b/.gitignore index f376a95..6a7ed22 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +navmon.pb.cc +navmon.pb.h +*.csv # Prerequisites *.d *~ diff --git a/Makefile b/Makefile index 82a5511..43007b9 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ CXXFLAGS:= -std=gnu++17 -Wall -O3 -MMD -MP -ggdb -fno-omit-frame-pointer -Iext/fmt-5.2.1/include/ -Iext/powerblog/ext/simplesocket -Iext/powerblog/ext/ -PROGRAMS = navparse ubxtool navnexus navrecv +PROGRAMS = navparse ubxtool navnexus navrecv navdump all: $(PROGRAMS) @@ -15,6 +15,10 @@ SIMPLESOCKETS=ext/powerblog/ext/simplesocket/swrappers.o ext/powerblog/ext/simpl navparse: navparse.o ext/fmt-5.2.1/src/format.o $(H2OPP) $(SIMPLESOCKETS) minicurl.o ubx.o bits.o navmon.pb.o gps.o g++ -std=gnu++17 $^ -o $@ -pthread -L/usr/local/lib -lh2o-evloop -lssl -lcrypto -lz -lcurl -lprotobuf # -lwslay +navdump: navdump.o ext/fmt-5.2.1/src/format.o bits.o navmon.pb.o + g++ -std=gnu++17 $^ -o $@ -pthread -lprotobuf + + navnexus: navnexus.o ext/fmt-5.2.1/src/format.o $(SIMPLESOCKETS) ubx.o bits.o navmon.pb.o storage.o g++ -std=gnu++17 $^ -o $@ -pthread -lprotobuf diff --git a/ephemeris.hh b/ephemeris.hh index c62dc69..fd207cd 100644 --- a/ephemeris.hh +++ b/ephemeris.hh @@ -11,7 +11,7 @@ void getCoordinates(int wn, double tow, const auto& iod, Point* p, bool quiet=tr double sqrtA = 1.0*iod.sqrtA / (1ULL<<19); double deltan = M_PI * 1.0*iod.deltan / (1LL<<43); - double t0e = 60.0*iod.t0e; + double t0e = iod.t0e; // t0e is PRE-SCALED double m0 = M_PI * 1.0*iod.m0 / (1LL<<31); double e = 1.0*iod.e / (1ULL<<33); double omega = M_PI * 1.0*iod.omega / (1LL<<31); @@ -65,7 +65,7 @@ void getCoordinates(int wn, double tow, const auto& iod, Point* p, bool quiet=tr double A3 = pow(sqrtA, 6.0); double n0 = sqrt(mu/A3); - double tk = tow - t0e; // in seconds, ignores WN!!! XX!!! ! XX + double tk = tow - t0e; // in seconds, should do ephAge double n = n0 + deltan; if(!quiet) diff --git a/gps.hh b/gps.hh index a3dddd2..d2e5d41 100644 --- a/gps.hh +++ b/gps.hh @@ -4,14 +4,14 @@ #include std::basic_string getCondensedGPSMessage(std::basic_string_view payload); -// expects input as 24 bit read to to use messages -void parseGPSMessage(std::basic_string_view cond, auto& out) +// expects input as 24 bit read to to use messages, returns frame number +int parseGPSMessage(std::basic_string_view cond, auto& out, uint8_t* pageptr=0) { using namespace std; int frame = getbitu(&cond[0], 24+19, 3); // 10 * 4 bytes in payload now out.tow = 1.5*(getbitu(&cond[0], 24, 17)*4); - cerr << "Preamble: "< cond, auto& out) // 1-8: LSB of IODC // 9-24: - out.wn = getbitu(&cond[0], 2*24, 10); + out.wn = 2048 + getbitu(&cond[0], 2*24, 10); out.ura = getbitu(&cond[0], 2*24+12, 4); out.gpshealth = getbitu(&cond[0], 2*24+16, 6); - cerr<<"GPS Week Number: "<< out.wn <<", URA: "<< (int)out.ura<<", health: "<< - (int)out.gpshealth < cond, auto& out) eph.cuc = getbits(&cond[0], 5*24, 16); // 2^-29 RADIANS eph.cus = getbits(&cond[0], 7*24, 16); // 2^-29 RADIANS - out.checkCompleteAndClean(); + out.checkCompleteAndClean(iod); } else if(frame == 3) { - out.iod = getbitu(&cond[0], 9*24, 8); - auto& eph = out.getEph(out.iod); + int iod = getbitu(&cond[0], 9*24, 8); + auto& eph = out.getEph(iod); eph.words[3]=1; eph.cic = getbits(&cond[0], 2*24, 16); // 2^-29 RADIANS eph.omega0 = getbits(&cond[0], 2*24 + 16, 32); // 2^-31 semi-circles @@ -78,24 +78,28 @@ void parseGPSMessage(std::basic_string_view cond, auto& out) eph.omegadot = getbits(&cond[0], 8*24, 24); // 2^-43, semi-circles/s eph.idot = getbits(&cond[0], 9*24+8, 14); // 2^-43, semi-cirlces/s - out.checkCompleteAndClean(); + out.checkCompleteAndClean(iod); } else if(frame == 4) { // this is a carousel frame int page = getbitu(&cond[0], 2*24 + 2, 6); - cerr<<"Frame 4, page "< 56 // page 25 -> 63 // 2-10 -> 25 -> 32 ?? @@ -103,4 +107,5 @@ void parseGPSMessage(std::basic_string_view cond, auto& out) else if(frame == 5) { // this is a caroussel frame // cerr<<"Frame 5, SV: "<"+d["utc-offset-ns"].toFixed(2)+" ns, Galileo-GPS offset: "+d["gps-offset-ns"].toFixed(2)+" ns, "+d["leap-seconds"]+" leap seconds"); + lastseen = moment(1000*d["last-seen"]); + d3.select("#freshness").html(lastseen.fromNow()); }); d3.json("svs", function(d) { diff --git a/html/index.html b/html/index.html index 73b0ba9..cd8155f 100644 --- a/html/index.html +++ b/html/index.html @@ -18,7 +18,7 @@ tr:nth-child(even) {background: #CCC} tr:nth-child(odd) {background: #FFF} - Live:
+ Last update:

@@ -33,13 +33,15 @@ tr:nth-child(odd) {background: #FFF} Some technical detail behind this setup can be found in this post. - For updates, follow @GalileoSats on Twitter. + For updates, follow @GalileoSats on Twitter, or join us on our IRC channel (chat) via the + web gateway. The meaning of the fields is as follows: + diff --git a/navdump.cc b/navdump.cc new file mode 100644 index 0000000..31f69c8 --- /dev/null +++ b/navdump.cc @@ -0,0 +1,80 @@ +#include +#include +#include +#include +#include "fmt/format.h" +#include "fmt/printf.h" +#include +#include +#include +#include +#include +#include +#include +#include "ubx.hh" +#include "bits.hh" +#include "minivec.hh" +#include "navmon.pb.h" +#include "ephemeris.hh" +#include "gps.hh" +#include +using namespace std; + +static std::string humanTime(time_t t) +{ + struct tm tm; + gmtime_r(&t, &tm); + + char buffer[80]; + strftime(buffer, sizeof(buffer), "%a, %d %b %Y %T %z", &tm); + return buffer; +} + + +int main(int argc, char** argv) +{ + for(;;) { + char bert[4]; + if(read(0, bert, 4) != 4 || bert[0]!='b' || bert[1]!='e' || bert[2] !='r' || bert[3]!='t') { + cerr<<"EOF or bad magic"< inav((uint8_t*)nmm.gi().contents().c_str(), nmm.gi().contents().size()); + unsigned int wtype = getbitu(&inav[0], 0, 6); + cout << "galileo inav for "< g_clients; std::string g_storage; -std::multimap, string> g_history; - - void unixDie(const std::string& str) { throw std::runtime_error(str+string(": ")+string(strerror(errno))); @@ -64,19 +61,15 @@ try cerr<<"New downstream client "< start = {0,0}; - start.first = time(0) - 1800; - + start.first = time(0) - 4*3600; // 4 hours of backlog + // so we have a ton of files, and internally these are not ordered map fpos; for(;;) { auto srcs = getSources(); vector nmms; - for(const auto& s: srcs) { - time_t t = time(0); - - cout< "< start) { + // don't drop data that is only 5 seconds too old + if(make_pair(nmm.localutcseconds() + 5, nmm.localutcnanoseconds()) >= start) { nmms.push_back(nmm); } ++looked; @@ -115,9 +109,13 @@ try buf+=out; SWriten(clientfd, buf); } - if(!nmms.empty()) - start = {nmms.rbegin()->localutcseconds(), nmms.rbegin()->localutcnanoseconds()}; - sleep(1); + if(3600 + start.first - (start.first%3600) < time(0)) + start.first = 3600 + start.first - (start.first%3600); + else { + if(!nmms.empty()) + start = {nmms.rbegin()->localutcseconds(), nmms.rbegin()->localutcnanoseconds()}; + sleep(1); + } } } catch(std::exception& e) { diff --git a/navparse.cc b/navparse.cc index e52ec85..68bde0b 100644 --- a/navparse.cc +++ b/navparse.cc @@ -19,7 +19,7 @@ #include "navmon.pb.h" #include "ephemeris.hh" #include "gps.hh" - +#include using namespace std; struct EofException{}; @@ -70,8 +70,12 @@ struct SVIOD int32_t m0, omega0, i0, omega, idot, omegadot, deltan; int16_t cuc{0}, cus{0}, crc{0}, crs{0}, cic{0}, cis{0}; - uint16_t t0c; // clock epoch - int32_t af0, af1; + // 60 seconds + uint16_t t0c; // clock epoch, stored UNSCALED, since it is not in the same place as GPS + + // 2^-34 2^-46 + int32_t af0 , af1; + // 2^-59 int8_t af2; uint8_t sisa; @@ -93,7 +97,7 @@ void SVIOD::addGalileoWord(std::basic_string_view page) words[wtype]=true; gnssid = 2; if(wtype == 1) { - t0e = getbitu(&page[0], 16, 14); + t0e = getbitu(&page[0], 16, 14) * 60; // WE SCALE THIS FOR THE USER! m0 = getbits(&page[0], 30, 32); e = getbitu(&page[0], 62, 32); sqrtA = getbitu(&page[0], 94, 32); @@ -136,6 +140,10 @@ struct SVPerRecv }; +/* Most of thes fields are raw, EXCEPT: + t0t = seconds, raw fields are 3600 seconds (galileo), 4096 seconds (GPS) +*/ + struct SVStat { uint8_t e5bhs{0}, e1bhs{0}; @@ -146,16 +154,19 @@ struct SVStat int BGDE1E5a{0}, BGDE1E5b{0}; bool e5bdvs{false}, e1bdvs{false}; bool disturb1{false}, disturb2{false}, disturb3{false}, disturb4{false}, disturb5{false}; - uint16_t wn{0}; + uint16_t wn{0}; // we put the "unrolled" week number here! uint32_t tow{0}; // "last seen" - int32_t a0{0}, a1{0}, t0t{0}, wn0t{0}; + // + // 2^-30 2^-50 1 8-bit week + int32_t a0{0}, a1{0}, t0t{0}, wn0t{0}; int32_t a0g{0}, a1g{0}, t0g{0}, wn0g{0}; int8_t dtLS{0}, dtLSF{0}; uint16_t wnLSF{0}; uint8_t dn; // leap second day number - int ura, af0, af1, af2, t0c, iod; // GPS parameters that should not be here XXX + // 1 2^-31 2^-43 2^-55 16 second + int ura, af0, af1, af2, t0c; // GPS parameters that should not be here XXX map perrecv; - + pair deltaHz; double latestDisco{-1}; map iods; @@ -171,7 +182,7 @@ struct SVStat { prevIOD.first = -1; } - void checkCompleteAndClean(); + void checkCompleteAndClean(int iod); }; bool SVStat::completeIOD() const @@ -197,7 +208,7 @@ SVIOD SVStat::liveIOD() const throw std::runtime_error("Asked for unknown IOD"); } -void SVStat::checkCompleteAndClean() +void SVStat::checkCompleteAndClean(int iod) { if(iods[iod].complete()) { for(const auto& i : iods) { @@ -225,7 +236,7 @@ void SVStat::addGalileoWord(std::basic_string_view page) else if(wtype >=1 && wtype <= 4) { // ephemeris uint16_t iod = getbitu(&page[0], 6, 10); iods[iod].addGalileoWord(page); - checkCompleteAndClean(); + checkCompleteAndClean(iod); } else if(wtype==5) { // disturbance, health, time ai0 = getbitu(&page[0], 6, 11); @@ -255,8 +266,7 @@ void SVStat::addGalileoWord(std::basic_string_view page) a0 = getbits(&page[0], 6, 32); a1 = getbits(&page[0], 38, 24); dtLS = getbits(&page[0], 62, 8); - cerr<<"Setting a0,a1 to "<,SVStat>& ent, string_view name, auto value) { - d_buffer+= string(name) +",sv=" +to_string(ent.first.second)+",gnssid="+to_string(ent.first.first)+" value="+to_string(value)+ - " "+to_string(nanoTime(ent.second.wn, ent.second.tow))+"\n"; + d_buffer+= string(name)+",gnssid="+to_string(ent.first.first)+ +",sv=" +to_string(ent.first.second)+" value="+to_string(value)+ + " "+to_string(nanoTime(ent.first.first, ent.second.wn, ent.second.tow))+"\n"; checkSend(); } void addValue(pair id, string_view name, auto value) { + if(g_svstats[id].wn ==0 && g_svstats[id].tow == 0) + return; d_buffer+= string(name) +",gnssid="+to_string(id.first)+",sv=" +to_string(id.second) + " value="+to_string(value)+" "+ - to_string(nanoTime(g_svstats[id].wn, g_svstats[id].tow))+"\n"; + to_string(nanoTime(id.first, g_svstats[id].wn, g_svstats[id].tow))+"\n"; + checkSend(); } @@ -350,6 +371,7 @@ struct InfluxPusher string buffer; buffer.swap(d_buffer); // thread t([buffer,this]() { + if(d_dbname != "null") doSend(buffer); // }); // t.detach(); @@ -421,6 +443,38 @@ int ephAge(int tow, int t0e) } } +std::optional getHzCorrection(time_t now) +{ + int galcount{0}, gpscount{0}, allcount{0}; + double galtot{0}, gpstot{0}, alltot{0}; + + for(const auto& s: g_svstats) { + if(now - s.second.deltaHz.first < 60) { + alltot+=s.second.deltaHz.second; + allcount++; + if(s.first.first == 0) { + gpstot+=s.second.deltaHz.second; + gpscount++; + } + else if(s.first.first == 2) { + galtot+=s.second.deltaHz.second; + galcount++; + } + } + } + std::optional galHzCorr, gpsHzCorr, allHzCorr; + if(galcount > 3) + galHzCorr = galtot/galcount; + if(gpscount > 3) + gpsHzCorr = gpstot/gpscount; + if(allcount > 3) + allHzCorr = alltot/allcount; + + if(galHzCorr) + return galHzCorr; + return allHzCorr; +} + int main(int argc, char** argv) try { @@ -447,7 +501,7 @@ try if(s.first.first != 2) // Galileo only continue; int dw = (uint8_t)s.second.wn - s.second.wn0t; - int age = dw * 7 * 86400 + s.second.tow - s.second.t0t * 3600; + int age = dw * 7 * 86400 + s.second.tow - s.second.t0t; // t0t is pre-scaled utcstats[age]=s.first.second; uint8_t wn0g = s.second.wn0t; @@ -482,6 +536,11 @@ try h2s.addHandler("/svs", [](auto handler, auto req) { nlohmann::json ret = nlohmann::json::object(); + + + + auto hzCorrection = getHzCorrection(time(0)); + for(const auto& s: g_svstats) { nlohmann::json item = nlohmann::json::object(); if(!s.second.tow) // I know, I know, will suck briefly @@ -492,7 +551,7 @@ try item["sisa"]=humanUra(s.second.ura); else item["sisa"]=humanSisa(s.second.liveIOD().sisa); - item["eph-age-m"] = ephAge(s.second.tow, 60*s.second.liveIOD().t0e)/60.0; + item["eph-age-m"] = ephAge(s.second.tow, s.second.liveIOD().t0e)/60.0; item["af0"] = s.second.liveIOD().af0; item["af1"] = s.second.liveIOD().af1; item["af2"] = (int)s.second.liveIOD().af2; @@ -518,18 +577,28 @@ try item["x"]=p.x; item["y"]=p.y; item["z"]=p.z; + + if(time(0) - s.second.deltaHz.first < 60) { + item["delta_hz"] = s.second.deltaHz.second; + if(hzCorrection) + item["delta_hz_corr"] = s.second.deltaHz.second - *hzCorrection; + } + } item["a0"]=s.second.a0; item["a1"]=s.second.a1; item["dtLS"]=s.second.dtLS; - item["a0g"]=s.second.a0g; - item["a1g"]=s.second.a1g; - item["e5bdvs"]=s.second.e5bdvs; - item["e1bdvs"]=s.second.e1bdvs; - item["e5bhs"]=s.second.e5bhs; - item["e1bhs"]=s.second.e1bhs; - item["gpshealth"]=s.second.gpshealth; + if(s.first.first == 2) { + item["a0g"]=s.second.a0g; + item["a1g"]=s.second.a1g; + item["e5bdvs"]=s.second.e5bdvs; + item["e1bdvs"]=s.second.e1bdvs; + item["e5bhs"]=s.second.e5bhs; + item["e1bhs"]=s.second.e1bhs; + } + else + item["gpshealth"]=s.second.gpshealth; nlohmann::json perrecv = nlohmann::json::object(); for(const auto& pr : s.second.perrecv) { @@ -593,7 +662,7 @@ try g_svstats[id].perrecv[nmm.sourceid()].db = nmm.rd().db(); g_svstats[id].perrecv[nmm.sourceid()].el = nmm.rd().el(); g_svstats[id].perrecv[nmm.sourceid()].azi = nmm.rd().azi(); - g_svstats[id].perrecv[nmm.sourceid()].t = nmm.localutcseconds(); + // THIS HAS TO SPLIT OUT PER SOURCE @@ -607,6 +676,7 @@ try int sv = nmm.gi().gnsssv(); pair id={2,sv}; g_svstats[id].wn = nmm.gi().gnsswn(); + unsigned int wtype = getbitu(&inav[0], 0, 6); if(1) { // cout< id{nmm.rfd().gnssid(), nmm.rfd().gnsssv()}; - if(!nmm.rfd().gnssid()) {// GPS - dopplercsv << std::fixed << utcFromGST(nmm.rfd().rcvwn(), nmm.rfd().rcvtow()) <<" " << nmm.rfd().gnssid() <<" " < 10) { + g_svstats[id].deltaHz = {t, nmm.rfd().doppler() - preddop}; + idb.addValue(id, "delta_hz", nmm.rfd().doppler() - preddop); + auto corr = getHzCorrection(t); + if(corr) { + idb.addValue(id, "delta_hz_cor", nmm.rfd().doppler() - preddop - *corr); + } + } + + // cout<<"Had doppler for "<((uint8_t*)nmm.gpsi().contents().c_str(), nmm.gpsi().contents().size())); pair id{nmm.gpsi().gnssid(), nmm.gpsi().gnsssv()}; - auto& svstat = g_svstats[id]; - parseGPSMessage(cond, svstat); + g_svstats[id].perrecv[nmm.sourceid()].t = nmm.localutcseconds(); + + auto& svstat = g_svstats[id]; + uint8_t page; + int frame=parseGPSMessage(cond, svstat, &page); + if(frame == 1) { + idb.addValue(id, "af0", 8* svstat.af0); // scaled to galileo units - native gps: 2^-31 + idb.addValue(id, "af1", 8* svstat.af1); // scaled to galileo units - native gps: 2^-43 + idb.addValue(id, "af2", 16* svstat.af2); // scaled to galileo units + idb.addValue(id, "t0c", 16 * svstat.t0c); + + double age = ephAge(g_svstats[id].tow, g_svstats[id].t0c * 16); + + double offset = ldexp(1000.0*(1.0*g_svstats[id].af0 + ldexp(age*g_svstats[id].af1, -12)), -31); + idb.addValue(id, "atomic_offset_ns", 1000000.0*offset); + } + else if(frame==4 && page==18) { + idb.addValue(id, "a0", g_svstats[id].a0); + idb.addValue(id, "a1", g_svstats[id].a1); + int dw = (uint8_t)g_svstats[id].wn - g_svstats[id].wn0t; + int age = dw * 7 * 86400 + g_svstats[id].tow - g_svstats[id].t0t; // t0t is PRESCALED + + long shift = g_svstats[id].a0 * (1LL<<20) + g_svstats[id].a1 * age; // in 2^-50 seconds units + idb.addValue(id, "utc_diff_ns", 1.073741824*ldexp(1.0*shift, -20)); + } + + g_svstats[id].perrecv[nmm.sourceid()].t = nmm.localutcseconds(); + g_svstats[id].tow = nmm.gpsi().gnsstow(); + g_svstats[id].wn = nmm.gpsi().gnsswn(); + if(g_svstats[id].wn < 512) + g_svstats[id].wn += 2048; } else { cout<<"Unknown type "<< (int)nmm.type()<
svSatellite Vehicle, an identifier for a Galileo satellite. Not the actual name of the satellite, other satellites could take over this number in case of failures
iodIssue of Data. Satellites periodically get sent updates on their orbit & other details, each update has a new IOD number. It is coincidence that all SVs currently receive the same IOD numbers, this is by no means guaranteed. Currently however, if an SV has a lower IOD, it has not received new updates recently.
eph‑age‑mAge of ephemeris in minutes. Denotes how old the current set of orbit data is. Could be very old if SV is out of sight (see below). An acceptable limit is 4 hours (240 minutes).
latest-disco"jump" of the orbit prediction at the latest ephemeris change. Centimeters are good.
sisaSignal In Space Accuracy, how well the position of an SV is known.
e1bhs, e1bdvs, e5bhs, e5bdvsHealth flags for E1 (common) and E5 (uncommon) frequencies.
a0, a1Offset of the Galileo Standard Time to UTC. a0 is (more or less) the offset in nanoseconds, a1 is a measure of the rate of change