#include #include #include #include #include "fmt/format.h" #include "fmt/printf.h" #include #include #include #include #include #include #include #include "ext/powerblog/h2o-pp.hh" #include "minicurl.hh" #include #include "ubx.hh" #include "bits.hh" #include "minivec.hh" #include "navmon.pb.h" #include "ephemeris.hh" #include "gps.hh" #include "glonass.hh" #include "beidou.hh" #include "galileo.hh" #include "tle.hh" #include #include "navmon.hh" #include #include "navparse.hh" using namespace std; struct ObserverPosition { Point pos; time_t lastSeen{0}; }; std::map g_srcpos; map g_beidoualma; map> g_glonassalma; map g_galileoalma; map g_gpsalma; GetterSetter> g_beidoualmakeeper; GetterSetter>> g_glonassalmakeeper; GetterSetter> g_galileoalmakeeper; GetterSetter> g_gpsalmakeeper; TLERepo g_tles; struct GNSSReceiver { Point position; }; int g_dtLS{18}, g_dtLSBeidou{4}; uint64_t utcFromGST(int wn, int tow) { return (935280000 + wn * 7*86400 + tow - g_dtLS); } double utcFromGST(int wn, double tow) { return (935280000.0 + wn * 7*86400 + tow - g_dtLS); } double utcFromGPS(int wn, double tow) { return (315964800 + wn * 7*86400 + tow - g_dtLS); } string humanFt(uint8_t ft) { static const char* ret[]={"100 cm", "200 cm", "250 cm", "400 cm", "500 cm", "7 m", "10 m", "12 m", "14 m", "16 m", "32 m", "64 m", "128 m", "256 m", "512 m", "NONE"}; if(ft < 16) return ret[ft]; return "???"; } string humanSisa(uint8_t sisa) { unsigned int sval = sisa; if(sisa < 50) return std::to_string(sval)+" cm"; if(sisa < 75) return std::to_string(50 + 2* (sval-50))+" cm"; if(sisa < 100) return std::to_string(100 + 4*(sval-75))+" cm"; if(sisa < 125) return std::to_string(200 + 16*(sval-100))+" cm"; if(sisa < 255) return "SPARE"; return "NO SIS AVAILABLE"; } string humanUra(uint8_t ura) { if(ura < 6) return fmt::sprintf("%d cm", (int)(100*pow(2.0, 1.0+1.0*ura/2.0))); else if(ura < 15) return fmt::sprintf("%d m", (int)(pow(2, ura-2))); return "NO URA AVAILABLE"; } void SVIOD::addGalileoWord(std::basic_string_view page) { uint8_t wtype = getbitu(&page[0], 0, 6); words[wtype]=true; gnssid = 2; if(wtype == 1) { 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); } else if(wtype == 2) { omega0 = getbits(&page[0], 16, 32); i0 = getbits(&page[0], 48, 32); omega = getbits(&page[0], 80, 32); idot = getbits(&page[0], 112, 14); } else if(wtype == 3) { omegadot = getbits(&page[0], 16, 24); deltan = getbits(&page[0], 40, 16); cuc = getbits(&page[0], 56, 16); cus = getbits(&page[0], 72, 16); crc = getbits(&page[0], 88, 16); crs = getbits(&page[0], 104, 16); sisa = getbitu(&page[0], 120, 8); } else if(wtype == 4) { cic = getbits(&page[0], 22, 16); cis = getbits(&page[0], 38, 16); t0c = getbitu(&page[0], 54, 14); af0 = getbits(&page[0], 68, 31); af1 = getbits(&page[0], 99, 21); af2 = getbits(&page[0], 120, 6); /* cout<<(int) t0c << " " <<(int) af0 <<" " <<(int) af1 <<" " <<(int) af2<second; throw std::runtime_error("Asked for live IOD, don't have one yet"); } void SVStat::checkCompleteAndClean(int iod) { if(iods[iod].complete()) { for(const auto& i : iods) { if(i.first != iod && i.second.complete()) prevIOD=i; } SVIOD latest = iods[iod]; decltype(iods) newiods; // XXX race condition here, newiods[iod]=latest; iods.swap(newiods); // try to keep it brief } } void SVStat::addGalileoWord(std::basic_string_view page) { uint8_t wtype = getbitu(&page[0], 0, 6); if(wtype == 0) { if(getbitu(&page[0], 6,2) == 2) { wn = getbitu(&page[0], 96, 12); if(tow != getbitu(&page[0], 108, 20)) { cerr<<"wtype "<=1 && wtype <= 4) { // ephemeris uint16_t iod = getbitu(&page[0], 6, 10); iods[iod].addGalileoWord(page); checkCompleteAndClean(iod); } else if(wtype==5) { // disturbance, health, time ai0 = getbitu(&page[0], 6, 11); ai1 = getbits(&page[0], 17, 11); // ai1 & 2 are signed, 0 not ai2 = getbits(&page[0], 28, 14); sf1 = getbitu(&page[0], 42, 1); sf2 = getbitu(&page[0], 43, 1); sf3 = getbitu(&page[0], 44, 1); sf4 = getbitu(&page[0], 45, 1); sf5 = getbitu(&page[0], 46, 1); BGDE1E5a = getbits(&page[0], 47, 10); BGDE1E5b = getbits(&page[0], 57, 10); e5bhs = getbitu(&page[0], 67, 2); e1bhs = getbitu(&page[0], 69, 2); e5bdvs = getbitu(&page[0], 71, 1); e1bdvs = getbitu(&page[0], 72, 1); wn = getbitu(&page[0], 73, 12); if(tow != getbitu(&page[0], 85, 20)) { cerr<<"wtype "< g_statskeeper; int latestWN(int gnssid, const svstats_t& stats) { map ages; for(const auto& s: stats) if(s.first.gnss == (unsigned int)gnssid) ages[7*s.second.wn*86400 + s.second.tow]= s.first; if(ages.empty()) throw runtime_error("Asked for latest WN for "+to_string(gnssid)+": we don't know it yet"); return stats.find(ages.rbegin()->second)->second.wn; } int latestTow(int gnssid, const svstats_t& stats) { map ages; for(const auto& s: stats) if(s.first.gnss == (unsigned int) gnssid) ages[7*s.second.wn*86400 + s.second.tow]= s.first; if(ages.empty()) throw runtime_error("Asked for latest TOW for "+to_string(gnssid)+": we don't know it yet"); return stats.find(ages.rbegin()->second)->second.tow; } int64_t nanoTime(int gnssid, int wn, double tow) { int offset; if(gnssid == 0) // GPS offset = 315964800; if(gnssid == 2) // Galileo, 22-08-1999 offset = 935280000; if(gnssid == 3) {// Beidou, 01-01-2006 - I think leap seconds count differently in Beidou!! XXX offset = 1136073600; return 1000000000ULL*(offset + wn * 7*86400 + tow - g_dtLSBeidou); } if(gnssid == 6) { // GLONASS offset = 820368000; return 1000000000ULL*(offset + wn * 7*86400 + tow); // no leap seconds in glonass } return 1000000000ULL*(offset + wn * 7*86400 + tow - g_dtLS); } struct InfluxPusher { explicit InfluxPusher(std::string_view dbname) : d_dbname(dbname) { if(dbname=="null") { d_mute = true; cout<<"Not sending data to influxdb"< void addValue( const pair& ent, string_view name, const T& value) { if(d_mute) return; if(nanoTime (ent.first.gnss, ent.second.wn, ent.second.tow)/1000000000 > 2000000000) { cerr<<"Unable to store item "< void addValueObserver(int src, string_view name, const T& value, time_t t) { if(d_mute) return; d_buffer+= string(name)+",src="+to_string(src)+ " value="+to_string(value)+ " "+to_string(t*1000000000)+"\n"; checkSend(); } template void addValue(const SatID& id, string_view name, const T& value, std::optional src = std::optional()) { if(d_mute) return; if(nanoTime (id.gnss, g_svstats[id].wn, g_svstats[id].tow)/1000000000 > 2000000000) { cerr<<"Unable to store item "< " <>& values, std::optional src = std::optional()) { if(d_mute) return; if(nanotime/1000000000 > 2000000000) { cerr<<"Unable to store item "< 1000000 || (time(0) - d_lastsent) > 10) { string buffer; buffer.swap(d_buffer); // thread t([buffer,this]() { if(!d_mute) doSend(buffer); // }); // t.detach(); d_lastsent=time(0); } } void doSend(std::string buffer) { MiniCurl mc; MiniCurl::MiniCurlHeaders mch; if(!buffer.empty()) { mc.postURL("http://127.0.0.1:8086/write?db="+d_dbname, buffer, mch); } } ~InfluxPusher() { if(d_dbname != "null") doSend(d_buffer); } std::string d_buffer; time_t d_lastsent{0}; string d_dbname; bool d_mute{false}; }; /* The GST start epoch is defined as 13 seconds before midnight between 21st August and 22nd August 1999, i.e. GST was equal to 13 seconds at 22nd August 1999 00:00:00 UTC. */ std::string humanTime(int gnssid, int wn, int tow) { time_t t = nanoTime(gnssid, wn, tow)/1000000000; struct tm tm; gmtime_r(&t, &tm); char buffer[80]; strftime(buffer, sizeof(buffer), "%a, %d %b %Y %T %z", &tm); return buffer; } char getGNSSChar(int id) { if(id==0) return 'G'; if(id==2) return 'E'; if(id==3) return 'C'; if(id==6) return 'R'; else return '0'+id; } std::string makeSatIDName(const SatID& satid) { return fmt::sprintf("%c%02d@%d", getGNSSChar(satid.gnss), satid.sv, satid.sigid); } std::string makeSatPartialName(const SatID& satid) { return fmt::sprintf("%c%02d", getGNSSChar(satid.gnss), satid.sv); } std::optional getHzCorrection(time_t now, int src, unsigned int gnssid, unsigned int sigid, const svstats_t svstats) { std::optional allHzCorr; double alltot=0; int allcount=0; // cout<<"getHzCorrection called for src "<second.deltaHzTime < 60) { // cout<<" Found entry for SV "<first<<", deltaHz: "<second.deltaHz<< " age " << now - iter->second.deltaHzTime<<" db "<second.db<second.deltaHz; allcount++; } } if(allcount > 3) { allHzCorr = alltot/allcount; // cout<<"Returning "<<*allHzCorr< options{"ok", "out of service", "will be out of service", "test"}; if(bhs >= (int)options.size()) { cerr<<"Asked for humanBHS "< 3 ? argv[3] : "galileo"); MiniCurl::init(); H2OWebserver h2s("galmon"); h2s.addHandler("/global.json", [](auto handler, auto req) { nlohmann::json ret = nlohmann::json::object(); auto svstats = g_statskeeper.get(); ret["leap-seconds"] = g_dtLS; try { ret["last-seen"]=utcFromGST(latestWN(2, svstats), latestTow(2, svstats)); } catch(...) {} map utcstats, gpsgststats, gpsutcstats; for(const auto& s: svstats) { if(!s.second.wn) // this will suck in 20 years continue; //Galileo-UTC offset: 3.22 ns, Galileo-GPS offset: 7.67 ns, 18 leap seconds if(s.first.gnss == 0) { // GPS int dw = (uint8_t)s.second.wn - s.second.wn0t; int age = dw * 7 * 86400 + s.second.tow - s.second.t0t; // t0t is PRESCALED // XXX changed this, gpsutcstats[age]=s.first; continue; } int dw = (uint8_t)s.second.wn - s.second.wn0t; int age = dw * 7 * 86400 + s.second.tow - s.second.t0t; // t0t is pre-scaled utcstats[age]=s.first; uint8_t wn0g = s.second.wn0t; int dwg = (((uint8_t)s.second.wn)&(1+2+4+8+16+32)) - wn0g; age = dwg*7*86400 + s.second.tow - s.second.t0g * 3600; gpsgststats[age]=s.first; } if(utcstats.empty()) { ret["utc-offset-ns"]=nullptr; } else { auto satid = utcstats.begin()->second; // freshest SV long shift = svstats[{2,satid.sv,satid.sigid}].a0 * (1LL<<20) + svstats[{2,satid.sv,satid.sigid}].a1 * utcstats.begin()->first; // in 2^-50 seconds units ret["utc-offset-ns"] = 1.073741824*ldexp(1.0*shift, -20); ret["leap-second-planned"] = (svstats[satid].dtLSF != svstats[satid].dtLS); } if(gpsgststats.empty()) { ret["gps-offset-ns"]=nullptr; } else { auto satid = gpsgststats.begin()->second; // freshest SV long shift = svstats[{2,satid.sv, satid.sigid}].a0g * (1L<<16) + svstats[{2,satid.sv, satid.sigid}].a1g * gpsgststats.begin()->first; // in 2^-51 seconds units ret["gps-offset-ns"] = 1.073741824*ldexp(shift, -21); } if(gpsutcstats.empty()) { ret["gps-utc-offset-ns"]=nullptr; } else { auto satid = gpsutcstats.begin()->second; // freshest SV long shift = svstats[{0,satid.sv,satid.sigid}].a0 * (1LL<<20) + svstats[{0,satid.sv,satid.sigid}].a1 * gpsutcstats.begin()->first; // In 2^-50 seconds units ret["gps-utc-offset-ns"] = 1.073741824*ldexp(shift, -20); } return ret; }); h2s.addHandler("/almanac.json", [](auto handler, auto req) { auto beidoualma = g_beidoualmakeeper.get(); auto svstats = g_statskeeper.get(); nlohmann::json ret = nlohmann::json::object(); for(const auto& ae : beidoualma) { nlohmann::json item = nlohmann::json::object(); item["gnssid"]=3; if(ae.second.alma.getT0e() > 7*86400) continue; Point sat; getCoordinates(latestTow(3, svstats), ae.second.alma, &sat); item["eph-ecefX"]= sat.x/1000; item["eph-ecefY"]= sat.y/1000; item["eph-ecefZ"]= sat.z/1000; auto longlat = getLongLat(sat.x, sat.y, sat.z); item["eph-longitude"] = 180*longlat.first/M_PI; item["eph-latitude"]= 180*longlat.second/M_PI; item["t0e"] = ae.second.alma.getT0e(); item["t"]= ephAge(ae.second.alma.getT0e(), latestTow(3, svstats))/86400.0; item["inclination"] = 180 * ae.second.alma.getI0() /M_PI; item["observed"]=false; if(auto iter = svstats.find({3, (uint32_t)ae.first, 0}); iter != svstats.end()) { if(time(0) - nanoTime(3, iter->second.wn, iter->second.tow)/1000000000 < 300) item["observed"] = true; } if(ephAge(ae.second.alma.getT0e(), latestTow(3, svstats)) < 0) { auto match = g_tles.getBestMatch(nanoTime(3, latestWN(3, svstats), latestTow(3, svstats))/1000000000.0, sat.x, sat.y, sat.z); if(match.distance < 200000) { item["best-tle"] = match.name; item["best-tle-norad"] = match.norad; item["best-tle-int-desig"] = match.internat; item["best-tle-dist"] = match.distance/1000.0; item["tle-ecefX"] = match.ecefX/1000; item["tle-ecefY"] = match.ecefY/1000; item["tle-ecefZ"] = match.ecefZ/1000; item["tle-eciX"] = match.eciX/1000; item["tle-eciY"] = match.eciY/1000; item["tle-eciZ"] = match.eciZ/1000; item["tle-latitude"] = 180*match.latitude/M_PI; item["tle-longitude"] = 180*match.longitude/M_PI; item["tle-altitude"] = match.altitude; } } auto name = fmt::sprintf("C%02d", ae.first); item["name"]=name; ret[name] = item; } auto glonassalma = g_glonassalmakeeper.get(); for(const auto& ae : glonassalma) { nlohmann::json item = nlohmann::json::object(); // ae.second.first -> even ae.second.sceond -> odd item["gnssid"]=6; item["e"] = ae.second.first.getE(); item["inclination"] = 180 * ae.second.first.getI0() /M_PI; item["health"] = ae.second.first.CnA; item["tlambdana"] = ae.second.second.gettLambdaNa(); item["lambdana"] = ae.second.second.getLambdaNaDeg(); item["hna"] = ae.second.second.hna; item["observed"] = false; for(uint32_t sigid : {0,1,2}) { // XXX SIGIDS if(auto iter = svstats.find({6, (uint32_t)ae.first, sigid}); iter != svstats.end()) { if(time(0) - nanoTime(6, iter->second.wn, iter->second.tow)/1000000000 < 300) { item["observed"] = true; auto longlat = getLongLat(iter->second.glonassMessage.x, iter->second.glonassMessage.y, iter->second.glonassMessage.z); item["eph-longitude"] = 180*longlat.first/M_PI; item["eph-latitude"]= 180*longlat.second/M_PI; break; } } } auto name = fmt::sprintf("R%02d", ae.first); item["name"]=name; ret[name] = item; } auto galileoalma = g_galileoalmakeeper.get(); for(const auto& ae : galileoalma) { nlohmann::json item = nlohmann::json::object(); item["gnssid"]=2; item["e"] = ae.second.getE(); item["e1bhs"] = ae.second.e1bhs; item["e5bhs"] = ae.second.e5bhs; item["t0e"] = ae.second.getT0e(); item["t"]= ephAge(ae.second.getT0e(), latestTow(2, svstats))/86400.0; item["eph-age"] = ephAge(latestTow(2, svstats), ae.second.getT0e()); item["i0"] = 180.0 * ae.second.getI0()/ M_PI; item["inclination"] = 180 * ae.second.getI0() /M_PI; Point sat; getCoordinates(latestTow(2, svstats), ae.second, &sat); item["eph-ecefX"]= sat.x/1000; item["eph-ecefY"]= sat.y/1000; item["eph-ecefZ"]= sat.z/1000; auto longlat = getLongLat(sat.x, sat.y, sat.z); item["eph-longitude"] = 180*longlat.first/M_PI; item["eph-latitude"]= 180*longlat.second/M_PI; item["observed"] = false; for(uint32_t sigid : {0,1,5}) { if(auto iter = svstats.find({2, (uint32_t)ae.first, sigid}); iter != svstats.end()) { if(time(0) - nanoTime(2, iter->second.wn, iter->second.tow)/1000000000 < 300) item["observed"] = true; } } auto match = g_tles.getBestMatch(nanoTime(2, latestWN(2, svstats), latestTow(2, svstats))/1000000000.0, sat.x, sat.y, sat.z); if(match.distance < 200000) { item["best-tle"] = match.name; item["best-tle-norad"] = match.norad; item["best-tle-int-desig"] = match.internat; item["best-tle-dist"] = match.distance/1000.0; item["tle-ecefX"] = match.ecefX/1000; item["tle-ecefY"] = match.ecefY/1000; item["tle-ecefZ"] = match.ecefZ/1000; item["tle-eciX"] = match.eciX/1000; item["tle-eciY"] = match.eciY/1000; item["tle-eciZ"] = match.eciZ/1000; item["tle-latitude"] = 180*match.latitude/M_PI; item["tle-longitude"] = 180*match.longitude/M_PI; item["tle-altitude"] = match.altitude; } auto name = fmt::sprintf("E%02d", ae.first); item["name"]= name; ret[name] = item; } auto gpsalma = g_gpsalmakeeper.get(); for(const auto& ae : gpsalma) { nlohmann::json item = nlohmann::json::object(); item["gnssid"]=0; item["e"] = ae.second.getE(); item["health"] = ae.second.health; item["t0e"] = ae.second.getT0e(); item["t"]= ephAge(ae.second.getT0e(), latestTow(2, svstats))/86400.0; item["eph-age"] = ephAge(latestTow(2, svstats), ae.second.getT0e()); item["i0"] = 180.0 * ae.second.getI0()/ M_PI; item["inclination"] = 180 * ae.second.getI0() /M_PI; Point sat; getCoordinates(latestTow(0, svstats), ae.second, &sat); item["eph-ecefX"]= sat.x/1000; item["eph-ecefY"]= sat.y/1000; item["eph-ecefZ"]= sat.z/1000; auto longlat = getLongLat(sat.x, sat.y, sat.z); item["eph-longitude"] = 180*longlat.first/M_PI; item["eph-latitude"]= 180*longlat.second/M_PI; item["observed"] = false; for(uint32_t sigid : {0,1,4}) { if(auto iter = svstats.find({0, (uint32_t)ae.first, sigid}); iter != svstats.end()) { if(time(0) - nanoTime(0, iter->second.wn, iter->second.tow)/1000000000 < 300) item["observed"] = true; } } auto match = g_tles.getBestMatch(nanoTime(0, latestWN(0, svstats), latestTow(0, svstats))/1000000000.0, sat.x, sat.y, sat.z); if(match.distance < 200000) { item["best-tle"] = match.name; item["best-tle-norad"] = match.norad; item["best-tle-int-desig"] = match.internat; item["best-tle-dist"] = match.distance/1000.0; item["tle-ecefX"] = match.ecefX/1000; item["tle-ecefY"] = match.ecefY/1000; item["tle-ecefZ"] = match.ecefZ/1000; item["tle-eciX"] = match.eciX/1000; item["tle-eciY"] = match.eciY/1000; item["tle-eciZ"] = match.eciZ/1000; item["tle-latitude"] = 180*match.latitude/M_PI; item["tle-longitude"] = 180*match.longitude/M_PI; item["tle-altitude"] = match.altitude; } auto name = fmt::sprintf("G%02d", ae.first); item["name"]=name; ret[name] = item; } return ret; }); h2s.addHandler("/observers.json", [](auto handler, auto req) { nlohmann::json ret = nlohmann::json::array(); for(const auto& src : g_srcpos) { nlohmann::json obj; obj["id"] = src.first; auto longlat = getLongLat(src.second.pos.x, src.second.pos.y, src.second.pos.z); longlat.first *= 180.0/M_PI; longlat.second *= 180.0/M_PI; longlat.first = ((int)(10*longlat.first))/10.0; longlat.second = ((int)(10*longlat.second))/10.0; obj["longitude"] = longlat.first; obj["latitude"] = longlat.second; obj["last-seen"] = src.second.lastSeen; auto svstats = g_statskeeper.get(); nlohmann::json svs = nlohmann::json::object(); for(const auto& sv : svstats) { if(auto iter = sv.second.perrecv.find(src.first); iter != sv.second.perrecv.end()) { if(iter->second.db > 0 && time(0) - iter->second.t < 120) { nlohmann::json svo = nlohmann::json::object(); svo["db"] = iter->second.db; svo["elev"] = iter->second.el; svo["azi"] = iter->second.azi; Point sat; if((sv.first.gnss == 0 || sv.first.gnss == 2) && sv.second.completeIOD()) getCoordinates(latestTow(sv.first.gnss, svstats), sv.second.liveIOD(), & sat); if(sv.first.gnss == 3 && sv.second.oldBeidouMessage.sow >= 0 && sv.second.oldBeidouMessage.sqrtA != 0) { getCoordinates(latestTow(sv.first.gnss, svstats), sv.second.oldBeidouMessage, &sat); } if(sv.first.gnss == 6) { sat.x = sv.second.glonassMessage.x; sat.y = sv.second.glonassMessage.y; sat.z = sv.second.glonassMessage.z; } if(sat.x) { Point our = g_srcpos[iter->first].pos; svo["elev"] = getElevationDeg(sat, our); svo["azi"] = getAzimuthDeg(sat, our); } svo["prres"] = iter->second.prres; svo["age-s"] = time(0) - iter->second.t; svo["last-seen"] = iter->second.t; svo["gnss"] = sv.first.gnss; svo["sv"] = sv.first.sv; svo["sigid"] = sv.first.sigid; svo["fullName"] = makeSatIDName(sv.first); svo["name"] = makeSatPartialName(sv.first); svs[makeSatIDName(sv.first)] = svo; } } } obj["svs"]=svs; ret.push_back(obj); } return ret; }); h2s.addHandler("/sv.json", [](auto handler, auto req) { string_view path = convert(req->path); cout<second; ret["a0"] = s.a0; ret["a1"] = s.a1; ret["a0g"] = s.a0g; ret["a1g"] = s.a1g; ret["sf1"] = s.sf1; ret["sf2"] = s.sf2; ret["sf3"] = s.sf3; ret["sf4"] = s.sf4; ret["sf5"] = s.sf5; ret["ai0"] = s.ai0; ret["ai1"] = s.ai1; ret["ai2"] = s.ai2; ret["BGDE1E5a"] = s.BGDE1E5a; ret["BGDE1E5b"] = s.BGDE1E5b; ret["wn"] = s.wn; ret["tow"] = s.tow; ret["dtLS"] = s.dtLS; ret["dtLSF"] = s.dtLSF; ret["wnLSF"] = s.wnLSF; ret["dn"] = s.dn; ret["e5bdvs"]=s.e5bdvs; ret["e1bdvs"]=s.e1bdvs; ret["e5bhs"]=s.e5bhs; ret["e1bhs"]=s.e1bhs; if(svstats[id].completeIOD()) { const auto& iod = svstats[id].liveIOD(); ret["iod"]= svstats[id].getIOD(); ret["e"] = iod.getE(); ret["omega"] = iod.getOmega(); ret["sqrtA"]= iod.getSqrtA(); ret["M0"] = iod.getM0(); ret["i0"] = iod.getI0(); ret["delta-n"] = iod.getDeltan(); ret["omega-dot"] = iod.getOmegadot(); ret["omega0"] = iod.getOmega0(); ret["idot"] = iod.getIdot(); ret["t0e"] = iod.getT0e(); ret["t0c"] = iod.getT0c(); ret["cuc"] = iod.getCuc(); ret["cus"] = iod.getCus(); ret["crc"] = iod.getCrc(); ret["crs"] = iod.getCrs(); ret["cic"] = iod.getCic(); ret["cis"] = iod.getCis(); ret["sisa"] = iod.sisa; ret["af0"] = iod.af0; ret["af1"] = iod.af1; ret["af2"] = iod.af2; Point p; getCoordinates(s.tow, iod, &p); ret["x"] = p.x; ret["y"] = p.y; ret["z"] = p.z; auto longlat = getLongLat(p.x, p.y, p.z); ret["longitude"] = 180.0*longlat.first/M_PI; ret["latitude"] = 180.0*longlat.second/M_PI; } return ret; } ); h2s.addHandler("/galcov.json", [](auto handler, auto req) { auto cov = emitCoverage(); auto ret = nlohmann::json::array(); // ret = // [ [90, [[-180, 3], [-179, 3], ... [180,3] ]] // [89, [[-180, 4], [-179, 4], ... [180,2] ]] // ] for(const auto& latvect : cov) { auto jslatvect = nlohmann::json::array(); auto jslongvect = nlohmann::json::array(); for(const auto& longpair : latvect.second) { auto jsdatum = nlohmann::json::array(); jsdatum.push_back((int)longpair.first); jsdatum.push_back(longpair.second); jslongvect.push_back(jsdatum); } jslatvect.push_back(latvect.first); jslatvect.push_back(jslongvect); ret.push_back(jslatvect); } return ret; }); h2s.addHandler("/svs.json", [](auto handler, auto req) { auto svstats = g_statskeeper.get(); nlohmann::json ret = nlohmann::json::object(); h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_CACHE_CONTROL, NULL, H2O_STRLIT("max-age=3")); for(const auto& s: svstats) { nlohmann::json item = nlohmann::json::object(); if(!s.second.tow) // I know, I know, will suck briefly continue; item["gnssid"] = s.first.gnss; item["svid"] = s.first.sv; item["sigid"] = s.first.sigid; // perhaps check oldBeidouMessage for sow >=0 as 'completeIOD'? if(s.first.gnss == 3) { // beidou item["sisa"]=humanUra(s.second.ura); if(s.second.t0eMSB >= 0 && s.second.t0eLSB >=0) item["eph-age-m"] = ephAge(s.second.tow, 8.0*((s.second.t0eMSB<<15) + s.second.t0eLSB))/60.0; if(s.second.tleMatch.distance >=0) { item["best-tle"] = s.second.tleMatch.name; item["best-tle-dist"] = s.second.tleMatch.distance /1000.0; item["best-tle-norad"] = s.second.tleMatch.norad; item["best-tle-int-desig"] = s.second.tleMatch.internat; } Point p; getCoordinates(s.second.tow, s.second.oldBeidouMessage, &p); auto beidoualma = g_beidoualmakeeper.get(); if(auto iter = beidoualma.find(s.first.sv); iter != beidoualma.end()) { Point almapos; getCoordinates(s.second.tow, iter->second.alma, &almapos); item["alma-dist"] = Vector(almapos, p).length()/1000.0; } } else if(s.first.gnss == 6) { // glonass if(s.second.glonassMessage.FT < 16) item["sisa"] = humanFt(s.second.glonassMessage.FT); item["aode"] = s.second.aode; item["iod"] = s.second.glonassMessage.Tb; time_t glonow = nanoTime(6, s.second.wn, s.second.tow)/1000000000.0; // the 820368000 stuff is to rebase to 'day 1' so the % works auto pseudoTow = (getGlonassT0e(glonow, s.second.glonassMessage.Tb) - 820368000) % (7*86400); // cout<=0) { item["best-tle"] = s.second.tleMatch.name; item["best-tle-dist"] = s.second.tleMatch.distance /1000.0; item["best-tle-norad"] = s.second.tleMatch.norad; item["best-tle-int-desig"] = s.second.tleMatch.internat; } } if(s.second.completeIOD()) { item["iod"]=s.second.getIOD(); if(s.first.gnss == 0 || s.first.gnss == 3) { item["sisa"]=humanUra(s.second.ura); // cout<<"Asked to convert "<second, &almapos); item["alma-dist"] = Vector(almapos, p).length()/1000.0; } } if(s.first.gnss == 2) { auto galileoalma = g_galileoalmakeeper.get(); if(auto iter = galileoalma.find(s.first.sv); iter != galileoalma.end()) { Point almapos; getCoordinates(s.second.tow, iter->second, &almapos); item["alma-dist"] = Vector(almapos, p).length()/1000.0; } } } item["a0"]=s.second.a0; item["a1"]=s.second.a1; if(s.first.gnss == 0) { // GPS auto deltaUTC = getGPSUTCOffset(s.second.tow, s.second.wn, s.second); item["delta-utc"] = fmt::sprintf("%.1f %+.1f/d", deltaUTC.first, deltaUTC.second * 86400); item["t0t"] = s.second.t0t; item["wn0t"] = s.second.wn0t; } if(s.first.gnss == 2) { auto deltaUTC = s.second.galmsg.getUTCOffset(s.second.tow, s.second.wn); item["delta-utc"] = fmt::sprintf("%.1f %+.1f/d", deltaUTC.first, deltaUTC.second * 86400); auto deltaGPS = s.second.galmsg.getGPSOffset(s.second.tow, s.second.wn); item["delta-gps"] = fmt::sprintf("%.1f %+.1f/d", deltaGPS.first, deltaGPS.second * 86400); item["t0t"] = s.second.galmsg.t0t; item["wn0t"] = s.second.galmsg.wn0t; } if(s.first.gnss == 3) { auto deltaUTC = s.second.oldBeidouMessage.getUTCOffset(s.second.oldBeidouMessage.sow); item["delta-utc"] = fmt::sprintf("%.1f %+.1f/d", deltaUTC.first, deltaUTC.second * 86400); auto deltaGPS = s.second.oldBeidouMessage.getGPSOffset(s.second.oldBeidouMessage.sow); item["delta-gps"] = fmt::sprintf("%.1f %+.1f/d", deltaGPS.first, deltaGPS.second * 86400); item["t0g"] =0; item["wn0g"] = 0; item["t0t"] = 0; item["wn0t"] = 0; } item["dtLS"]=s.second.dtLS; if(s.first.gnss == 3) { // beidou item["a0g"]=s.second.a0g; item["a1g"]=s.second.a1g; if(s.second.aode >= 0) item["aode"]=s.second.aode; if(s.second.aodc >= 0) item["aodc"]=s.second.aodc; } if(s.first.gnss == 2) { // galileo item["a0g"]=s.second.a0g; item["a1g"]=s.second.a1g; item["t0g"]=s.second.t0g; item["wn0g"]=s.second.wn0g; item["health"] = humanBhs(s.second.e1bhs) +"/" + humanBhs(s.second.e5bhs) +"/" + (s.second.e1bdvs ? "NG" : "val") +"/"+ (s.second.e5bdvs ? "NG" : "val"); item["e5bdvs"]=s.second.e5bdvs; item["e1bdvs"]=s.second.e1bdvs; item["e5bhs"]=s.second.e5bhs; item["e1bhs"]=s.second.e1bhs; item["healthissue"]=0; if(s.second.e1bhs == 2 || s.second.e5bhs == 2) item["healthissue"] = 1; if(s.second.e1bhs == 3 || s.second.e5bhs == 3) item["healthissue"] = 1; if(s.second.e1bdvs || s.second.e5bdvs || s.second.e1bhs == 1 || s.second.e5bhs == 1) item["healthissue"] = 2; } else if(s.first.gnss == 0 || s.first.gnss == 3 || s.first.gnss == 6) {// gps or beidou or GLONASS item["health"]= s.second.gpshealth ? ("NOT OK: "+to_string(s.second.gpshealth)) : string("OK"); item["healthissue"]= 2* !!s.second.gpshealth; } nlohmann::json perrecv = nlohmann::json::object(); for(const auto& pr : s.second.perrecv) { if(pr.second.db > 0) { nlohmann::json det = nlohmann::json::object(); det["elev"] = pr.second.el; Point sat; if((s.first.gnss == 0 || s.first.gnss == 2) && s.second.completeIOD()) getCoordinates(latestTow(s.first.gnss, svstats), s.second.liveIOD(), & sat); if(s.first.gnss == 3 && s.second.oldBeidouMessage.sow >= 0 && s.second.oldBeidouMessage.sqrtA != 0) { getCoordinates(latestTow(s.first.gnss, svstats), s.second.oldBeidouMessage, &sat); } if(sat.x) { Point our = g_srcpos[pr.first].pos; det["elev"] = getElevationDeg(sat, our); det["azi"] = getAzimuthDeg(sat, our); } else det["elev"] = pr.second.el; det["db"] = pr.second.db; det["last-seen-s"] = time(0) - pr.second.t; det["prres"] = pr.second.prres; if(time(0) - pr.second.deltaHzTime < 60) { det["delta_hz"] = pr.second.deltaHz; auto hzCorrection = getHzCorrection(time(0), pr.first, s.first.gnss, s.first.sigid, svstats); if(hzCorrection) det["delta_hz_corr"] = pr.second.deltaHz - *hzCorrection; } perrecv[to_string(pr.first)]=det; } } item["perrecv"]=perrecv; item["last-seen-s"] = time(0) - nanoTime(s.first.gnss, s.second.wn, s.second.tow)/1000000000; if(s.second.latestDisco >=0) { item["latest-disco"]= s.second.latestDisco; item["latest-disco-age"]= s.second.latestDiscoAge; } if(s.second.timeDisco > -100 && s.second.timeDisco < 100) { item["time-disco"]= s.second.timeDisco; } item["wn"] = s.second.wn; item["tow"] = s.second.tow; item["fullName"] = makeSatIDName(s.first); item["name"] = makeSatPartialName(s.first); ret[makeSatIDName(s.first)] = item; } return ret; }); h2s.addDirectory("/", argc > 2 ? argv[2] : "./html/"); const char *address = argc > 1 ? argv[1] : "127.0.0.1:29599"; std::thread ws([&h2s, address]() { auto actx = h2s.addContext(); ComboAddress listenOn(address); h2s.addListener(listenOn, actx); cout<<"Listening on "<< listenOn.toStringWithPort() < inav((uint8_t*)nmm.gi().contents().c_str(), nmm.gi().contents().size()); int sv = nmm.gi().gnsssv(); int sigid; if(nmm.gi().has_sigid()) sigid = nmm.gi().sigid(); else sigid = 1; // default to E1B SatID id={2,(uint32_t)sv,(uint32_t)sigid}; g_svstats[id].wn = nmm.gi().gnsswn(); auto& svstat = g_svstats[id]; auto oldgm = svstat.galmsg; auto& gm = svstat.galmsg; unsigned int wtype = gm.parse(inav); svstat.galmsgTyped[wtype] = gm; if(wtype == 1 || wtype == 2 || wtype == 3) { idb.addValue(id, "iod-live", svstat.galmsg.iodnav); } if(wtype == 5 && svstat.galmsgTyped.count(5)) { const auto& old5gm = svstat.galmsgTyped[5]; if(make_tuple(old5gm.e5bhs, old5gm.e1bhs, old5gm.e5bdvs, old5gm.e1bdvs) != make_tuple(gm.e5bhs, gm.e1bhs, gm.e5bdvs, gm.e1bdvs)) { cout< ["<< humanBhs(gm.e5bhs)<<", "<< humanBhs(gm.e1bhs)<<", "<< (int)gm.e5bdvs <<", " << (int)gm.e1bdvs<<"], lastseen "<=1 && wtype <= 4) { // ephemeris uint16_t iod = getbitu(&inav[0], 6, 10); if(wtype == 3) { idb.addValue(id, "sisa", g_svstats[id].iods[iod].sisa); } else if(wtype == 4) { idb.addValue(id, "af0", g_svstats[id].iods[iod].af0); idb.addValue(id, "af1", g_svstats[id].iods[iod].af1); idb.addValue(id, "af2", g_svstats[id].iods[iod].af2); idb.addValue(id, "t0c", g_svstats[id].iods[iod].t0c * 60); double age = ephAge(g_svstats[id].tow, g_svstats[id].iods[iod].t0c * 60); double offset = ldexp(1000.0*(1.0*g_svstats[id].iods[iod].af0 + ldexp(age*g_svstats[id].iods[iod].af1, -12)), -34); idb.addValue(id, "atomic_offset_ns", 1000000.0*offset); if(oldgm.af0 && oldgm.t0c != svstat.galmsg.t0c) { auto oldOffset = oldgm.getAtomicOffset(svstat.tow); auto newOffset = svstat.galmsg.getAtomicOffset(svstat.tow); svstat.timeDisco = oldOffset.first - newOffset.first; idb.addValue(id, "clock_jump_ns", svstat.timeDisco); } } } else if(wtype == 5) { idb.addValue(id, "ai0", g_svstats[id].ai0); idb.addValue(id, "ai1", g_svstats[id].ai1); idb.addValue(id, "ai2", g_svstats[id].ai2); idb.addValue(id, "sf1", g_svstats[id].sf1); idb.addValue(id, "sf2", g_svstats[id].sf2); idb.addValue(id, "sf3", g_svstats[id].sf3); idb.addValue(id, "sf4", g_svstats[id].sf4); idb.addValue(id, "sf5", g_svstats[id].sf5); idb.addValue(id, "BGDE1E5a", g_svstats[id].BGDE1E5a); idb.addValue(id, "BGDE1E5b", g_svstats[id].BGDE1E5b); idb.addValue(id, "e1bhs", g_svstats[id].e1bhs); idb.addValue(id, "e5bhs", g_svstats[id].e5bhs); idb.addValue(id, "e5bdvs", g_svstats[id].e5bdvs); idb.addValue(id, "e1bdvs", g_svstats[id].e1bdvs); } else if(wtype == 6) { // GST-UTC 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_dtLS = g_svstats[id].dtLS; } else if(wtype == 7) { // this contains first part of alma1 } else if(wtype == 8) { if(gm.tow - svstat.galmsgTyped[7].tow < 5 && svstat.galmsgTyped[7].alma1.svid && gm.iodalmanac == svstat.galmsgTyped[7].iodalmanac) { // cout<<(int)wtype<<" alma-sv "<= 0) { int ephage = ephAge(ent.second.tow, ent.second.prevIOD.second.t0e); if(ent.second.liveIOD().sisa != ent.second.prevIOD.second.sisa) { cout<= 0 && g_svstats[id].oldBeidouMessage.sqrtA != 0) { double freq = 1561.098; if(nmm.rfd().sigid() != 0) freq = 1207.140; auto res = doDoppler(nmm.rfd().rcvtow(), g_srcpos[nmm.sourceid()].pos, g_svstats[id].oldBeidouMessage, freq * 1000000); if(isnan(res.preddop)) { cerr<<"Problem with doppler calculation for C"< 10) { g_svstats[id].perrecv[nmm.sourceid()].deltaHz = nmm.rfd().doppler() - res.preddop; g_svstats[id].perrecv[nmm.sourceid()].deltaHzTime = t; idb.addValue(id, "delta_hz", nmm.rfd().doppler() - res.preddop); auto corr = getHzCorrection(t, nmm.sourceid(), id.gnss, id.sigid, g_svstats); if(corr) { idb.addValue(id, "delta_hz_cor", nmm.rfd().doppler() - res.preddop - (1561.098/1575.42) * (*corr)); } } } else if(g_svstats[id].completeIOD()) { double freqMHZ = 1575.42; if(id.gnss == 2 && id.sigid == 5) // this is exactly the beidou b2i freq? freqMHZ = 1207.140; auto res = doDoppler(nmm.rfd().rcvtow(), g_srcpos[nmm.sourceid()].pos, g_svstats[id].liveIOD(),freqMHZ * 1000000); idb.addValue(id, nanoTime(0, nmm.rfd().rcvwn(), nmm.rfd().rcvtow()), "orbit", {{"preddop", res.preddop}, {"radvel", res.radvel}}); time_t t = utcFromGPS(nmm.rfd().rcvwn(), nmm.rfd().rcvtow()); if(t - g_svstats[id].perrecv[nmm.sourceid()].deltaHzTime > 10) { // only replace after 10 seconds g_svstats[id].perrecv[nmm.sourceid()].deltaHz = nmm.rfd().doppler() - res.preddop; g_svstats[id].perrecv[nmm.sourceid()].deltaHzTime = t; idb.addValue(id, "delta_hz", nmm.rfd().doppler() - res.preddop); auto corr = getHzCorrection(t, nmm.sourceid(), id.gnss, id.sigid, g_svstats); if(corr) { idb.addValue(id, "delta_hz_cor", nmm.rfd().doppler() - res.preddop - *corr); } } } } else if(nmm.type()== NavMonMessage::GPSInavType) { if(nmm.gpsi().sigid()) { cout<<"ignoring sigid "<((uint8_t*)nmm.gpsi().contents().c_str(), nmm.gpsi().contents().size())); SatID id{nmm.gpsi().gnssid(), nmm.gpsi().gnsssv(), nmm.gpsi().sigid()}; g_svstats[id].perrecv[nmm.sourceid()].t = nmm.localutcseconds(); auto& svstat = g_svstats[id]; auto oldsvstat = svstat; 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); // cout<<"Got ura "< 1) { // XX find better way to check cout< [" << humanUra(svstat.ura)<<"] "<<(int)svstat.ura<<", lastseen "< [" << (int)svstat.gpshealth <<"], lastseen "< ["<< (int)svstat.gpshealth<<"] , lastseen "<=25 && page<=32)) { g_gpsalma[svstat.gpsalma.sv] = svstat.gpsalma; } 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 if(nmm.type()== NavMonMessage::BeidouInavTypeD1) { SatID id{nmm.bid1().gnssid(), nmm.bid1().gnsssv(), nmm.bid1().sigid()}; g_svstats[id].perrecv[nmm.sourceid()].t = nmm.localutcseconds(); auto& svstat = g_svstats[id]; uint8_t pageno; auto cond = getCondensedBeidouMessage(std::basic_string((uint8_t*)nmm.bid1().contents().c_str(), nmm.bid1().contents().size())); auto& bm = svstat.beidouMessage; auto oldbm = bm; int fraid=bm.parse(cond, &pageno); svstat.tow = nmm.bid1().gnsstow(); svstat.wn = nmm.bid1().gnsswn(); if(fraid == 1) { svstat.ura = bm.urai; svstat.gpshealth = bm.sath1; svstat.af0 = bm.a0; svstat.af1 = bm.a1; svstat.af2 = bm.a2; svstat.aode = bm.aode; svstat.aodc = bm.aodc; if(oldbm.sath1 != bm.sath1) { cout<=0 && svstat.lastBeidouMessage1.t0c != bm.t0c) { auto oldOffset = svstat.lastBeidouMessage1.getAtomicOffset(bm.sow); auto newOffset = bm.getAtomicOffset(bm.sow); svstat.timeDisco = oldOffset.first - newOffset.first; idb.addValue(id, "clock_jump_ns", svstat.timeDisco); } svstat.lastBeidouMessage1 = bm; } if(fraid == 2) { svstat.lastBeidouMessage2 = bm; svstat.t0eMSB = bm.t0eMSB; } if(fraid == 3) { svstat.t0eLSB = bm.t0eLSB; Point oldpoint, newpoint; if(bm.sow - svstat.lastBeidouMessage2.sow == 6 && svstat.oldBeidouMessage.sow >= 0) { getCoordinates(svstat.tow, bm, &newpoint); if(fabs(svstat.lastTLELookupX - newpoint.x) > 300000) { // cout<<"fraid 3 lookup, delta " << fabs(svstat.lastTLELookupX - newpoint.x) << endl; auto match = g_tles.getBestMatch(nanoTime(3, svstat.wn, svstat.tow)/1000000000.0, newpoint.x, newpoint.y, newpoint.z); svstat.tleMatch = match; svstat.lastTLELookupX = newpoint.x; } if(svstat.oldBeidouMessage.getT0e() != svstat.beidouMessage.getT0e()) { getCoordinates(svstat.tow, svstat.oldBeidouMessage, &oldpoint); Vector jump(oldpoint ,newpoint); /* cout< (%f, %f, %f), jump: %f, seconds: %f\n", id.second, oldpoint.x, oldpoint.y, oldpoint.z, newpoint.x, newpoint.y, newpoint.z, jump.length(), (double)bm.getT0e() - svstat.oldBeidouMessage.getT0e()); */ double hours = (bm.getT0e() - svstat.oldBeidouMessage.getT0e())/3600; if(hours < 4) { svstat.latestDisco = jump.length(); idb.addValue(id, "eph-disco", jump.length()); } else svstat.latestDisco = -1; if(hours < 24) { idb.addValue(id, nanoTime(id.gnss, bm.wn, bm.sow), "eph-disco2", {{"meters", jump.length()}, {"seconds", hours*3600.0}, {"oldx", oldpoint.x}, {"oldy", oldpoint.y}, {"oldz", oldpoint.z}, {"x", newpoint.x}, {"y", newpoint.y}, {"z", newpoint.z}, {"oid", 0}, {"oldoid", 0}}); } } } if(bm.sqrtA) // only copy in if complete svstat.oldBeidouMessage = bm; } else if((fraid == 4 && 1<= pageno && pageno <= 24) || (fraid == 5 && 1<= pageno && pageno <= 6) || (fraid == 5 && 11<= pageno && pageno <= 23) ) { struct BeidouAlmanacEntry bae; // bm.alma.AmEpID = svstat.oldBeidouMessage.alma.AmEpID; // this comes from older messages if(processBeidouAlmanac(bm, bae)) { g_beidoualma[bae.sv]=bae; } } if(fraid==5 && pageno == 9) { svstat.a0g = bm.a0gps; svstat.a1g = bm.a1gps; } if(fraid==5 && pageno == 10) { svstat.a0 = bm.a0utc; svstat.a1 = bm.a1utc; g_dtLSBeidou = bm.deltaTLS; // cout<<"Beidou leap seconds: "<((uint8_t*)nmm.bid2().contents().c_str(), nmm.bid2().contents().size())); /* int fraid = getbitu(&cond[0], beidouBitconv(16), 3); int sow = getbitu(&cond[0], beidouBitconv(19), 20); int pnum = getbitu(&cond[0], beidouBitconv(43), 4); int pre = getbitu(&cond[0], beidouBitconv(1), 11); // cout<<"C"<< nmm.bid2().gnsssv() << " sent D2 message, pre "<((uint8_t*)nmm.gloi().contents().c_str(), nmm.gloi().contents().size())); g_svstats[id].perrecv[nmm.sourceid()].t = nmm.localutcseconds(); if(strno == 1 && gm.n4 != 0 && gm.NT !=0) { uint32_t glotime = gm.getGloTime(); // this starts GLONASS time at 31st of december 1995, 00:00 UTC svstat.wn = glotime / (7*86400); svstat.tow = glotime % (7*86400); // cout<<"Glonass now: "<