| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- #ifdef PC_BUILD
- #include "../gps-test-tool/main.h"
- #else
- #include "main.h"
- #endif
- /*----------------------------------------------------*/
- /* Get a line received from GPS module */
- /*----------------------------------------------------*/
- uint16_t get_line ( /* 0:line incomplete or timed out, >0: Number of bytes received. */
- char *buff,
- uint16_t sz_buf
- )
- {
- char c;
- static uint16_t i = 0;
- uint16_t ret_len;
- set_timer(recv_timeout, 1000);
- for (;;) {
- wdt_reset();
- if (FLAGS & (F_LVD | F_POWEROFF))
- return 0; /* A brownout is detected */
- if (timer_expired(recv_timeout))
- return 0; /* timeout; continue the main loop */
- if (System.keypress) /* process user keypress */
- return 0;
- if (!uart0_test()) {
- sleep();
- continue;
- }
- c = (char)uart0_get();
- if (i == 0 && c != '$' )
- continue; /* Find start of line */
- if (c == '\n' || c == '\r') {
- buff[i++] = '\r';
- buff[i++] = '\n';
- buff[i++] = '\0'; /* add null termination for string */
- break; /* EOL */
- }
- buff[i++] = c;
- if (System.conf.uart_log_mode != UART_LOG_MODE_SYSTEM)
- uart1_put(c);
- if (i >= sz_buf - 3) /* keep 3 bytes for terminating character */
- i = 0; /* Buffer overflow (abort this line) */
- }
- ret_len = i;
- i = 0;
- if (ret_len > 0 && System.conf.uart_log_mode != UART_LOG_MODE_SYSTEM) {
- uart1_put('\r');
- uart1_put('\n');
- }
- return ret_len;
- }
- /* Compare sentence header string */
- BYTE gp_comp (const char *str1, __flash const char *str2)
- {
- char c;
- do {
- c = pgm_read_byte(str2++);
- } while (c && c == *str1++);
- return c;
- }
- /* Per-epoch GGA/GSA/logged tracking for nmea_epoch_complete() - partial parse
- * state, not global "fully cooked" data, so kept local to this file rather
- * than on System. */
- static struct {
- unsigned char gga_ok : 1;
- unsigned char gsa_ok : 1;
- unsigned char logged : 1;
- } epoch;
- #define FIELD_BUF_LEN 32
- /* Get a column item */
- static
- const char* gp_col ( /* Returns pointer to the item (returns a NULL when not found) */
- const char* buf, /* Pointer to the sentence */
- BYTE col /* Column number (0 is the 1st item) */
- ) {
- BYTE c;
- static char field_buf[FIELD_BUF_LEN];
- unsigned char length = 0;
- while (col) {
- do {
- c = *buf++;
- if (c <= ' ') return NULL;
- } while (c != ',');
- col--;
- }
- while (*buf && *buf != ',' && length < FIELD_BUF_LEN-1) {
- field_buf[length++] = *buf++;
- }
- field_buf[length] = '\0';
- return field_buf;
- }
- unsigned int gp_val(const char *db, unsigned char count) {
- unsigned int out = 0;
- while (count--) {
- unsigned char n;
- n = *db - '0';
- if (n >= 10)
- return 0;
- out *= 10;
- out += n;
- db++;
- }
- return out;
- }
- #define SPEED_SAMPLE_CAP_MULT 3 /* cap any single sample at this multiple of the running average... */
- #define SPEED_SAMPLE_CAP_MIN_SAMPLES 60 /* ...but only once the average actually means something, ... */
- #define SPEED_SAMPLE_CAP_FLOOR_X100 2000 /* ...and never cap below this (20 km/h): the cap is a backstop against
- * a single wild outlier, not a speed limit - persistence (below) is what
- * lets a genuinely sustained fast segment (car/train) through. */
- #define SPEED_SAMPLE_HIGH_HISTORY_LEN 7 /* consider this many of the most recent samples... */
- #define SPEED_SAMPLE_HIGH_HISTORY_MASK ((1U << SPEED_SAMPLE_HIGH_HISTORY_LEN) - 1)
- #define SPEED_SAMPLE_HIGH_MIN_COUNT 5 /* ...and once this many of them were above the cap, stop clamping:
- * a single spike (e.g. the 200 km/h one-second glitch seen on a real
- * hike) never reaches this, but a sustained fast segment does within a
- * few seconds. */
- /* Bit history of "was this raw sample above the cap" for the last
- * SPEED_SAMPLE_HIGH_HISTORY_LEN samples (bit 0: most recent). Static file
- * scope, not part of System - it's working state for the cap logic only,
- * nothing else needs it. */
- static unsigned char speed_sample_high_history;
- static void speed_sample_process(double speed_kmh) {
- unsigned char paused = tracking_is_paused();
- unsigned char fix_trustworthy = gps_fix_trustworthy();
- System.speed = speed_kmh+0.5;
- /* Average speed: fixed-point accumulation of instantaneous samples,
- * excluding time spent paused, instead of dividing distance by time. */
- if (!paused && fix_trustworthy) {
- unsigned long int sample_x100 = (unsigned long int)(speed_kmh*100.0 + 0.5);
- if (System.speed_sample_count >= SPEED_SAMPLE_CAP_MIN_SAMPLES) {
- unsigned long int avg_x100 = System.speed_accum_x100 / System.speed_sample_count;
- unsigned long int cap_x100 = avg_x100 * SPEED_SAMPLE_CAP_MULT;
- unsigned char is_high;
- if (cap_x100 < SPEED_SAMPLE_CAP_FLOOR_X100)
- cap_x100 = SPEED_SAMPLE_CAP_FLOOR_X100;
- is_high = sample_x100 > cap_x100;
- speed_sample_high_history = (speed_sample_high_history << 1) | is_high;
- if (is_high && __builtin_popcount(speed_sample_high_history & SPEED_SAMPLE_HIGH_HISTORY_MASK) < SPEED_SAMPLE_HIGH_MIN_COUNT)
- sample_x100 = cap_x100; /* not (yet) persistent: clamp as a backstop against a lone spike */
- }
- System.speed_accum_x100 += sample_x100;
- System.speed_sample_count++;
- }
- }
- static time_t gp_rmc_parse(const char *str) {
- const char *p;
- struct tm tmc;
- double speed_knots;
- epoch.gga_ok = 0;
- epoch.gsa_ok = 0;
- epoch.logged = 0;
- p = gp_col(str, 1); /* Get h:m:s */
- if (!p)
- return 0;
- tmc.tm_hour = gp_val(p, 2);
- tmc.tm_min = gp_val(p+2, 2);
- tmc.tm_sec = gp_val(p+4, 2);
- p = gp_col(str, 9); /* Get y:m:d */
- if (!p)
- return 0;
- tmc.tm_mday = gp_val(p, 2);
- tmc.tm_mon = gp_val(p+2, 2) - 1;
- tmc.tm_year = gp_val(p+4, 2) + 100;
- utc = mktime(&tmc); /* Check time validity */
- if (utc == -1)
- return 0;
- p = gp_col(str, 2); /* Get status */
- if (!p || *p != 'A') {
- System.location_valid = LOC_INVALID;
- FLAGS &= ~F_GPSOK;
- return 0; /* Return 0 even is time is valid (comes from module's internal RTC) */
- }
- FLAGS |= F_GPSOK;
- /* Speed over ground, from this same sentence (and the same epoch whose
- * status field was just checked above), in knots - converted to km/h.
- * Not VTG: some receivers omit VTG or send a shorter form of it, and RMC
- * is the one sentence essentially every NMEA GNSS module emits. */
- p = gp_col(str, 7);
- if (p && *p) {
- xatof(&p, &speed_knots);
- speed_sample_process(speed_knots * 1.852);
- }
- return utc;
- }
- /* A fix is only trusted (for acceptance into gpx_process_point(), the speed
- * average, and the auto-pause speed-based unpause check) when it is valid,
- * of a known type, and reasonably accurate: a single sample from a poor fix
- * (e.g. HDOP 16 with only a few satellites) would otherwise distort the
- * whole-session average, corrupt the track with a wild position/altitude
- * outlier, or - worse - talk the auto-pause logic into resuming a stationary
- * logger. */
- #define FIX_QUALITY_HDOP_MAX_X100 500 /* a fix is untrustworthy once HDOP > 5.00 */
- static void gp_gga_parse(const char *str) {
- const char *p;
- double tmp, hdop;
- unsigned int hdop_x100;
- p = gp_col(str, 7); /* satellites used */
- if (!p)
- return;
- System.satellites_used = atoi(p);
- epoch.gga_ok = 1;
- if (System.satellites_used >= System.conf.min_sats) {
- System.sat_count_low = 0;
- } else {
- System.sat_count_low = 1;
- }
- /* HDOP, from this same sentence: GSA (which also reports HDOP, into
- * System.hdop_x100) is parsed after GGA within the epoch, so its value
- * is still last epoch's here - read GGA's own HDOP field instead. */
- p = gp_col(str, 8);
- if (!p)
- return;
- xatof(&p, &hdop);
- hdop_x100 = (unsigned int)(hdop*100.0 + 0.5);
- /* check validity */
- p = gp_col(str, 6);
- if (!p)
- return;
- if (*p == '0') {
- System.location_valid = LOC_INVALID;
- return;
- }
- /* Reject (rather than just not-promote) on a low satellite count, poor
- * HDOP, or anything short of a 3D fix, so a bad fix can't coast on
- * LOC_VALID left over from the last good epoch - otherwise a
- * low-quality point (wrong position and/or altitude) would still be
- * accepted downstream. A 2D fix in particular can't solve for altitude
- * at all (only 3 satellites: 2 horizontal DOF + time, no vertical) and
- * reports a held-over or drifting one instead - seen after a real
- * reacquisition, where a 2D fix reported an altitude a few hundred
- * meters off before enough satellites came in for a real 3D fix.
- * System.fix_type is set by GSA, parsed after GGA within the epoch, so
- * it's last epoch's value here - same staleness as HDOP above, but a
- * bad 2D fix reliably spans several consecutive epochs, so a one-epoch
- * lag isn't a practical gap here (unlike a true point fix). */
- if (!System.sat_count_low && hdop_x100 <= FIX_QUALITY_HDOP_MAX_X100
- && System.fix_type == FIX_TYPE_3D)
- System.location_valid = LOC_VALID_NEW;
- else
- System.location_valid = LOC_INVALID;
- /* parse location */
- p = gp_col(str, 2); /* latitude */
- if (!p)
- return;
- location.lat = gp_val(p, 2); /* degrees */
- p += 2;
- xatof(&p, &tmp); /* minutes */
- tmp /= 60; /* convert minutes to degrees */
- location.lat += tmp;
- p = gp_col(str, 3); /* N/S */
- if (!p)
- return;
- if (*p != 'N')
- location.lat = -location.lat;
- p = gp_col(str, 4); /* longitude */
- if (!p)
- return;
- location.lon = gp_val(p, 3); /* degrees */
- p += 3;
- xatof(&p, &tmp); /* minutes */
- tmp /= 60; /* convert minutes to degrees */
- location.lon += tmp;
- p = gp_col(str, 5); /* E/W */
- if (!p)
- return;
- if (*p != 'E')
- location.lon = -location.lon;
- p = gp_col(str, 6); /* fix type */
- if (!p)
- return;
- if (*p == '2')
- System.sbas = 1;
- else
- System.sbas = 0;
- p = gp_col(str, 9); /* MSL altitude */
- if (!p)
- return;
- xatof(&p, &tmp);
- location.alt = tmp;
- location.time = utc; /* parsed from RMC */
- }
- unsigned char gps_fix_trustworthy(void) {
- return (FLAGS & F_GPSOK) && System.fix_type != FIX_TYPE_UNKNOWN
- && System.hdop_x100 <= FIX_QUALITY_HDOP_MAX_X100;
- }
- static void gp_gsa_parse(const char *str) {
- const char *p;
- double hdop;
- p = gp_col(str, 2); /* fix type: 1 no fix, 2 2D, 3 3D */
- if (p && *p == '2')
- System.fix_type = FIX_TYPE_2D;
- else if (p && *p == '3')
- System.fix_type = FIX_TYPE_3D;
- else
- System.fix_type = FIX_TYPE_UNKNOWN;
- p = gp_col(str, 16); /* HDOP */
- if (p && *p) {
- xatof(&p, &hdop);
- System.hdop_x100 = (unsigned int)(hdop*100.0 + 0.5);
- }
- epoch.gsa_ok = 1;
- }
- /* Returns 1 exactly once per epoch (the moment both GGA and GSA have been
- * parsed for it, whichever arrives second), so callers that need this
- * epoch's satellite count and HDOP together (e.g. logging a fix event) never
- * see a stale value left over from a previous epoch or an earlier line -
- * regardless of which order the receiver sends GGA/GSA in. */
- unsigned char nmea_epoch_complete(void) {
- if (epoch.gga_ok && epoch.gsa_ok && !epoch.logged) {
- epoch.logged = 1;
- return 1;
- }
- return 0;
- }
- /*$PMTK355*31<CR><LF>
- Return $PMTK001,355,3,1,0,0*2E “$PMTK001,355,3,GLON_Enable,BEIDOU_Enable,GALILEO_Enable”
- The GLONASS search mode is enabled. */
- static void pmtk001_parse(const char *str) {
- const char *p;
- /* check validity */
- p = gp_col(str, 1);
- if (strcmp_P(p, PSTR("355"))) /* not the PMTK355 reply */
- return;
- p = gp_col(str, 2);
- if (*p == '0' || *p == '1') { /* invalid / unsupported */
- System.gps_only = 1;
- xputs_P(PSTR("GPS only\r\n"));
- } else {
- System.gps_only = 0;
- xputs_P(PSTR("Multi GNSS\r\n"));
- }
- gps_initialize();
- }
- unsigned char nmea_checksum(const char *str) {
- unsigned char cs = 0;
- while (*str && *str != '*') {
- cs ^= *str++; /* NMEA checksum is quite primitive, but still should catch simple bitstream errors or truncated lines */
- }
- return cs;
- }
- time_t gps_parse(const char *str) { /* Get all required data from NMEA sentences */
- signed int len = strlen(str)-2; /* remove final \r\n */
- const char *checksum;
- unsigned char calc_checksum, inc_checksum;
- signed int i;
- char c;
- if (len < 4)
- return 0; /* each message must contain $.*xx where . is one or more actual data characters and xx is the checksum */
- for (i = len-1; i && i >= (len-2); i--) { /* find checksum */
- if (str[i] == '*')
- break;
- }
- checksum = str+i+1; /* skip * character */
- inc_checksum = 0; /* parse hex string */
- while (*checksum && isalnum(*checksum)) {
- inc_checksum *= 16;
- c = *checksum++;
- if (c >= '0' && c <= '9') {
- inc_checksum += c - '0';
- } else if (c >= 'a' && c <= 'f') {
- inc_checksum += c - 'a' + 10;
- } else if (c >= 'A' && c <= 'F') {
- inc_checksum += c - 'A' + 10;
- } else {
- xputs_P(PSTR("Invalid NMEA: malformed checksum\r\n"));
- return 0; /* invalid checksum character */
- }
- }
-
- str++; /* drop initial $ */
- calc_checksum = nmea_checksum(str);
- if (inc_checksum != calc_checksum) {
- xputs_P(PSTR("Invalid NMEA checksum received\r\n"));
- return 0;
- }
-
- if (!gp_comp(str, PSTR("GPRMC")) || !gp_comp(str, PSTR("GNRMC")) || !gp_comp(str, PSTR("BDRMC")) || !gp_comp(str, PSTR("GARMC"))) {
- return gp_rmc_parse(str);
- }
- if (!gp_comp(str, PSTR("GPGGA")) || !gp_comp(str, PSTR("GNGGA")) || !gp_comp(str, PSTR("BDGGA")) || !gp_comp(str, PSTR("GAGGA"))) {
- gp_gga_parse(str);
- return 0;
- }
- if (!gp_comp(str, PSTR("GPGSA")) || !gp_comp(str, PSTR("GNGSA")) || !gp_comp(str, PSTR("BDGSA")) || !gp_comp(str, PSTR("GAGSA"))) {
- gp_gsa_parse(str);
- return 0;
- }
- if (!System.gps_initialized && !gp_comp(str, PSTR("PMTK011"))) {
- gps_initialize();
- return 0;
- }
- if (!gp_comp(str, PSTR("PMTK001"))) {
- pmtk001_parse(str);
- return 0;
- }
- if (!gp_comp(str, PSTR("PMTK"))) {
- /* Any other PMTK reply (e.g. PMTK705, the firmware release query
- * response) - log it verbatim, no need for a dedicated parser. */
- log_prefix();
- xputs_P(PSTR("RX "));
- xputs(str);
- return 0;
- }
- return 0;
- }
- void uart0_put_wrap(int c) {
- uart0_put((char)c);
- }
- /* Sends the SBAS-enable/disable and GNSS-mode commands for the currently
- * configured settings. Used both as the second step of gps_initialize()'s
- * boot sequence, and directly as the settings menu's .changed callback for
- * the SBAS/GNSS-mode entries - the latter must apply the just-changed
- * setting immediately, regardless of System.gps_initialized, rather than
- * going through gps_initialize()'s boot state machine (which would instead
- * re-send the GNSS-mode query if called before the module's PMTK011 boot
- * line has been seen). */
- void gps_settings_changed(void) {
- /*
- * PMTK353: set gnss search mode (GPS/Galileo/Glonass/Beidou)
- * PMTK313: enable SBAS
- */
- if (get_flag(CONFFLAG_ENABLE_SBAS)) {
- xfprintf(uart0_put_wrap, PSTR("$PMTK313,1*2E\r\n"));
- xputs_P(PSTR("SBAS enable sent\r\n"));
- } else {
- xfprintf(uart0_put_wrap, PSTR("$PMTK313,0*2F\r\n"));
- xputs_P(PSTR("SBAS disable sent\r\n"));
- }
- if (!System.gps_only) {
- switch (System.conf.gnss_mode) {
- default:
- case GNSS_MODE_GPS_GLONASS_GALILEO:
- xfprintf(uart0_put_wrap, PSTR("$PMTK353,1,1,1,0,0*2A\r\n"));
- break;
- case GNSS_MODE_GPS:
- xfprintf(uart0_put_wrap, PSTR("$PMTK353,1,0,0,0,0*2A\r\n"));
- break;
- case GNSS_MODE_GPS_GALILEO:
- xfprintf(uart0_put_wrap, PSTR("$PMTK353,1,0,1,0,0*2B\r\n"));
- break;
- case GNSS_MODE_GALILEO:
- xfprintf(uart0_put_wrap, PSTR("$PMTK353,0,0,1,0,0*2A\r\n"));
- break;
- case GNSS_MODE_GPS_BEIDOU:
- xfprintf(uart0_put_wrap, PSTR("$PMTK353,1,0,0,0,1*2B\r\n"));
- break;
- case GNSS_MODE_BEIDOU:
- xfprintf(uart0_put_wrap, PSTR("$PMTK353,0,0,0,0,1*2A\r\n"));
- break;
- }
- xputs_P(PSTR("GNSS mode setting sent\r\n"));
- } else {
- System.conf.gnss_mode = GNSS_MODE_GPS;
- }
- }
- void gps_initialize(void) {
- /*
- * PMTK355: query gnss search mode (will fail if only GPS is supported)
- * PMTK605: query firmware release; the reply (PMTK705) is logged
- * verbatim by the generic PMTK passthrough in gps_parse()
- */
- switch (System.gps_initialized) {
- case GPS_INIT_NOT_INITIALIZED:
- xfprintf(uart0_put_wrap, PSTR("$PMTK355*31\r\n"));
- xputs_P(PSTR("GNSS mode query sent\r\n"));
- xfprintf(uart0_put_wrap, PSTR("$PMTK605*31\r\n"));
- System.gps_initialized = GPS_INIT_QUERY_SENT;
- break;
- case GPS_INIT_QUERY_SENT:
- gps_settings_changed();
- break;
- default:
- break;
- }
- }
- void check_min_sat_limit(void) {
- if (System.conf.min_sats > 6 && (System.conf.gnss_mode == GNSS_MODE_GPS || System.conf.gnss_mode == GNSS_MODE_GALILEO || System.conf.gnss_mode == GNSS_MODE_BEIDOU)) {
- /* by geometry, max visible number is 6..12 */
- System.conf.min_sats = 6;
- }
- }
|