diff --git a/apps/spacer/ChangeLog b/apps/spacer/ChangeLog index 263d4078d..88e8a64fc 100644 --- a/apps/spacer/ChangeLog +++ b/apps/spacer/ChangeLog @@ -1 +1,2 @@ 0.01: attempt to import +0.10: major updates, display satellites, display per-system satellite info diff --git a/apps/spacer/README.md b/apps/spacer/README.md index 5a8725a96..b3fc437ba 100644 --- a/apps/spacer/README.md +++ b/apps/spacer/README.md @@ -4,5 +4,17 @@ Compare GPS with Baido and Glonass This turns GNSS receiver into mode with all three systems enabled, and displays debug info from all of them. Click into top left corner to -switch navigation systems. +switch navigation systems. Clicks in bottom half of screen switch info +pages. + +GNSS acquisition has few phases, and this software is assuming you are +not doing a cold start. GNSS fix needs 4 or 5 satellites with +reasonable signal strength, and then holding the same place for 40 or +so seconds. + +Uxx -- satellites are known but not being received + +S1..S4 -- not enough satellites being decoded (5 needed) + +XXdB -- this is strength of 5th strongest satellite diff --git a/apps/spacer/app-icon.js b/apps/spacer/app-icon.js index 903d14af5..53dbfa31e 100644 --- a/apps/spacer/app-icon.js +++ b/apps/spacer/app-icon.js @@ -1,2 +1,2 @@ -require("heatshrink").decompress(atob("mEwgIQNgQFEj/gAof+jgECgeAAIIFBgwCBuACBhgCEjAOEAoQ6CmAhCDwItDoEB4AFCsEBFgUEkEDG4XEJYcL8gFCgUP+gxCAoP8DIIFBhfsiEIAoMJAogCBAoYlBiBMBAoUwrA0B////ALECI0QAocgAolgApVADolAHYnAAomAAoqdBAoKVBMoRvCOIQDCRIIFBYwKVBAoKqC4AFBVQVggTRDn0CYgQcBN4LpDV4T7IAooAJA=")) +require("heatshrink").decompress(atob("mEwhHXAH4A/AH4A/AGMAF34v/F34EBAAIvrFwQxnEoIsFGEyNHF9ZgNGrheMF4guJHDAvLEhCdbLyLLdL5ImFfRo6SLpjGOCgw5TdZRhKHwhEPFxjOJCwwOGF6zOJBxpjFAB4TKR6aEJDpgwKd5rzUYBrkVMxwtVF7CYHeBxGRF5LwMR9YvdPiJfjBhYv/F/4v/F/4v/F/4v/F/4v/F/4vfAH4Ad")) diff --git a/apps/spacer/app.png b/apps/spacer/app.png index ce7ac239f..6743d6e59 100644 Binary files a/apps/spacer/app.png and b/apps/spacer/app.png differ diff --git a/apps/spacer/metadata.json b/apps/spacer/metadata.json index 55598f953..e114d8dd1 100644 --- a/apps/spacer/metadata.json +++ b/apps/spacer/metadata.json @@ -1,6 +1,6 @@ { "id": "spacer", "name": "Space Race", - "version": "0.01", + "version": "0.10", "description": "Compare GPS with Baido and Glonass", "icon": "app.png", "readme": "README.md", diff --git a/apps/spacer/spacer.app.js b/apps/spacer/spacer.app.js index 13812b2a5..99453e79a 100644 --- a/apps/spacer/spacer.app.js +++ b/apps/spacer/spacer.app.js @@ -1,20 +1,6 @@ /* Space race */ -/* - -Performance Assessment of GNSS Signals in -terms of Time to First Fix for -Cold, Warm and Hot Start -Matteo Paonni, Marco Anghileri, Stefan Wallner, José-Ángel Ávila-Rodríguez, Bernd Eissfeller -Institute of Geodesy and Navigation, University FAF Munich, Germany - -=> 22 to 26 dB -- long time / no fix / ... -=> 26db + -- basically strength no longer matters - -apps/assistedgps/custom.html - -https://github.com/espruino/EspruinoDocs/blob/master/info/Bangle.js2%20Technical.md#gps - +/* gsa mi rika 2d/3d fix, a taky pdop/vdop/hdop CFG-NAVX z CASIC_en -- umoznuje nastavit chodec / auto / letadlo @@ -68,12 +54,12 @@ let ui = { touchHandler: function(d) { let x = Math.floor(d.x); let y = Math.floor(d.y); - + if (d.b != 1 || this.last_b != 0) { this.last_b = d.b; return; } - + print("touch", x, y, this.h, this.w); if ((x this.drawSat(s)); - - if (fix && fix.fix && fix.lat) { - g.setColor(0, 0, 0) - .setFontAlign(-1, 1); - g.drawString(fix.satellites + "/" + fix.hdop, 5, ui.y2); - } - this.decorate(); - }, - parseSats: function(s) { let view = 1 * s[3]; let k = Math.min(4, view - this.snum); @@ -193,24 +139,186 @@ let sky = { } }, + snrSort: function() { + return this.sats.slice(0, this.snum).sort((a, b) => b.snr - a.snr); + }, + getSatSNR: function(n) { /* Get n-th strongest sat */ + if (n <= 0 || n > this.sats.length) + return -1; + + // Sort the satellites by snr in descending order + let sortedSats = this.snrSort(); + + // Return the SNR of the n-th strongest satellite + return sortedSats[n - 1].snr; + }, + qualest: function() { + // Sort the satellites by snr in descending order + let sortedSats = this.snrSort(); + if (sortedSats[4] && sortedSats[4].snr) { + return "" + sortedSats[4].snr + "dB"; + } + for (let i=4; i>=0; i--) { + if (sortedSats[i] && sortedSats[i].snr) + return "S" + (i+1); + } + return "U" + this.snum; + }, + satVisibility: [], + trackSatelliteVisibility: function() { + const threshold = this.snrLim; // SNR threshold + const now = getTime(); + let newVisibility = []; + //this.satVisibility = []; + for (let i = 0; i < this.snum; i++) { + let sat = this.sats[i]; + let existingSat = this.satVisibility[sat.id]; + if (sat.snr >= threshold) { + if (!existingSat) { + // New satellite starts visibility + newVisibility[sat.id] = { start: now, visible: true }; + } else + newVisibility[sat.id] = this.satVisibility[sat.id]; + } + } + this.satVisibility = newVisibility; + }, + getnthLowestStartTimeSat: function(n) { + // Collect all satellites from visibility + let satellites = Object.values(this.satVisibility); + + // Ensure we have at least 5 satellites + if (satellites.length < n) + return -1; + + // Sort satellites by start time in ascending order + satellites.sort((a, b) => a.start - b.start); + + // Return the satellite with the 5th lowest start time + return satellites[n-1]; // 0-based index, so 5th is index 4 + }, + goodest: function () { + let s = this.getnthLowestStartTimeSat(5); + if (s==-1) + return ""; + let t = getTime() - s.start; + return "" + t + "s"; + }, + summary: function () { + let s = this.goodest(); + if (s != "") + return s; + return this.qualest(); + }, + onEnd: function () { + this.trackSatelliteVisibility(); + if (this.sats_used < 5) + this.sky_start = getTime(); + this.reset(); + }, +}; + +function deepCopy(obj) { + if (obj === null || typeof obj !== "object") { + return obj; // Return primitive values as-is + } + + if (Array.isArray(obj)) { + return obj.map(deepCopy); // Handle arrays recursively + } + + const copy = {}; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + copy[key] = deepCopy(obj[key]); // Recursively copy properties + } + } + return copy; +} + +let sky = { + this_usable: 0, + debug: 0, + all: skys, /* Sattelites from all systems */ + split: 1, + + init: function () { + if (this.split) { + this.s_gp = deepCopy(skys); + this.s_gl = deepCopy(skys); + this.s_bd = deepCopy(skys); + } + }, + + drawGrid: function() { + g.setColor(0,0,0); + ui.radLine(0, 1, 0.5, 1); + ui.radLine(0.25, 1, 0.75, 1); + ui.radCircle(0.5); + ui.radCircle(1.0); + }, + + /* 18.. don't get reliable fix in 40s */ + snrLim: 22, + drawSat: function(s) { + let a = s.azi / 360; + let e = ((90 - s.ele) / 90); + let x = ui.radX(a, e); + let y = ui.radY(a, e); + + if (s.snr == 0) + g.setColor(1, 0.25, 0.25); + else if (s.snr < this.snrLim) + g.setColor(0.25, 0.5, 0.25); + else + g.setColor(0, 0, 0); + g.drawString(s.id, x, y); + }, + + // Should correspond to view from below. + // https://in-the-sky.org//satmap_radar.php?year=2023&month=10&day=24&skin=1 + decorate: function() {}, + drawSats: function(sats) { + g.reset() + .setColor(1, 1, 1) + .fillRect(0, ui.wi, ui.w, ui.y2) + .setFont("Vector", 20) + .setFontAlign(0, 0); + this.drawGrid(); + sats.forEach(s => this.drawSat(s)); + + if (fix && fix.fix && fix.lat) { + g.setColor(0, 0, 0) + .setFontAlign(-1, 1); + g.drawString(fix.satellites + "/" + fix.hdop, 5, ui.y2); + } + this.decorate(); + }, + old_msg: {}, msg: {}, - tof: function(v) { let i = (1*v); return i.toFixed(0); }, - fmtSys: function(sys) { - if (sys && sys.sent !== undefined && sys.d23 !== undefined) - return sys.sent + "." + sys.d23 + "D "+ this.tof(sys.pdop) + " " + this.tof(sys.vdop) + "\n"; - else - return "(no data)\n"; + tof: function(v, n) { let i = (1*v); return i.toFixed(n); }, + tof0: function(v) { return this.tof(v, 0); }, + tof1: function(v) { return this.tof(v, 1); }, + fmtSys: function(sys, sats) { + if (!sys.sent) + return " off\n"; + let r = sys.sent + " "; + // r+= sys.d23 + "D "; + if (sats) + // r += sats.sats_used + "/" + sats.snum; + r += sats.summary(); + return r + "\n"; }, - display: function() { - if (ui.display != 1) - return; + drawRace: function() { let m = this.old_msg; - let msg = "" + this.tof(m.time) + "\n" + - "q" + m.quality + " " + m.in_view + " " + m.hdop + "\n" + - "gp"+ this.fmtSys(m.gp) + - "bd" + this.fmtSys(m.bd) + - "gl" + this.fmtSys(m.gl); + let msg = "gmt" + this.tof0(m.time) + "\n" + + "q" + m.quality + " S" + m.in_view + " h" + this.tof0(m.hdop) + "m\n" + + /* "v" + this.tof0(m.vdop) + "m " + "p" + this.tof0(m.pdop) + "m\n" + */ + this.all.summary() + "\n" + + "gp"+ this.fmtSys(m.gp, this.s_gp) + + "bd" + this.fmtSys(m.bd, this.s_bd) + + "gl" + this.fmtSys(m.gl, this.s_gl); if (this.msg.finished != 1) msg += "!"; g.reset().clear().setFont("Vector", 30) @@ -218,37 +326,68 @@ let sky = { .setFontAlign(-1, -1) .drawString(msg, 0, 0); }, + drawEstimates: function() { + /* + Performance Assessment of GNSS Signals in terms of Time to + First Fix for Cold, Warm and Hot Start Matteo Paonni, Marco Anghileri, + Stefan Wallner, José-Ángel Ávila-Rodríguez, Bernd Eissfeller Institute + of Geodesy and Navigation, University FAF Munich, Germany + + => 22 to 26 dB -- long time / no fix / ... + => 26db + -- basically strength no longer matters + */ + let r = this.all.qualest(); + let r1 = this.all.goodest(); + print(r, r1, this.old_msg.hdop, this.old_msg.quality); + ui.drawMsg(r + "\n" + r1 + "\n" + this.old_msg.hdop + "-" + this.old_msg.quality + "d\n" + (getTime() - this.all.sky_start)); + }, + onMessageEnd: function() {}, + messageEnd: function() { + this.old_msg = this.msg; + this.msg = {}; + this.msg.gp = {}; + this.msg.bd = {}; + this.msg.gl = {}; + this.onMessageEnd(); + //print(this.sats); + this.all.onEnd(); + if (this.split) { + this.s_gp.onEnd(); + this.s_gl.onEnd(); + this.s_bd.onEnd(); + } + }, parseRaw: function(msg, lost) { + //print(msg); if (lost) print("## data lost"); let s = msg.split(","); + // print(getTime(), s[0]); + //return; let cmd = s[0].slice(3); //print("cmd", cmd); + if (cmd === "RMC") { + /* Repeat of position/speed/course */ + this.messageEnd(); + return; + } if (cmd === "GGA") { - this.old_msg = this.msg; - this.msg = {}; this.msg.time = s[1]; this.msg.quality = s[6]; this.msg.in_view = s[7]; this.msg.hdop = s[8]; - this.msg.gp = {}; - this.msg.bd = {}; - this.msg.gl = {}; - print("-----------------------------------------------"); - print("GGA Time", s[1], "fix quality", s[4], "sats in view ", s[5]); - this.drawSats(this.sats); - if (this.sats_used < 5) - this.sky_start = getTime(); - this.snum = 0; - this.sats = []; - this.sats_used = 0; + + if (this.debug > 0) { + print("-----------------------------------------------"); + print("GGA Time", s[1], "fix quality", s[4], "sats in view ", s[5]); + } return; } if (cmd === "GLL") return; /* Position lat/lon */ if (cmd === "GSA") { /* - $GNGSA,A,1,,,,,,,,,,,,,25.5,25.5,25.5,4*04 - 0 1 2 15 16 17 18 - */ + $GNGSA,A,1,,,,,,,,,,,,,25.5,25.5,25.5,4*04 + 0 1 2 15 16 17 18 + */ /* Satelites used, fix type! INTERESTING */ let sys = s[18]; let add = {}; @@ -256,41 +395,48 @@ let sky = { add.pdop = s[15]; add.hdop = s[16]; add.vdop = s[17]; + sys = sys[0]; /* FIXME -- should really add to the sentence */ if (sys == 1) { this.msg.gp = add; } else if (sys == 2) { this.msg.gl = add; } else if (sys == 4) { this.msg.bd = add; } - else print("GSA Unknown system\n"); - - print(msg); + else { + print("GSA Unknown system -- ", sys, "\n"); + print(msg); + } return; } if (s[0] === "$GPGSV") { - print("Have gps sentences", s[1], "/", s[2]); - this.parseSats(s); + if (this.debug > 0) + print("Have gps sentences", s[1], "/", s[2]); + this.all.parseSats(s); + if (this.split) + this.s_gp.parseSats(s); this.msg.gp.sent = ""+s[2]; return; } if (s[0] === "$BDGSV") { - print("Have baidu sentences", s[1], "/", s[2]); - this.parseSats(s); + if (this.debug > 0) + print("Have baidu sentences", s[1], "/", s[2]); + this.all.parseSats(s); + if (this.split) + this.s_bd.parseSats(s); this.msg.bd.sent = ""+s[2]; return; } if (s[0] === "$GLGSV") { - print("Have glonass sentences", s[1], "/", s[2]); - this.parseSats(s); + if (this.debug > 0) + print("Have glonass sentences", s[1], "/", s[2]); + this.all.parseSats(s); + if (this.split) + this.s_gl.parseSats(s); this.msg.gl.sent = ""+s[2]; return; } - if (cmd === "RMC") return; /* Repeat of position/speed/course */ + if (cmd === "VTG") return; /* Speeds in knots/kph */ if (cmd === "ZDA") return; /* Time + timezone */ - if (cmd === "TXT") { - this.msg.finished = 1; - return; /* Misc text? antena open */ - } - + if (cmd === "TXT") return; /* Misc text? antena open */ print(msg); }, casic_cmd: function (cmd) { @@ -314,20 +460,32 @@ let sky = { function start() { Bangle.setGPSPower(1); - Bangle.on('GPS-raw', function(a, b) { sky.parseRaw(a, b); }); + Bangle.on('GPS-raw', (a, b) => sky.parseRaw(a, b)); setTimeout(function() { Bangle.removeAllListeners('GPS-raw'); }, 1000000); - setInterval(function() { sky.display(); }, 1000); +} + +function onMessage() { + /* quality.updateGps(); /* FIXME -- for skyspy + if (ui.display == 4) + sky.drawEstimates(); + */ + if (ui.display == 0) + sky.drawSats(sky.all.sats); + if (ui.display == 1) + sky.drawRace(); } // CASIC_CMD("$PCAS06,0"); /* Query product information */ setTimeout(() => sky.casic_cmd("$PCAS04,7"), 1000); /* Enable gps + beidou + glonass */ -setTimeout(() => sky.casic_cmd("$PCAS03,1,1,1,1,1,1,1,1"), 1000); /* Enable gps + beidou + glonass */ +setTimeout(() => sky.casic_cmd("$PCAS03,1,1,1,1,1,1,1,1"), 1500); /* Enable all messages */ //setTimeout(() => sky.casic_cmd("$PCAS10,2"), 1200); /* 2: cold start, 1 warm start, 0: hot start */ ui.init(); ui.topLeft = () => sky.selectSpace(); Bangle.on("drag", (b) => ui.touchHandler(b)); +sky.onMessageEnd = onMessage; +sky.init(); start();