Ver Fonte

Harden reset-cause/speed-gating edge cases; wire the test tool up to the real system log

Reset cause:
- Confirmed by inspecting the actual linked binary that reset_cause already
  lived in .noinit, correctly excluded from avr-libc's bss-clear loop
  (__bss_end == start of .noinit == reset_cause's address), and that
  get_mcusr_from_r2() compiles to exactly two instructions (mov, sts) with
  no stack/r1 dependency - the reset vector jumps straight into it.
- Added an ISP-without-Optiboot fallback: if r2 came back 0 (no bootloader
  ran to populate it - Optiboot's app-start path only ever reaches the app
  with a nonzero ch), fall back to reading MCUSR directly before clearing it
  and disabling the watchdog ourselves, since Optiboot wouldn't have.

Speed gating: the 3x cap now only applies once at least 60 samples have
accumulated (so the early-session near-zero average doesn't clip almost
every sample), and never caps below 20 km/h - a backstop against wild
outliers, not a speed limit, since this logger is also meant for car/train
sessions where real fast segments must get through.

Malformed-NMEA regression test (gps-test-tool):
- New fixture malformed_test.nmea: the three real invalid epochs (RMC status
  V, VTG mode N) from the 2026-09-26 hike log with their real surrounding
  context, plus synthetic edge cases (truncated sentence, bad checksum, VTG
  missing its mode field, GSA with an empty fix-type field, a very long
  line), plus a 65-sample warm-up followed by the real HDOP-16.2/44.8 km/h
  anomaly (must be excluded outright) and a good-HDOP 200 km/h spike (must
  be capped, not excluded).
- New test-malformed driver (test_malformed.c) feeds these through the real
  gps_parse(), asserting no crash and that the speed average is left alone
  by the invalid lines and behaves as above for the two anomaly cases. Also
  clean under -fsanitize=address,undefined. `make run-test-malformed` builds
  and runs it.
- Found and fixed two real NULL-deref risks doing this: gp_gsa_parse()'s
  fix-type field and gp_vtg_parse()'s mode-indicator/speed fields weren't
  guarded against a missing column (a shorter/older NMEA dialect than this
  MTK receiver emits).

Test tool now exercises the real system log, not just GPX output:
- soft/syslog.c is compiled into gps-test-tool too (new syslog_wrapper.c),
  instead of being no-op stubbed out, so running the tool produces the same
  BOOT/CFG/FIX/SBAS/ST/REJ/SUMMARY lines the firmware would (into the
  existing debug_*.txt, which already served as this tool's stand-in for the
  system log). process_nmea_file() now mirrors soft/main.c's event/status
  triggers, paced off the NMEA timestamps since there's no real clock here.
- Fixed a real cross-translation-unit bug this surfaced: FLAGS was declared
  `static` in the shared PC-build header, so nmea.c (compiled via
  nmea_actual_wrapper.c) and this new event-tracking code (in main.c) were
  each mutating their own private copy - the fix/SBAS tracking would never
  have seen F_GPSOK. Made it a single externally-linked variable.
- Verified end-to-end against the full real 2026-09-26 hike log (260903
  NMEA lines, ~9h20m, 33608 points): GPX is well-formed with sane altitudes
  now that nloc.alt is set (previous session's fix), the log is dense but
  no longer spammy (the original "Write with filters disabled" x32,984
  complaint: now 0 occurrences), REJ/ST/SUMMARY read sensibly, and the
  exact real anomaly this session's gating was written for shows up as
  `FIX 2D/D regained ... hdop=16.22` without disturbing avg_speed vs
  avg_dist in the surrounding SUMMARY lines.
- Known test-tool-only gaps, not firmware bugs: auto-pause (tracking_auto_
  paused/auto_pause_process) lives only in soft/main.c's main loop, so this
  harness never exercises it - session summaries here always show
  stopped=0h00m and no PAUSE/RESUME events. The very first FIX event after
  cold start can show a stale sats=0 (the fix/sbas transition check runs
  per NMEA sentence, same as the firmware, so it can observe a GSA/VTG
  before that epoch's GGA has updated satellite count) - same behavior
  as real firmware, not introduced by the test tool.

The 2026-09-26 hike log this was tested against (a large real-world
NMEA capture) is intentionally left untracked in the repo root; only the
small extracted fixture is committed.
k4be há 1 semana atrás
pai
commit
3f6735e9c3

+ 2 - 0
gps-test-tool/.gitignore

@@ -1,5 +1,7 @@
 gps-test-tool
+test-malformed
 *.nmea
+!malformed_test.nmea
 *.NMEA
 *.gpx
 *.o

+ 12 - 3
gps-test-tool/Makefile

@@ -5,10 +5,13 @@ CFLAGS = -Wall -Wextra -g -O2 -I. -I../soft -DPC_BUILD
 LDFLAGS = -lm
 
 TARGET = gps-test-tool
-SOURCES = main.c gpx_wrapper.c nmea_actual_wrapper.c
+SOURCES = main.c gpx_wrapper.c nmea_actual_wrapper.c syslog_wrapper.c
 OBJECTS = $(SOURCES:.c=.o)
 
-.PHONY: all clean
+TEST_MALFORMED = test-malformed
+TEST_MALFORMED_OBJECTS = test_malformed.o gpx_wrapper.o nmea_actual_wrapper.o syslog_wrapper.o
+
+.PHONY: all clean run-test-malformed
 
 all: $(TARGET)
 
@@ -18,11 +21,17 @@ $(TARGET): $(OBJECTS)
 	@echo "Build complete! Run with:"
 	@echo "  ./$(TARGET) <input.nmea>"
 
+$(TEST_MALFORMED): $(TEST_MALFORMED_OBJECTS)
+	$(CC) $(TEST_MALFORMED_OBJECTS) -o $(TEST_MALFORMED) $(LDFLAGS)
+
+run-test-malformed: $(TEST_MALFORMED)
+	./$(TEST_MALFORMED) malformed_test.nmea
+
 %.o: %.c avr_compat.h
 	$(CC) $(CFLAGS) -c $< -o $@
 
 clean:
-	rm -f $(OBJECTS) $(TARGET) output_*.gpx debug_*.txt
+	rm -f $(OBJECTS) $(TEST_MALFORMED) test_malformed.o $(TARGET) output_*.gpx debug_*.txt
 
 test: $(TARGET)
 	@echo "Running test with sample NMEA data..."

+ 56 - 0
gps-test-tool/main.c

@@ -19,10 +19,24 @@ volatile struct system_s System;
 struct location_s location;
 time_t utc;
 unsigned char config_flags = 0;
+unsigned char FLAGS = 0;
+volatile unsigned char reset_cause = 0; /* no real MCU reset happened; log_boot_record() reports "UNKNOWN" */
 
 /* File handle for debug output redirection */
 FILE *debug_output = NULL;
 
+/* soft/syslog.c's log_config()/log_boot_record() print these; not part of
+ * this PC build otherwise (they're normally in soft/settings.c). */
+__flash const char gnss_name_stub[] = "GPS+GLONASS+GALILEO/etc (PC test tool - not tracked per mode)";
+__flash const char *gnss_names[] = {
+	gnss_name_stub, gnss_name_stub, gnss_name_stub,
+	gnss_name_stub, gnss_name_stub, gnss_name_stub,
+};
+__flash const char uart_log_mode_stub[] = "n/a (PC test tool)";
+__flash const char *uart_log_mode_names[] = {
+	uart_log_mode_stub, uart_log_mode_stub, uart_log_mode_stub,
+};
+
 /* Function prototypes from project files */
 unsigned char gpx_init(FIL *file);
 unsigned char gpx_close(FIL *file);
@@ -58,6 +72,8 @@ void reset_system(void) {
     System.conf.min_sats = 4;  /* Require at least 4 satellites */
     System.tracking_paused = 0;
     System.tracking_auto_paused = 0;
+    System.bat_volt = 4.0f;   /* no real ADC here; keeps the status line's v=/bat%= fields sane */
+    System.bat_volt_min = 4.0f;
     utc = 0;
 }
 
@@ -89,6 +105,8 @@ int process_nmea_file(const char *input_file, const char *output_gpx,
 
     /* Reset system state */
     reset_system();
+    gps_powered_on();
+    log_boot_record();
 
     /* Open GPX output file */
     if (f_open(&gpx_file, output_gpx, FA_WRITE | FA_OPEN_ALWAYS) != 0) {
@@ -113,6 +131,10 @@ int process_nmea_file(const char *input_file, const char *output_gpx,
     fprintf(debug_output, "Output: %s\n\n", output_gpx);
 
     /* Process NMEA file line by line */
+    unsigned char fix_ok_prev = 0;
+    unsigned char sbas_prev = 0xFF; /* 0xFF: not yet known, suppress the first (non-)transition */
+    time_t next_status_utc = 0;
+    time_t next_summary_utc = 0;
     while (fgets(line, sizeof(line), input)) {
         size_t len = strlen(line);
 
@@ -140,10 +162,44 @@ int process_nmea_file(const char *input_file, const char *output_gpx,
                 point_count++;
             }
         }
+
+        /* Mirror soft/main.c's system-log event/status triggers, so this
+         * tool exercises and lets you review the same log output the
+         * firmware produces. There's no real clock here, so pace the
+         * periodic status line/session summary off the NMEA timestamps
+         * instead of uptime. */
+        if (utc) {
+            unsigned char fix_ok_now = (FLAGS & F_GPSOK) && !System.sat_count_low;
+            if (fix_ok_now != fix_ok_prev) {
+                if (fix_ok_now)
+                    log_fix_event();
+                else
+                    log_fix_lost();
+                fix_ok_prev = fix_ok_now;
+            }
+            if (System.sbas != sbas_prev) {
+                if (sbas_prev != 0xFF)
+                    log_sbas_transition();
+                sbas_prev = System.sbas;
+            }
+            if (!next_status_utc)
+                next_status_utc = utc + 60;
+            if (utc >= next_status_utc) {
+                log_status_line();
+                next_status_utc = utc + 60;
+            }
+            if (!next_summary_utc)
+                next_summary_utc = utc + 3600;
+            if (utc >= next_summary_utc) {
+                log_session_summary(0);
+                next_summary_utc = utc + 3600;
+            }
+        }
     }
 
     /* Close GPX file */
     gpx_close(&gpx_file);
+    log_session_summary(1);
 
     /* Print summary */
     fprintf(debug_output, "\n=== Summary ===\n");

+ 80 - 10
gps-test-tool/main.h

@@ -162,6 +162,7 @@
 #define CONFFLAG_ENABLE_SBAS     0x02
 #define CONFFLAG_AUTO_PAUSE      0x04
 #define CONFFLAG_VERBOSE_LOG     0x08
+#define CONFFLAG_RESET_ON_NEW_FILE 0x10
 
 /* Reject reasons (see soft/syslog.h) */
 #define REJECT_REASON_KALMAN     0
@@ -199,6 +200,8 @@
 struct config_s {
     unsigned char skip_points;
     unsigned char auto_pause_dist;
+    unsigned char auto_pause_time;
+    unsigned char auto_pause_speed;
     unsigned char gnss_mode;
     unsigned char min_sat;
     unsigned char min_sats;  /* Alias for compatibility */
@@ -221,6 +224,10 @@ struct system_s {
     unsigned int hdop_x100;
     unsigned char fix_type;
     unsigned char speed;                /* km/h */
+    float bat_volt;
+    float bat_volt_min;
+    float temperature;
+    unsigned char temperature_ok;
     time_t time_start;
     time_t current_pause_start;
     time_t pause_time;
@@ -255,6 +262,39 @@ static inline unsigned char get_flag(unsigned char flag) {
     return (config_flags & flag) ? 1 : 0;
 }
 
+/* soft/main.c's helpers, reimplemented here: soft/main.c itself isn't part
+ * of this PC build (this test tool has its own main()), but soft/syslog.c
+ * calls into them. */
+unsigned char is_paused(void); /* soft/gpx.c, via gpx_wrapper.c */
+
+static inline time_t get_pause_time(void) {
+    time_t res = System.pause_time;
+    if (System.current_pause_start < System.time_start)
+        System.current_pause_start = System.time_start;
+    if (is_paused() && System.current_pause_start)
+        res += utc - System.current_pause_start;
+    return res;
+}
+
+static inline unsigned int get_logging_time(void) {
+    if (!utc || !System.time_start)
+        return 0;
+    return utc - System.time_start - get_pause_time();
+}
+
+static inline unsigned int get_avg_speed_x100(void) {
+    if (!System.speed_sample_count)
+        return 0;
+    return System.speed_accum_x100 / System.speed_sample_count;
+}
+
+static inline unsigned int get_dist_avg_speed_x100(void) {
+    unsigned int moving = get_logging_time();
+    if (!moving)
+        return 0;
+    return (unsigned int)((unsigned long int)System.distance * 36UL / (10UL * moving));
+}
+
 /* Time functions */
 char *get_iso_time(time_t time, unsigned char local);
 void iso_time_to_filename(char *time);
@@ -264,17 +304,42 @@ void gps_initialize(void);
 void add_distance(float dist);
 void add_elevation(float ele_change);
 
-/* System log stubs for the PC build: the structured system log (soft/syslog.c)
- * is not part of this test tool, but gpx.c calls into it. */
-static inline void log_reject(unsigned char reason) { System.rejected_count++; (void)reason; }
-static inline void log_jump(unsigned char is_alt, float meters) { (void)is_alt; (void)meters; }
-
-/* soft/main.c's SD I/O timing instrumentation isn't meaningful for the PC
- * build's stdio-based file stubs; no-op it out. */
+/* The structured system log (soft/syslog.c) is compiled into this test tool
+ * too (see syslog_wrapper.c), so its real log_reject()/log_jump()/log_prefix()
+ * etc. are used here, not stubs - see gnss_names/uart_log_mode_names/
+ * reset_cause below for what it needs that isn't otherwise part of this
+ * PC build. */
+void log_prefix(void);
+void log_reject(unsigned char reason);
+void log_reject_flush(void);
+void log_jump(unsigned char is_alt, float meters);
+void log_boot_record(void);
+void log_status_line(void);
+void log_fix_event(void);
+void log_fix_lost(void);
+void log_sbas_transition(void);
+void log_session_summary(unsigned char final);
+void gps_powered_on(void);
+
+extern volatile unsigned char reset_cause;
+extern __flash const char *gnss_names[];
+extern __flash const char *uart_log_mode_names[];
+
+/* MCUSR reset-cause bits and _BV(): meaningless on PC (reset_cause is always
+ * 0 here, so log_boot_record() just reports "UNKNOWN"), but syslog.c
+ * references them unconditionally. */
+#define _BV(x) (1 << (x))
+#define WDRF 3
+#define BORF 2
+#define EXTRF 1
+#define PORF 0
+
+/* soft/main.c's SD I/O timing and stack-headroom instrumentation isn't
+ * meaningful for the PC build's stdio-based file stubs; stub it out. */
 static inline unsigned long int get_uptime_ms(void) { return 0; }
 static inline void io_mark(unsigned long int start_ms) { (void)start_ms; }
-static inline void log_prefix(void) {}
-static inline void log_reject_flush(void) {}
+static inline unsigned int io_get_and_reset_max(void) { return 0; }
+static inline unsigned int get_stack_free_bytes(void) { return 0; }
 
 /* Stub functions for PC build */
 #ifdef PC_BUILD
@@ -286,7 +351,12 @@ static inline int uart0_test(void) { return 0; }
 static inline unsigned char uart0_get(void) { return 0; }
 static inline void uart0_put(char c) { (void)c; }
 static inline void uart1_put(char c) { (void)c; }
-static unsigned char FLAGS = 0;
+/* Must have external linkage (defined once, in main.c/test_malformed.c):
+ * nmea.c (compiled via nmea_actual_wrapper.c, a separate translation unit)
+ * sets F_GPSOK here, and the driver loop needs to see that same flag. A
+ * `static` FLAGS in this shared header would give every .c file its own
+ * private copy, silently discarding those updates. */
+extern unsigned char FLAGS;
 #define F_LVD 0x01
 #define F_POWEROFF 0x02
 #define F_GPSOK 0x04

+ 316 - 0
gps-test-tool/malformed_test.nmea

@@ -0,0 +1,316 @@
+# Section 1: real invalid epochs (RMC status V, VTG mode N) from the
+# 2026-09-26 hike log (2026-09-26T07-31-40_000p02-NMEA_LOG), with their
+# real surrounding context. Must not crash the parser, and the speed
+# average must be unchanged by the V/N lines specifically (checked by
+# the test driver, which recognizes them by content).
+$GNGGA,123903.000,,,,,0,0,,,M,,M,,*5C
+$GPGSA,A,1,,,,,,,,,,,,,,,*1E
+$GLGSA,A,1,,,,,,,,,,,,,,,*02
+$GAGSA,A,1,,,,,,,,,,,,,,,*0F
+$GNRMC,123903.000,V,,,,,2.33,58.89,260926,,,N*6E
+$GNVTG,58.89,T,,M,2.33,N,4.31,K,N*14
+$GNGGA,123904.000,5037.6400,N,01624.8674,E,2,3,16.26,553.8,M,43.6,M,,*79
+$GPGSA,A,2,07,14,,,,,,,,,,,16.29,16.26,1.00*0F
+$GLGSA,A,2,67,,,,,,,,,,,,16.29,16.26,1.00*10
+$GAGSA,A,2,,,,,,,,,,,,,16.29,16.26,1.00*1C
+$GPGSV,3,1,11,30,67,069,12,20,57,296,,14,56,142,16,05,51,246,*7F
+$GPGSV,3,2,11,22,41,166,12,21,38,198,,07,29,069,24,15,23,302,*76
+$GPGSV,3,3,11,08,14,047,,18,11,312,,27,02,014,*4C
+$GLGSV,2,1,07,68,70,325,16,78,61,196,,67,53,132,16,69,19,317,*6F
+$GNGGA,123911.000,,,,,0,0,,,M,,M,,*5F
+$GPGSA,A,1,,,,,,,,,,,,,,,*1E
+$GLGSA,A,1,,,,,,,,,,,,,,,*02
+$GAGSA,A,1,,,,,,,,,,,,,,,*0F
+$GNRMC,123911.000,V,,,,,0.18,58.89,260926,,,N*66
+$GNVTG,58.89,T,,M,0.18,N,0.34,K,N*1E
+$GNGGA,123912.000,,,,,0,0,,,M,,M,,*5C
+$GPGSA,A,1,,,,,,,,,,,,,,,*1E
+$GLGSA,A,1,,,,,,,,,,,,,,,*02
+$GAGSA,A,1,,,,,,,,,,,,,,,*0F
+$GNRMC,123912.000,V,,,,,0.02,58.89,260926,,,N*6E
+$GNVTG,58.89,T,,M,0.02,N,0.03,K,N*11
+$GNGGA,123913.000,5037.6383,N,01624.8556,E,2,3,3.82,554.1,M,43.6,M,,*44
+$GPGSA,A,2,14,,,,,,,,,,,,3.95,3.82,1.00*01
+$GLGSA,A,2,68,,,,,,,,,,,,3.95,3.82,1.00*16
+$GAGSA,A,2,29,,,,,,,,,,,,3.95,3.82,1.00*1E
+
+# Section 2: malformed/edge-case sentences. None of these may crash the
+# parser; they are all expected to be silently rejected or ignored.
+$GNGGA,123903.000,,,,,0,0
+$GNRMC,123920.000,A,5037.6400,N,01624.8674,E,2.86,58.89,260926,,,D*00
+$GNVTG,58.89,T,,M,2.33,N,4.31,K*76
+$GPGSA,A,,,,,,,,,,,,,,,,*2F
+$GPGSV,3,1,11,30,67,069,111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111*46
+
+# Section 3: HDOP-based speed gating. 65 warm-up epochs at ~4.3 km/h
+# with a good HDOP (clears the 60-sample minimum before capping kicks
+# in), then one low-quality (HDOP 16.2) fix reporting 44.8 km/h - the
+# same shape as the real hike anomaly this gating was added for - which
+# must be excluded outright, then one good-quality fix reporting a 200
+# km/h spike, which must be capped rather than excluded (the cap is a
+# backstop, not a speed limit: real fast segments, e.g. car/train, must
+# still get through).
+$GNRMC,130000.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4E
+$GNGGA,130000.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*43
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130001.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4F
+$GNGGA,130001.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*42
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130002.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4C
+$GNGGA,130002.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*41
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130003.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4D
+$GNGGA,130003.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*40
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130004.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4A
+$GNGGA,130004.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*47
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130005.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4B
+$GNGGA,130005.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*46
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130006.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*48
+$GNGGA,130006.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*45
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130007.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*49
+$GNGGA,130007.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*44
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130008.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*46
+$GNGGA,130008.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*4B
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130009.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*47
+$GNGGA,130009.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*4A
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130010.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4F
+$GNGGA,130010.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*42
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130011.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4E
+$GNGGA,130011.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*43
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130012.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4D
+$GNGGA,130012.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*40
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130013.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4C
+$GNGGA,130013.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*41
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130014.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4B
+$GNGGA,130014.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*46
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130015.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4A
+$GNGGA,130015.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*47
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130016.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*49
+$GNGGA,130016.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*44
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130017.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*48
+$GNGGA,130017.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*45
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130018.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*47
+$GNGGA,130018.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*4A
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130019.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*46
+$GNGGA,130019.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*4B
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130020.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4C
+$GNGGA,130020.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*41
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130021.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4D
+$GNGGA,130021.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*40
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130022.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4E
+$GNGGA,130022.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*43
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130023.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4F
+$GNGGA,130023.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*42
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130024.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*48
+$GNGGA,130024.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*45
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130025.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*49
+$GNGGA,130025.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*44
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130026.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4A
+$GNGGA,130026.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*47
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130027.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4B
+$GNGGA,130027.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*46
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130028.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*44
+$GNGGA,130028.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*49
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130029.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*45
+$GNGGA,130029.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*48
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130030.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4D
+$GNGGA,130030.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*40
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130031.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4C
+$GNGGA,130031.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*41
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130032.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4F
+$GNGGA,130032.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*42
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130033.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4E
+$GNGGA,130033.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*43
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130034.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*49
+$GNGGA,130034.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*44
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130035.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*48
+$GNGGA,130035.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*45
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130036.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4B
+$GNGGA,130036.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*46
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130037.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4A
+$GNGGA,130037.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*47
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130038.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*45
+$GNGGA,130038.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*48
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130039.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*44
+$GNGGA,130039.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*49
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130040.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4A
+$GNGGA,130040.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*47
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130041.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4B
+$GNGGA,130041.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*46
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130042.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*48
+$GNGGA,130042.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*45
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130043.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*49
+$GNGGA,130043.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*44
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130044.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4E
+$GNGGA,130044.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*43
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130045.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4F
+$GNGGA,130045.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*42
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130046.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4C
+$GNGGA,130046.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*41
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130047.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4D
+$GNGGA,130047.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*40
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130048.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*42
+$GNGGA,130048.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*4F
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130049.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*43
+$GNGGA,130049.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*4E
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130050.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4B
+$GNGGA,130050.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*46
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130051.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4A
+$GNGGA,130051.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*47
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130052.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*49
+$GNGGA,130052.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*44
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130053.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*48
+$GNGGA,130053.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*45
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130054.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4F
+$GNGGA,130054.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*42
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130055.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4E
+$GNGGA,130055.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*43
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130056.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4D
+$GNGGA,130056.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*40
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130057.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4C
+$GNGGA,130057.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*41
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130058.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*43
+$GNGGA,130058.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*4E
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130059.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*42
+$GNGGA,130059.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*4F
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130100.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4F
+$GNGGA,130100.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*42
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130101.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4E
+$GNGGA,130101.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*43
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130102.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4D
+$GNGGA,130102.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*40
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130103.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4C
+$GNGGA,130103.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*41
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GNRMC,130104.000,A,5037.6400,N,01624.8674,E,2.32,58.89,260926,,,D*4B
+$GNGGA,130104.000,5037.6400,N,01624.8674,E,2,6,1.20,553.8,M,43.6,M,,*46
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,2.32,N,4.30,K,D*1E
+$GPGSA,A,3,07,,,,,,,,,,,,16.90,16.20,1.00*0F
+$GNVTG,58.89,T,,M,24.19,N,44.80,K,D*1C
+$GPGSA,A,3,07,14,22,21,08,18,,,,,,,1.30,1.20,1.00*02
+$GNVTG,58.89,T,,M,108.0,N,200.0,K,D*11

+ 10 - 0
gps-test-tool/syslog_wrapper.c

@@ -0,0 +1,10 @@
+/*
+ * Syslog Wrapper for PC Compilation
+ * Simply includes actual syslog.c - main.h handles all PC/embedded switches
+ */
+
+#define PC_BUILD
+#include "main.h"
+
+/* Include the actual syslog.c source */
+#include "../soft/syslog.c"

+ 144 - 0
gps-test-tool/test_malformed.c

@@ -0,0 +1,144 @@
+/*
+ * Malformed/edge-case NMEA regression test.
+ *
+ * Feeds soft/nmea.c's real gps_parse() (through the same PC_BUILD shim as
+ * the rest of this test tool) a fixture of real and synthetic sentences
+ * that are known to be awkward: invalid epochs from an actual hike log,
+ * truncated/bad-checksum/short-dialect sentences, and a run long enough to
+ * exercise the HDOP-based speed-sample gating added after that hike showed
+ * a single poor-quality fix (HDOP 16, 44.8 km/h) skewing the whole-session
+ * average.
+ *
+ * Nothing in malformed_test.nmea should crash the parser (run under a tool
+ * like valgrind or ASan for a stronger guarantee than "the process exited
+ * zero"), and the checks below assert the speed average is actually left
+ * alone by the invalid/poor-quality samples, and is capped rather than
+ * excluded for a genuinely fast, good-quality one.
+ *
+ * Usage: ./test-malformed [malformed_test.nmea]
+ */
+
+#define PC_BUILD
+#include "main.h"
+
+volatile struct system_s System;
+struct location_s location;
+time_t utc;
+unsigned char config_flags = 0;
+unsigned char FLAGS = 0;
+volatile unsigned char reset_cause = 0;
+FILE *debug_output = NULL;
+
+__flash const char gnss_name_stub[] = "n/a";
+__flash const char *gnss_names[] = { gnss_name_stub, gnss_name_stub, gnss_name_stub, gnss_name_stub, gnss_name_stub, gnss_name_stub };
+__flash const char uart_log_mode_stub[] = "n/a";
+__flash const char *uart_log_mode_names[] = { uart_log_mode_stub, uart_log_mode_stub, uart_log_mode_stub };
+
+time_t gps_parse(const char *nmea);
+
+char *get_iso_time(time_t time, unsigned char local) {
+	(void)local;
+	static char output[32];
+	struct tm *ct = gmtime(&time);
+	sprintf(output, "%04d-%02d-%02dT%02d:%02d:%02d.000Z",
+		ct->tm_year + 1900, ct->tm_mon + 1, ct->tm_mday,
+		ct->tm_hour, ct->tm_min, ct->tm_sec);
+	return output;
+}
+
+void iso_time_to_filename(char *time) {
+	(void)time;
+}
+
+static int failures;
+
+static void check(int cond, const char *what) {
+	if (!cond) {
+		fprintf(stderr, "FAIL: %s\n", what);
+		failures++;
+	} else {
+		printf("ok: %s\n", what);
+	}
+}
+
+/* Recognize the known-invalid lines from malformed_test.nmea's section 1,
+ * to check precisely that they leave the speed average alone - not just
+ * that nothing crashes. */
+static int is_invalid_epoch_line(const char *line) {
+	return strstr(line, ",V,,,,,") != NULL				/* RMC: empty lat/lon, status V */
+		|| (strstr(line, "VTG") && strstr(line, ",N*"));	/* VTG: mode indicator N (not valid) */
+}
+
+int main(int argc, char *argv[]) {
+	const char *path = argc > 1 ? argv[1] : "malformed_test.nmea";
+	FILE *f = fopen(path, "r");
+	char raw[512];
+	char line[512];
+	int total_lines = 0;
+
+	if (!f) {
+		fprintf(stderr, "Cannot open %s\n", path);
+		return 1;
+	}
+
+	memset((void *)&System, 0, sizeof(System));
+	memset(&location, 0, sizeof(location));
+	System.conf.min_sats = 4;
+	utc = 0;
+
+	while (fgets(raw, sizeof raw, f)) {
+		unsigned long int count_before, accum_before;
+		size_t len = strlen(raw);
+
+		while (len && (raw[len - 1] == '\n' || raw[len - 1] == '\r'))
+			raw[--len] = '\0';
+		if (len == 0 || raw[0] == '#')
+			continue; /* blank line or fixture comment, not fed to the parser */
+
+		snprintf(line, sizeof line, "%s\r\n", raw);
+		total_lines++;
+
+		count_before = System.speed_sample_count;
+		accum_before = System.speed_accum_x100;
+
+		gps_parse(line); /* must not crash, regardless of how malformed the line is */
+
+		if (is_invalid_epoch_line(raw)) {
+			char what[560];
+			snprintf(what, sizeof what, "no speed sample added by: %s", raw);
+			check(System.speed_sample_count == count_before && System.speed_accum_x100 == accum_before, what);
+		}
+	}
+	fclose(f);
+
+	printf("\nParsed %d lines without crashing.\n", total_lines);
+	check(total_lines > 250, "fixture file actually has content");
+
+	/* Section 3: the HDOP-16.2, 44.8 km/h sample must be excluded outright -
+	 * the average right after it must still equal the ~4.3 km/h warm-up
+	 * average, not be pulled up towards 44.8. */
+	check(System.speed_sample_count >= 65, "warm-up samples were accepted");
+	{
+		double avg = (double)System.speed_accum_x100 / System.speed_sample_count / 100.0;
+		char what[128];
+		snprintf(what, sizeof what, "average (%.2f km/h) unaffected by the HDOP-16.2 44.8 km/h sample", avg);
+		check(avg > 2.0 && avg < 6.0, what);
+	}
+
+	/* The 200 km/h, good-HDOP spike must be capped (added to the sum at
+	 * something well below 200, per SPEED_SAMPLE_CAP_MULT/FLOOR), not
+	 * excluded outright: a real fast segment (car/train) must still count. */
+	{
+		double avg_after = (double)System.speed_accum_x100 / System.speed_sample_count / 100.0;
+		char what[160];
+		snprintf(what, sizeof what, "200 km/h spike was capped, not excluded nor let through raw (average now %.2f km/h)", avg_after);
+		check(avg_after > 2.5 && avg_after < 15.0, what);
+	}
+
+	if (failures) {
+		printf("\n%d check(s) FAILED\n", failures);
+		return 1;
+	}
+	printf("\nAll checks passed.\n");
+	return 0;
+}

+ 11 - 5
soft/main.c

@@ -40,13 +40,19 @@ void get_mcusr_from_r2(void) {
 	__asm__ __volatile__ ("mov %0, r2\n" : "=r" (reset_cause) :);
 }
 
-/* Disable the watchdog as early as possible (in case this image is ever
- * flashed directly via ISP, bypassing Optiboot, which already turns the
- * watchdog off itself before jumping to the application): otherwise a
- * watchdog-triggered reset with a short timeout could loop forever before
- * main() re-enables it with the timeout it actually wants. */
+/* Disable the watchdog as early as possible, and fall back to reading MCUSR
+ * directly if this image is ever flashed straight over ISP, bypassing
+ * Optiboot: r2 is only ever populated by Optiboot's own app-start path (see
+ * above), so if it came back 0, no bootloader ran and r2 just holds whatever
+ * was left in it at power-on - in that case MCUSR itself still holds the
+ * real reset cause (nothing has cleared it yet), and needs disabling the
+ * watchdog ourselves, since Optiboot isn't there to have done it already.
+ * This runs in .init3, after the stack pointer is set up in .init2, so it
+ * can be plain C rather than hand-written asm. */
 void wdt_disable_early(void) __attribute__((naked, used, section(".init3")));
 void wdt_disable_early(void) {
+	if (!reset_cause && MCUSR)
+		reset_cause = MCUSR; /* no bootloader ran: fall back to MCUSR directly, before it's cleared below */
 	MCUSR = 0;
 	wdt_disable();
 }

+ 9 - 3
soft/nmea.c

@@ -204,7 +204,11 @@ static void gp_gga_parse(const char *str) {
  * a single sample from a poor fix (e.g. HDOP 16 with only a few satellites)
  * would otherwise distort the whole-session average by itself. */
 #define SPEED_SAMPLE_HDOP_MAX_X100	500	/* reject samples with HDOP > 5.00 */
-#define SPEED_SAMPLE_CAP_MULT		3	/* also cap any single sample at this multiple of the running average */
+#define SPEED_SAMPLE_CAP_MULT		3	/* also 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
+						 * wild outliers, not a speed limit - a car/train session must still
+						 * get through a genuinely fast segment. */
 
 static void gp_vtg_parse(const char *str) {
 	const char *p;
@@ -227,10 +231,12 @@ static void gp_vtg_parse(const char *str) {
 	 * excluding time spent paused, instead of dividing distance by time. */
 	if (!paused && fix_trustworthy) {
 		unsigned long int sample_x100 = (unsigned long int)(speed*100.0 + 0.5);
-		if (System.speed_sample_count) {
+		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;
-			if (cap_x100 && sample_x100 > cap_x100)
+			if (cap_x100 < SPEED_SAMPLE_CAP_FLOOR_X100)
+				cap_x100 = SPEED_SAMPLE_CAP_FLOOR_X100;
+			if (sample_x100 > cap_x100)
 				sample_x100 = cap_x100;
 		}
 		System.speed_accum_x100 += sample_x100;

+ 4 - 0
soft/syslog.c

@@ -1,4 +1,8 @@
+#ifdef PC_BUILD
+#include "../gps-test-tool/main.h"
+#else
 #include "main.h"
+#endif
 
 #ifndef FW_VERSION
 #define FW_VERSION "unknown"