45 Commit-ok 132898def8 ... 9242c6c9eb

Szerző SHA1 Üzenet Dátum
  k4be 9242c6c9eb Print moving/stopped time and average speeds in test tool summary 3 hete
  k4be 7a5cf4fc75 Gate and debounce fix acquired/lost logging on completed epochs 3 hete
  k4be 6f1dcae5c1 Rework auto-pause resume-by-distance to use an anchor and persistence 3 hete
  k4be a1393896af Flush log files immediately after boot and low-battery events 3 hete
  k4be b93b12ee1f Read speed over ground from RMC instead of VTG 3 hete
  k4be 3308f20baa Use a discharge-curve lookup table for battery percentage 3 hete
  k4be a3c1f2abf5 Account for the Optiboot bootloader when reporting flash usage 3 hete
  k4be 57bf067771 Harden auto-pause against spurious resume/pause on a noisy or winding track 3 hete
  k4be 4d34eb81be Share auto-pause detection between the firmware and gps-test-tool 3 hete
  k4be 91a1c7dbfc Harden reset-cause/speed-gating edge cases; wire the test tool up to the real system log 3 hete
  k4be 77e52cafe2 Fix reset-cause detection under Optiboot, gate speed samples, widen counters, add stack/IO diagnostics 3 hete
  k4be 89ce87fc9a Show device-specific flash/RAM usage percentages in the size report 3 hete
  k4be ad8a6f9c04 Elevation dead-band/smoothing, NMEA-average speed, and a restructured system log 3 hete
  k4be 6078b1da4b Add project CLAUDE.md instructions and ignore gps-test-tool build artifact 3 hete
  k4be 6e0e9f220f Add elevation gain/loss display screen; require multiple consecutive speed points before auto-unpause 3 hete
  kbere 2f395d2108 Include files refactoring 10 hónapja
  k4be 139fe81b28 Add test system for NMEA parsing and GPX generation 10 hónapja
  k4be c50ce61aec Add elevation support. Discard distance data when paused. 10 hónapja
  k4be 0c170402ad Improve distance calculation, always use filtering for it 10 hónapja
  k4be b14680390a Fix LCD=graphic build errors with error message strings 11 hónapja
  k4be 231a84821f Add comprehensive error tracking and browsing system 11 hónapja
  k4be 3fb0005118 Display time in a more readable format 11 hónapja
  k4be 11073a2531 Improve module initialization code 3 éve
  k4be 4abdcabfcd Fix temperature measurement 3 éve
  k4be c8678b954d Add option to reset counters when opening new file 3 éve
  k4be 61916a2550 Stop counting time and distance when paused 3 éve
  k4be 5244baed7d Fix battery icon ordering 3 éve
  k4be 2f32caa8c8 Graphical LCD is usable (not fully utilized) 3 éve
  kbere 95af1f48b8 Start working with graphical LCD 3 éve
  kbere 7e6c20d035 Export schematics for new PCB version 3 éve
  kbere 132898def8 Include files refactoring 10 hónapja
  k4be 849a4926c0 Add test system for NMEA parsing and GPX generation 10 hónapja
  k4be 0904570d7b Add elevation support. Discard distance data when paused. 10 hónapja
  k4be cefb69d394 Improve distance calculation, always use filtering for it 10 hónapja
  k4be 706fbd7c57 Fix LCD=graphic build errors with error message strings 11 hónapja
  k4be 80c8a01353 Add comprehensive error tracking and browsing system 11 hónapja
  k4be 825d2e2059 Display time in a more readable format 11 hónapja
  k4be db12e2fc8a Improve module initialization code 3 éve
  k4be 99428100ff Fix temperature measurement 3 éve
  k4be a424baba0d Add option to reset counters when opening new file 3 éve
  k4be 4d742b3206 Stop counting time and distance when paused 3 éve
  k4be 5812919c4e Fix battery icon ordering 3 éve
  k4be 127dd9d74b Graphical LCD is usable (not fully utilized) 3 éve
  kbere ccd1000e23 Start working with graphical LCD 3 éve
  kbere b946c3ac51 Export schematics for new PCB version 3 éve

+ 82 - 0
CLAUDE.md

@@ -0,0 +1,82 @@
+## Project
+
+GPStracker is a portable device used for acquiring GNSS data via a wireless module and saving tracks onto a memory card.
+
+## Main directory structure
+
+`optiboot` - An "arduino"-type bootloader, modified slightly to handle device's power circuits. Likely does not need any modifications.
+`PCB` - Altium Designer and mechanical files for project PCBs.
+`soft` - Main application code.
+`gps-test-tool` - PC program to process NMEA data just like the embedded device would do, using actual main project code. Used for manual reprocessing of already recorded data and for code testing.
+
+## Main Project source files
+
+Optimally, all program parts should have a `.c` and a corresponding `.h` file.
+`1wire`: Utility for communicating with a Dallas 1-Wire bus.
+`diskio.h`: Header file for memory card states.
+`display`: Main display routines.
+`ds18b20`: Utility for reading temperature sensor data using 1-Wire.
+`expander`: Utility for accessing I2C GPIO expander.
+`ff`: Utility for accessing FAT filesystem.
+`ffconf.h`: FAT configuration
+`ffunicode.c`: FAT helper functions for unicode support
+`gpx`: Handles GPX file generation and writes, also processes and filters location data.
+`HD44780-I2C`: Utility for writing data to an alphanumerical display.
+`I2C`: Utility for communicating with devices on the I2C bus.
+`integer.h`: Type definition header.
+`main`: Main loop, global data storage, device initialization and system utilities.
+`menu`: Utility for drawing nested menus with various item types.
+`mmc`: Utility for accessing a memory card.
+`nmea`: Utility for parsing and generating NMEA messages.
+`settings`: Device configuration and settings menu.
+`stime`: Manages UNIX time conversions.
+`timec`: Calculates local time and generates time and date strings.
+`uart*`: Initializes and handles serial port communication.
+`UC1601S-I2C':Utility for writing data to a graphic display.
+`working_modes`: Provides main display (`default_menu`), handles device state.
+`xprintf`: Utility providing printf-like API.
+
+## Build
+
+Two display types are supported. Always provide display type as `make` argument. Use `make clean` when changing display types. At the moment, both types display identical data.
+
+cd soft
+make clean
+make LCD=alnum
+make clean
+make LCD=graphic
+
+## Notes
+
+User facing (display) messages - always in Polish. Logging, code constructs and code comments always in English.
+Polish character support not available at the time.
+
+## Main Code guidelines
+
+This is a very restricted embedded environment. Minimize RAM and Flash usage, maximize code efficiency. Some of these guidelines may be deemed antipatterns in other environments, but are essential here.
+1. All const variables should be declared as `__flash const`, example `__flash const char _logging_active[] =	"Zapis aktywny";`. Do not use `PROGMEM`.
+2. Const array of strings example:
+```
+__flash const char gnss_gps_glonass_galileo[] = "GPS+GL.NS+GAL.EO";
+__flash const char gnss_gps[] = "GPS";
+__flash const char gnss_gps_galileo[] = "GPS+GALILEO";
+__flash const char gnss_galileo[] = "GALILEO";
+__flash const char gnss_gps_beidou[] = "GPS+BEIDOU";
+__flash const char gnss_beidou[] = "BEIDOU";
+
+__flash const char *gnss_names[] = {
+	gnss_gps_glonass_galileo,
+	gnss_gps,
+	gnss_gps_galileo,
+	gnss_galileo,
+	gnss_gps_beidou,
+	gnss_beidou,
+};
+```
+3. When a constant is to be used as a function argument, also declare the parameter as `__flash const` to allow direct accesses without `pgm_read_XXX()`. If the function (or a struct field) is to support both RAM variables and Flash constants, declare it as `const` and add another function argument / structure member to determine the memory type. Then, RAM data can be accessed directly, and Flash consts with `pgm_read_XXX(addr)` (DON'T USE direct accesses if a passed `const` pointer without `__flash` is for Flash - it won't work!). It is recommended to create separate functions for const and variable parameters (these can, in turn, call a single helper).
+4. Avoid placing auto variables on stack. Don't declare auto (local) variables larger than 4 bytes (beware of structs). If such storage is needed, consider using `static` type instead - even if persistence is not needed. It will consume predictable amount of RAM and generate more efficient code. Only variables allowed on stack are arguments for variadic functions.
+5. Consider using global buffer variables (for example text buffers), to be later used locally by a function, if possible. Be sure other functions won't overwrite it when being in use.
+6. Use `struct system_s` structure, with its only `extern volatile struct system_s System` instance, to keep global state - both for "system" info and for output data from application logic. Ideally, store only "fully cooked" data here - no partial states and unprocessed data, unless needed globally in the application.
+7. For busy loops, use `_delay_ms()` and `_delay_us()` - watch out for watchdog timeouts.
+8. For non-precision delays, use `set_timer(timer_name, time_in_ms)`. Declare new timer names inside `struct timers` (always `unsigned int`!). To check whether time has passed, query `timer_expired(timer_name)`.
+9. Don't add static and global variables to store data which can be cheaply recomputed.

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

@@ -1,6 +1,9 @@
 gps-test-tool
+test-malformed
 *.nmea
+!malformed_test.nmea
 *.NMEA
 *.gpx
 *.o
 *.txt
+a.out

+ 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 autopause_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..."

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

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

+ 84 - 7
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);
@@ -54,10 +68,16 @@ void reset_system(void) {
     memset((void*)&System, 0, sizeof(System));
     memset(&location, 0, sizeof(location));
     System.conf.skip_points = 0;  /* Don't skip points for testing */
-    System.conf.auto_pause_dist = 100;
+    /* Match soft/settings.c's defaults_u8, so this tool's auto-pause behavior
+     * (enabled below) matches an out-of-the-box device. */
+    System.conf.auto_pause_time = 30;
+    System.conf.auto_pause_dist = 10;
+    System.conf.auto_pause_speed = 3;
     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;
 }
 
@@ -85,10 +105,12 @@ int process_nmea_file(const char *input_file, const char *output_gpx,
     }
 
     /* Configure filters */
-    config_flags = filters_enabled ? 0 : CONFFLAG_DISABLE_FILTERS;
+    config_flags = (filters_enabled ? 0 : CONFFLAG_DISABLE_FILTERS) | CONFFLAG_AUTO_PAUSE;
 
     /* 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 +135,13 @@ 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 */
+    /* Kept in sync with soft/main.c's FIX_HOLD_EPOCHS. */
+    #define FIX_HOLD_EPOCHS 5
+    unsigned char fix_ok_prev = 0;
+    unsigned char fix_hold_counter = 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);
 
@@ -137,20 +166,68 @@ int process_nmea_file(const char *input_file, const char *output_gpx,
             /* Only process if we have valid coordinates from GGA */
             if (System.location_valid == LOC_VALID_NEW || System.location_valid == LOC_VALID) {
                 gpx_process_point(&location, &gpx_file);
+                auto_pause_process();
                 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) {
+            if (nmea_epoch_complete()) {
+                unsigned char fix_ok_now = (FLAGS & F_GPSOK) && !System.sat_count_low;
+                if (fix_ok_now == fix_ok_prev) {
+                    fix_hold_counter = 0;
+                } else if (++fix_hold_counter >= FIX_HOLD_EPOCHS) {
+                    fix_hold_counter = 0;
+                    fix_ok_prev = fix_ok_now;
+                    if (fix_ok_now)
+                        log_fix_event();
+                    else
+                        log_fix_lost();
+                }
+            }
+            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");
-    fprintf(debug_output, "Points processed: %d\n", point_count);
-    fprintf(debug_output, "Total distance: %.2f km\n", System.distance / 100000.0);
-    fprintf(debug_output, "Elevation gain: %.1f m\n", System.elevation_gain / 10.0);
-    fprintf(debug_output, "Elevation loss: %.1f m\n", System.elevation_loss / 10.0);
+    {
+        unsigned int moving = get_logging_time();
+        unsigned long int stopped = get_pause_time();
+        fprintf(debug_output, "\n=== Summary ===\n");
+        fprintf(debug_output, "Points processed: %d\n", point_count);
+        fprintf(debug_output, "Total distance: %.2f km\n", System.distance / 100000.0);
+        fprintf(debug_output, "Elevation gain: %.1f m\n", System.elevation_gain / 10.0);
+        fprintf(debug_output, "Elevation loss: %.1f m\n", System.elevation_loss / 10.0);
+        fprintf(debug_output, "Moving time: %uh%02um\n", moving/3600, (moving/60)%60);
+        fprintf(debug_output, "Stopped time: %luh%02lum\n", stopped/3600, (stopped/60)%60);
+        fprintf(debug_output, "Average speed: %.2f km/h\n", get_avg_speed_x100()/100.0);
+        fprintf(debug_output, "Average speed (dist/time): %.2f km/h\n", get_dist_avg_speed_x100()/100.0);
+    }
 
     /* Close files */
     fclose(input);

+ 137 - 3
gps-test-tool/main.h

@@ -161,6 +161,23 @@
 #define CONFFLAG_DISABLE_FILTERS 0x01
 #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
+#define REJECT_REASON_DISTDIFF   1
+#define REJECT_REASON_MINDIST    2
+
+/* UART log mode (see soft/settings.h) */
+#define UART_LOG_MODE_MIXED     0
+#define UART_LOG_MODE_NMEA      1
+#define UART_LOG_MODE_SYSTEM    2
+
+/* Fix type (see soft/main.h) */
+#define FIX_TYPE_UNKNOWN        0
+#define FIX_TYPE_2D             2
+#define FIX_TYPE_3D             3
 
 /* GNSS modes */
 #define GNSS_MODE_GPS_GLONASS_GALILEO 0
@@ -183,18 +200,34 @@
 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 */
+    unsigned char uart_log_mode;
 };
 
 /* System structure */
 struct system_s {
     struct config_s conf;
     unsigned long int distance;         /* cm */
-    unsigned long int elevation_gain;   /* dm (decimeters, 0.1m resolution) */
-    unsigned long int elevation_loss;   /* dm (decimeters, 0.1m resolution) */
+    unsigned long int elevation_gain;   /* dm (decimeters, 0.1m resolution); filtered */
+    unsigned long int elevation_loss;   /* dm; filtered */
+    unsigned long int elevation_gain_raw; /* dm; unfiltered, for comparison only */
+    unsigned long int elevation_loss_raw; /* dm; unfiltered, for comparison only */
+    unsigned long int alt_max;          /* dm; highest smoothed altitude seen */
+    unsigned long int points_written;
+    unsigned long int rejected_count;
+    unsigned long int speed_accum_x100;
+    unsigned long int speed_sample_count;
+    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;
@@ -207,6 +240,9 @@ struct system_s {
     unsigned sat_count_low:1;
     unsigned sbas:1;
     unsigned satellites_used:5;
+    unsigned epoch_gga_ok:1;
+    unsigned epoch_gsa_ok:1;
+    unsigned epoch_logged:1;
 };
 
 /* Location structure */
@@ -217,6 +253,18 @@ struct location_s {
     time_t time;
 };
 
+/* Must match soft/autopause.h exactly: soft/autopause.c is compiled into
+ * this tool too (see autopause_wrapper.c). */
+struct auto_pause_s {
+    unsigned char point_counter;
+    unsigned long int prev_distance;
+    unsigned char speed_counter;
+    struct location_s pause_anchor;
+    unsigned int anchor_sample_count;
+    unsigned char resume_high_history;
+};
+extern struct auto_pause_s auto_pause;
+
 /* Global variables */
 extern volatile struct system_s System;
 extern struct location_s location;
@@ -229,6 +277,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);
@@ -237,6 +318,54 @@ void iso_time_to_filename(char *time);
 void gps_initialize(void);
 void add_distance(float dist);
 void add_elevation(float ele_change);
+float distance(struct location_s *pos1, struct location_s *pos2);
+unsigned char nmea_epoch_complete(void);
+
+/* 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 log_pause_event(unsigned char started);
+void gps_powered_on(void);
+
+/* Auto-pause is real logic (soft/autopause.c), not stubbed - it directly
+ * gates System.distance/elevation_gain/elevation_loss, which is a major part
+ * of what this tool's output looks like. It calls beep(), which is
+ * meaningless on PC and stubbed out here. */
+void auto_pause_process(void);
+unsigned char gps_fix_trustworthy(void);
+static inline void beep(unsigned int len, unsigned char cnt) { (void)len; (void)cnt; }
+
+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 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
@@ -248,7 +377,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;
+}

+ 30 - 2
soft/Makefile

@@ -1,14 +1,33 @@
 ### Project name (also used for output file name)
 PROJECT	= glg
 
+### Firmware version string, embedded in the boot log record
+FW_VERSION_NUM = 1.0.0
+GIT_HASH := $(shell git describe --always --dirty --abbrev=8 2>/dev/null || echo unknown)
+
 ### Source files and search directory
-CSRC    = main.c uart0.c uart1.c ff.c mmc.c 1wire.c ds18b20.c expander.c I2C.c xprintf.c gpx.c ffunicode.c display.c working_modes.o timec.o nmea.o settings.o menu.o
+CSRC    = main.c uart0.c uart1.c ff.c mmc.c 1wire.c ds18b20.c expander.c I2C.c xprintf.c gpx.c ffunicode.c display.c working_modes.o timec.o nmea.o settings.o menu.o syslog.o autopause.o
 ASRC    = stime.S
 VPATH   =
 
 ### Target device
 DEVICE  = atmega644p
 
+### Device memory sizes, for the size report below (bytes)
+FLASH_SIZE = 65536
+RAM_SIZE   = 4096
+EEPROM_SIZE = 2048
+
+### Flash available to this app, i.e. FLASH_SIZE minus the Optiboot bootloader
+### section actually reserved on this device: optiboot/Makefile.1284 forces
+### -DBIGBOOT for the atmega644p target, which hardcodes __BOOT_SIZE__ to
+### 1024 bytes in optiboot.c's "#if BIGBOOT" branch - confirmed independently
+### by the programmed high fuse (hfuse=0x14, optiboot/Makefile:736): BOOTSZ1:
+### BOOTSZ0 = 1:0 = "10", the 644P's 512-word (1024-byte) boot size option,
+### boot address 0x7E00 (word) = 0xFC00 (byte). Re-check both if the fuse
+### settings or optiboot's BIGBOOT option for this target ever change.
+FLASH_APP_SIZE = 64512
+
 ### Bootloader port
 PROGPORT ?= /dev/ttyUSB0
 
@@ -23,6 +42,7 @@ LIBS	=
 LIBDIRS	=
 INCDIRS	=
 DEFS	= F_CPU=7372800
+DEFS	+= 'FW_VERSION="$(FW_VERSION_NUM)+$(GIT_HASH)"'
 # DEFS	+= LEDR_UART
 ADEFS	=
 
@@ -134,11 +154,19 @@ version :
 	@echo
 	$(NM) -n $< > $@
 
-# Display size of file.
+# Display size of file, with a device-specific (DEVICE) usage summary.
 size:
 	@echo
 	$(SIZE) $(PROJECT).elf
 	$(SIZE) $(PROJECT).hex
+	@echo
+	@$(SIZE) -A $(PROJECT).elf | awk '\
+		$$1==".text" || $$1==".data" { prog += $$2 } \
+		$$1==".data" || $$1==".bss" || $$1==".noinit" { ram += $$2 } \
+		END { \
+			printf "Program: %6d bytes (%5.1f%% of %d byte flash on $(DEVICE), %d bytes reserved for the bootloader)\n", prog, prog*100.0/$(FLASH_APP_SIZE), $(FLASH_APP_SIZE), $(FLASH_SIZE) - $(FLASH_APP_SIZE); \
+			printf "Data:    %6d bytes (%5.1f%% of %d byte RAM on $(DEVICE))\n", ram, ram*100.0/$(RAM_SIZE), $(RAM_SIZE); \
+		}'
 
 
 # Link: create ELF output file from object files.

+ 136 - 0
soft/autopause.c

@@ -0,0 +1,136 @@
+/*
+ * Auto-pause detection: shared between the firmware and gps-test-tool (see
+ * autopause_wrapper.c), since it directly gates what ends up in
+ * System.distance/elevation_gain/elevation_loss (excluded while paused) and
+ * is a major part of what those tools' output actually looks like.
+ */
+#ifdef PC_BUILD
+#include "../gps-test-tool/main.h"
+#else
+#include "main.h"
+#endif
+
+struct auto_pause_s auto_pause;
+
+/* Resuming from a period of auto-pause because of apparent distance needs
+ * more apparent movement than triggering the pause did in the first place:
+ * position noise while genuinely stationary (multipath, poor sky view) can
+ * still occasionally clear the plain auto_pause_dist threshold over enough
+ * auto_pause_time windows, and unlike a false pause (which just costs one
+ * quiet window before the speed-based check or the next window catches the
+ * resume), a false resume pollutes the moving/stopped split and the
+ * odometer for as long as it isn't caught. */
+#define AUTO_PAUSE_RESUME_MIN_DIST_M	15	/* resume distance-from-anchor floor, regardless of HDOP */
+#define AUTO_PAUSE_RESUME_HDOP_MULT	5	/* ...and scale up from there by this many meters per HDOP unit -
+						 * a standard rule-of-thumb for GPS position error (HDOP x a typical
+						 * few-meter UERE), so a poor fix needs to move further to count */
+#define AUTO_PAUSE_RESUME_HISTORY_LEN	7	/* the resume condition must hold for most (see MIN_COUNT below) of
+						 * the last this-many samples, not just instantaneously - the same
+						 * persistence idea used for the speed cap, so a few seconds of
+						 * genuine drift-free movement are needed, not one lucky sample */
+#define AUTO_PAUSE_RESUME_HISTORY_MASK	((1U << AUTO_PAUSE_RESUME_HISTORY_LEN) - 1)
+#define AUTO_PAUSE_RESUME_MIN_COUNT	5
+
+static void auto_unpause(void) {
+	if (!System.tracking_auto_paused)
+		return;
+	System.tracking_auto_paused = 0;
+	log_pause_event(0);
+	beep(50, 4);
+}
+
+static void auto_pause_activate(void) {
+	System.tracking_auto_paused = 1;
+	/* Otherwise, speed-counter progress from just before this decision (e.g.
+	 * 2 of the 3 consecutive above-threshold samples the resume check below
+	 * wants) could carry over and complete on the very next sample,
+	 * instantly flip-flopping straight back out of the pause this just
+	 * activated. */
+	auto_pause.speed_counter = 0;
+	/* Anchor the resume-by-distance check on where the pause actually
+	 * started, then let it drift towards the running average of positions
+	 * seen while paused (steadier than any single fix - see
+	 * auto_pause_process()). */
+	auto_pause.pause_anchor = location;
+	auto_pause.anchor_sample_count = 1;
+	auto_pause.resume_high_history = 0;
+	log_pause_event(1);
+	beep(50, 3);
+}
+
+void auto_pause_process(void) {
+	if (System.tracking_paused || !get_flag(CONFFLAG_AUTO_PAUSE)) { /* remove auto-pause */
+		System.tracking_auto_paused = 0;
+		auto_pause.prev_distance = System.distance;
+		auto_pause.point_counter = 0;
+		auto_pause.speed_counter = 0;
+		return;
+	}
+	/* A poor/invalid fix must not be read as "moving": a single noisy sample
+	 * (e.g. a low-satellite, high-HDOP fix briefly reporting tens of km/h,
+	 * as seen on a real hike where reception dropped for a few seconds)
+	 * could otherwise satisfy the "3 consecutive" debounce below on its own
+	 * and resume a logger that is actually sitting still. Such a sample is
+	 * simply ignored - neither counted towards, nor resetting, the
+	 * consecutive-samples debounce - so it can't itself trigger a resume,
+	 * but also can't erase real progress made by genuine movement. */
+	if (gps_fix_trustworthy()) {
+		if (System.speed >= System.conf.auto_pause_speed) { /* unpause when set speed is exceeded for 3 consecutive measurements */
+			if (++auto_pause.speed_counter >= 3) {
+				auto_pause.point_counter = 0;
+				auto_pause.speed_counter = 0;
+				auto_unpause();
+				return;
+			}
+		} else {
+			auto_pause.speed_counter = 0;
+		}
+	}
+	/* Resume-by-distance: compared against an anchor updated to the running
+	 * average position seen while paused (steadier than any single fix),
+	 * scaled by HDOP (a poor fix needs to move further to count), and
+	 * required to persist for most of a short window rather than firing on
+	 * one sample - stationary GPS jitter (well under a meter per second) can
+	 * otherwise accumulate across enough net-movement windows to look like a
+	 * real departure. Evaluated every accepted point, not gated on the
+	 * auto_pause_time window below (that window is for the pause trigger
+	 * only), so the persistence history has one sample per point. */
+	if (System.tracking_auto_paused && gps_fix_trustworthy()) {
+		float resume_threshold_m = AUTO_PAUSE_RESUME_MIN_DIST_M;
+		float hdop_threshold_m = (System.hdop_x100/100.0f) * AUTO_PAUSE_RESUME_HDOP_MULT;
+		float dist_from_anchor_m;
+		unsigned char resumed_this_sample;
+
+		if (hdop_threshold_m > resume_threshold_m)
+			resume_threshold_m = hdop_threshold_m;
+
+		auto_pause.anchor_sample_count++;
+		auto_pause.pause_anchor.lat += (location.lat - auto_pause.pause_anchor.lat) / auto_pause.anchor_sample_count;
+		auto_pause.pause_anchor.lon += (location.lon - auto_pause.pause_anchor.lon) / auto_pause.anchor_sample_count;
+
+		dist_from_anchor_m = distance(&location, &auto_pause.pause_anchor);
+		resumed_this_sample = dist_from_anchor_m > resume_threshold_m;
+		auto_pause.resume_high_history = (auto_pause.resume_high_history << 1) | resumed_this_sample;
+		if (__builtin_popcount(auto_pause.resume_high_history & AUTO_PAUSE_RESUME_HISTORY_MASK) >= AUTO_PAUSE_RESUME_MIN_COUNT) {
+			auto_pause.point_counter = 0;
+			auto_unpause();
+			return;
+		}
+	}
+	if (++auto_pause.point_counter < System.conf.auto_pause_time)
+		return;
+	auto_pause.point_counter = 0;
+	if (!System.tracking_auto_paused) {
+		/* Net displacement over this window can be small even while genuinely
+		 * moving throughout it - a winding path folding back on itself, or a
+		 * short out-and-back - and pausing on that alone, only to have the
+		 * very next sample or two immediately satisfy the speed-based resume
+		 * above, is itself a spurious (if brief) pause. If the logger is
+		 * already moving fast enough to resume on its own right now, don't
+		 * pause it in the first place. */
+		if ((System.distance - auto_pause.prev_distance)/100 <= System.conf.auto_pause_dist
+			&& System.speed < System.conf.auto_pause_speed)
+			auto_pause_activate(); /* pause: not enough net movement, and not currently moving fast */
+	}
+	auto_pause.prev_distance = System.distance;
+}

+ 14 - 0
soft/autopause.h

@@ -0,0 +1,14 @@
+#pragma once
+
+struct auto_pause_s {
+	unsigned char point_counter;
+	unsigned long int prev_distance;
+	unsigned char speed_counter;
+	struct location_s pause_anchor; /* running average position while paused */
+	unsigned int anchor_sample_count;
+	unsigned char resume_high_history; /* bit history: was the anchor-distance/speed resume condition met */
+};
+
+extern struct auto_pause_s auto_pause;
+
+void auto_pause_process(void);

+ 9 - 4
soft/display.c

@@ -231,6 +231,11 @@ void disp_func_ele_sat(void) {
 		strcat_P(disp.line2, PSTR(", DGPS"));
 }
 
+void disp_func_elevation_gain(void) {
+	xsprintf(disp.line1, PSTR("Wznios: %.1fm"), (float)System.elevation_gain / 10.0);
+	xsprintf(disp.line2, PSTR("Zjazd: %.1fm"), (float)System.elevation_loss / 10.0);
+}
+
 static void format_time(char *buf, unsigned int seconds) {
 	unsigned int hours;
 	unsigned char mins, secs;
@@ -272,10 +277,10 @@ void disp_pause_time(void) {
 }
 
 void disp_speed(void) {
-	unsigned int time = get_logging_time();
-	disp_line1(PSTR("Predkosc:"));
-	if (time) {
-		xsprintf(disp.line2, PSTR("%.2f km/h"), (float)System.distance / (float)(time) * 0.036); /* convert seconds to hours */
+	unsigned int avg = get_avg_speed_x100();
+	disp_line1(PSTR("Sr. predkosc:"));
+	if (avg) {
+		xsprintf(disp.line2, PSTR("%.2f km/h"), (float)avg / 100.0);
 	} else {
 		disp_line2(PSTR("nieznana"));
 	}

+ 1 - 0
soft/display.h

@@ -25,6 +25,7 @@ void display_refresh(unsigned char changed);
 void disp_func_main_default(void);
 void disp_func_coord(void);
 void disp_func_ele_sat(void);
+void disp_func_elevation_gain(void);
 void disp_distance_and_time(void);
 void disp_speed(void);
 void disp_time(void);

+ 123 - 15
soft/gpx.c

@@ -31,6 +31,14 @@ struct kalman_s {
 #define AVG_COUNT	3
 #define MIN_DIST_DELTA	2.0
 
+/* Elevation dead-band + smoothing, applied to the filtered ascent/descent totals */
+#define ELEV_SMOOTH_TAU		30.0	/* seconds */
+#define ELEV_DEADBAND		5.0		/* meters */
+
+/* Large-jump detection thresholds (informational logging only, points are not rejected because of these) */
+#define POS_JUMP_THRESHOLD	100.0	/* meters between consecutive raw fixes */
+#define ALT_JUMP_THRESHOLD	50.0	/* meters between consecutive raw fixes */
+
 struct prev_points_s {
 	struct location_s data[PREV_POINTS_LENGTH];
 	unsigned char start;
@@ -43,6 +51,13 @@ struct avg_store_s {
 	time_t time;
 };
 
+struct elevation_s {
+	unsigned char initialized;
+	float smoothed_alt;
+	float baseline;
+	time_t last_time;
+};
+
 static struct gpx_s {
 	struct prev_points_s prev_points;
 	unsigned char avg_count;
@@ -52,11 +67,13 @@ static struct gpx_s {
 	struct location_s last_saved;
 	struct location_s last_distance_point; /* Last accepted point for distance calculation */
 	struct kalman_s kalman[2];
+	struct elevation_s elevation;
 } gpx;
 
 float kalman_predict(struct kalman_s *k, float data);
 void kalman_init(struct kalman_s *k);
-float distance(struct location_s *pos1, struct location_s *pos2);
+float elevation_process(float alt, time_t time);
+void add_elevation_filtered(float amount, unsigned char is_gain);
 
 void prev_points_append(struct location_s *new){
 	gpx.prev_points.data[(gpx.prev_points.start + gpx.prev_points.count)%PREV_POINTS_LENGTH] = *new;
@@ -90,6 +107,7 @@ unsigned char gpx_init(FIL *file) {
 	gpx.last_distance_point.lon = 0;
 	gpx.last_distance_point.lat = 0;
 	gpx.last_distance_point.time = 0;
+	gpx.elevation.initialized = 0;
 
 	gpx.paused = 1; /* make it add a <trkseg> tag */
 
@@ -153,7 +171,13 @@ unsigned char gpx_write(struct location_s *loc, FIL *file) {
 		strcat_P(buf, PSTR("\t\t\t</trkpt>\n"));
 	}
 
-	return f_write(file, buf, strlen(buf), &bw);
+	{
+		unsigned char ret;
+		unsigned long int t0 = get_uptime_ms();
+		ret = f_write(file, buf, strlen(buf), &bw);
+		io_mark(t0);
+		return ret;
+	}
 }
 
 unsigned char gpx_close(FIL *file) {
@@ -187,19 +211,26 @@ void gpx_process_point(struct location_s *loc, FIL *file){
 	filtered_loc.alt = loc->alt;
 
 	if (get_flag(CONFFLAG_DISABLE_FILTERS)) {
-		/* Write unfiltered data to GPX, but calculate distance from filtered data */
-		xputs_P(PSTR("Write with filters disabled\r\n"));
+		/* Write unfiltered data to GPX, but always calculate distance/elevation from filtered data */
+		if (get_flag(CONFFLAG_VERBOSE_LOG))
+			xputs_P(PSTR("Write with filters disabled\r\n"));
 
 		gpx_write(loc, file);
+		System.points_written++;
 
 		/* Calculate distance and elevation from filtered points */
 		if (gpx.last_distance_point.lat != 0) {
 			float ele_change;
 			dist = distance(&gpx.last_distance_point, &filtered_loc);
+			if (dist > POS_JUMP_THRESHOLD)
+				log_jump(0, dist);
 			add_distance(dist);
 			ele_change = filtered_loc.alt - gpx.last_distance_point.alt;
+			if (fabs(ele_change) > ALT_JUMP_THRESHOLD)
+				log_jump(1, ele_change);
 			add_elevation(ele_change);
 		}
+		elevation_process(filtered_loc.alt, filtered_loc.time);
 		gpx.last_distance_point = filtered_loc;
 
 	} else {
@@ -208,7 +239,9 @@ void gpx_process_point(struct location_s *loc, FIL *file){
 		lon_err = fabs(loc->lon - lon_est);
 	//	xprintf(PSTR("lat_err: %e, lon_err: %e, limit: %e\r\n"), lat_err, lon_err, (float)KALMAN_ERR_MAX);
 		if(lat_err > KALMAN_ERR_MAX || lon_err > KALMAN_ERR_MAX){
-			xputs_P(PSTR("KALMAN REJECT\r\n"));
+			if (get_flag(CONFFLAG_VERBOSE_LOG))
+				xputs_P(PSTR("KALMAN REJECT\r\n"));
+			log_reject(REJECT_REASON_KALMAN);
 			return;
 		}
 
@@ -218,27 +251,37 @@ void gpx_process_point(struct location_s *loc, FIL *file){
 			float dist12 = distance(prev_points_get(0), prev_points_get(1));
 			float dist34 = distance(prev_points_get(2), prev_points_get(3));
 			float dist32 = distance(prev_points_get(2), prev_points_get(1));
-			xprintf(PSTR("New distance: %fm\r\n"), dist32);
+			if (get_flag(CONFFLAG_VERBOSE_LOG))
+				xprintf(PSTR("New distance: %fm\r\n"), dist32);
 			if(dist34 > dist12 && dist32 > dist12){
-				xputs_P(PSTR("DISTANCE DIFF REJECT\r\n"));
+				if (get_flag(CONFFLAG_VERBOSE_LOG))
+					xputs_P(PSTR("DISTANCE DIFF REJECT\r\n"));
+				log_reject(REJECT_REASON_DISTDIFF);
 				return;
 			}
+			if (dist32 > POS_JUMP_THRESHOLD)
+				log_jump(0, dist32);
 			ptr = prev_points_get(PREV_POINTS_LENGTH - 2);
 		} else {
 			if(gpx.prev_points.count >= PREV_POINTS_LENGTH-2){
 				ptr = prev_points_get(gpx.prev_points.count - 2);
-				xputs_P(PSTR("NEW\r\n"));
+				if (get_flag(CONFFLAG_VERBOSE_LOG))
+					xputs_P(PSTR("NEW\r\n"));
 			} else {
 				return;
 			}
 		}
 
 		if(distance(&gpx.last_saved, ptr) < MIN_DIST_DELTA){
-			xputs_P(PSTR("Too small position change REJECT\r\n"));
+			if (get_flag(CONFFLAG_VERBOSE_LOG))
+				xputs_P(PSTR("Too small position change REJECT\r\n"));
+			log_reject(REJECT_REASON_MINDIST);
 			return;
 		}
 
-		xputs_P(PSTR("ACCEPT\r\n"));
+		if (get_flag(CONFFLAG_VERBOSE_LOG))
+			xputs_P(PSTR("ACCEPT\r\n"));
+		log_reject_flush(); /* close out any pending reject burst now that good data has resumed */
 
 		/* Calculate distance and elevation for accepted point */
 		if (gpx.last_distance_point.lat != 0) {
@@ -246,8 +289,11 @@ void gpx_process_point(struct location_s *loc, FIL *file){
 			dist = distance(&gpx.last_distance_point, ptr);
 			add_distance(dist);
 			ele_change = ptr->alt - gpx.last_distance_point.alt;
+			if (fabs(ele_change) > ALT_JUMP_THRESHOLD)
+				log_jump(1, ele_change);
 			add_elevation(ele_change);
 		}
+		elevation_process(ptr->alt, ptr->time);
 		gpx.last_distance_point = *ptr;
 
 		gpx.avg_store.lat += ptr->lat;
@@ -259,12 +305,14 @@ void gpx_process_point(struct location_s *loc, FIL *file){
 			nloc.lat = gpx.avg_store.lat / AVG_COUNT;
 			nloc.lon = gpx.avg_store.lon / AVG_COUNT;
 			nloc.time = gpx.avg_store.time;
+			nloc.alt = gpx.elevation.smoothed_alt; /* filtered (smoothed) altitude, for the filtered GPX write */
 			gpx.avg_count = 0;
 			gpx.avg_store.lat = 0;
 			gpx.avg_store.lon = 0;
 			gpx.avg_store.time = 0;
 			gpx.last_saved = nloc;
 			gpx_write(&nloc, file);
+			System.points_written++;
 		}
 	}
 	if (System.time_start == 0)
@@ -316,19 +364,79 @@ void add_distance(float dist) {
 	unsigned char paused = System.tracking_paused || System.tracking_auto_paused;
 	if (!paused)
 		System.distance += (dist+0.005)*100.0;
-	xprintf(PSTR("Distance: %f m; sum: %f m\r\n"), dist, System.distance/100.0);
+	if (get_flag(CONFFLAG_VERBOSE_LOG))
+		xprintf(PSTR("Distance: %.2f m; sum: %.2f m\r\n"), (double)dist, (double)System.distance/100.0);
 }
 
+/* Unfiltered (raw), per-point gain/loss - kept only for comparison against the
+ * filtered (dead-band+smoothed) totals in the periodic status line/session summary. */
 void add_elevation(float ele_change) {
 	unsigned char paused = System.tracking_paused || System.tracking_auto_paused;
 	if (!paused) {
 		if (ele_change > 0) {
-			System.elevation_gain += (ele_change+0.05)*10.0;
+			System.elevation_gain_raw += (ele_change+0.05)*10.0;
 		} else if (ele_change < 0) {
-			System.elevation_loss += (-ele_change+0.05)*10.0;
+			System.elevation_loss_raw += (-ele_change+0.05)*10.0;
+		}
+	}
+	if (get_flag(CONFFLAG_VERBOSE_LOG))
+		xprintf(PSTR("Elevation change: %.1f m; raw gain: %.1f m, raw loss: %.1f m\r\n"),
+			(double)ele_change, (double)System.elevation_gain_raw/10.0, (double)System.elevation_loss_raw/10.0);
+}
+
+/* Filtered (dead-band + smoothed) gain/loss, used for display, GPX ascent stats
+ * and the periodic status line/session summary. */
+void add_elevation_filtered(float amount, unsigned char is_gain) {
+	unsigned long int dm = (unsigned long int)(amount*10.0 + 0.5);
+	if (is_gain)
+		System.elevation_gain += dm;
+	else
+		System.elevation_loss += dm;
+}
+
+/* Exponential smoothing (~30s time constant) followed by a 5m dead-band on the
+ * result, so that a step in the smoothed altitude only counts once it clears
+ * the dead-band, and only the amount past the dead-band edge is credited. */
+float elevation_process(float alt, time_t time) {
+	unsigned char paused = System.tracking_paused || System.tracking_auto_paused;
+	unsigned long int alt_dm;
+	float delta;
+
+	if (!gpx.elevation.initialized) {
+		gpx.elevation.smoothed_alt = alt;
+		gpx.elevation.baseline = alt;
+		gpx.elevation.last_time = time;
+		gpx.elevation.initialized = 1;
+	} else {
+		float dt = (float)(time - gpx.elevation.last_time);
+		float alpha;
+		if (dt <= 0)
+			dt = 1.0;
+		gpx.elevation.last_time = time;
+		alpha = dt / (ELEV_SMOOTH_TAU + dt);
+		gpx.elevation.smoothed_alt += alpha * (alt - gpx.elevation.smoothed_alt);
+	}
+
+	if (gpx.elevation.smoothed_alt > 0) {
+		alt_dm = (unsigned long int)(gpx.elevation.smoothed_alt*10.0 + 0.5);
+		if (alt_dm > System.alt_max)
+			System.alt_max = alt_dm;
+	}
+
+	if (paused) {
+		/* Discard the dead-band reference drift accumulated while paused,
+		 * the same way distance/raw elevation data is discarded when paused. */
+		gpx.elevation.baseline = gpx.elevation.smoothed_alt;
+	} else {
+		delta = gpx.elevation.smoothed_alt - gpx.elevation.baseline;
+		if (delta > ELEV_DEADBAND) {
+			add_elevation_filtered(delta - ELEV_DEADBAND, 1);
+			gpx.elevation.baseline = gpx.elevation.smoothed_alt - ELEV_DEADBAND;
+		} else if (delta < -ELEV_DEADBAND) {
+			add_elevation_filtered(-delta - ELEV_DEADBAND, 0);
+			gpx.elevation.baseline = gpx.elevation.smoothed_alt + ELEV_DEADBAND;
 		}
 	}
-	xprintf(PSTR("Elevation change: %f m; gain: %f m, loss: %f m\r\n"),
-		ele_change, System.elevation_gain/10.0, System.elevation_loss/10.0);
+	return gpx.elevation.smoothed_alt;
 }
 

+ 1 - 0
soft/gpx.h

@@ -9,6 +9,7 @@ unsigned char gpx_write(struct location_s *loc, FIL *file);
 unsigned char gpx_close(FIL *file);
 void gpx_process_point(struct location_s *loc, FIL *file);
 void gpx_save_single_point(struct location_s *loc);
+float distance(struct location_s *pos1, struct location_s *pos2);
 void add_distance(float dist);
 void add_elevation(float ele_change);
 unsigned char is_paused(void);

+ 218 - 71
soft/main.c

@@ -18,7 +18,101 @@ FIL system_log;				/* System log file */
 char Line[100];				/* Line buffer */
 time_t utc;					/* current time */
 struct location_s location;
-struct auto_pause_s auto_pause;
+volatile unsigned long int uptime_ms; /* milliseconds since MCU startup, for the system log */
+static unsigned int io_max_ms; /* longest single SD f_write()/f_sync() duration since the last status line, ms */
+
+/*
+ * Reset cause: this board is always started via Optiboot, not a fresh MCU
+ * reset straight into main(). Optiboot reads MCUSR at its own entry, clears
+ * only WDRF (to avoid looping on its own watchdog-based re-entry), and passes
+ * the *original* MCUSR value to the application in register r2 before jumping
+ * to address 0 - see optiboot.c ("save the reset flags in the designated
+ * register ... by putting code in .init0 ... to save R2 to a global
+ * variable"). By the time main() runs, the real MCUSR register no longer
+ * reflects the actual reset cause, so it must be read out of r2 instead, and
+ * that has to happen in .init0 - the very first code executed - before
+ * anything else can clobber the register.
+ */
+volatile unsigned char reset_cause __attribute__((section(".noinit")));
+void get_mcusr_from_r2(void) __attribute__((naked, used, section(".init0")));
+void get_mcusr_from_r2(void) {
+	__asm__ __volatile__ ("mov %0, r2\n" : "=r" (reset_cause) :);
+}
+
+/* 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();
+}
+
+/*
+ * Stack headroom (AVR121 "StackPaint" pattern): fill all RAM between the end
+ * of .bss/.noinit and the top of RAM with a known byte as early as possible
+ * (.init1, before the stack pointer itself is set up in .init2), then later
+ * count how many bytes of that pattern are still untouched. That count can
+ * only shrink over the session, as the deepest interrupt/call nesting grows,
+ * so it is a running record of the worst-case stack headroom seen so far -
+ * no extra storage needed to remember a "minimum".
+ */
+#define STACK_CANARY 0xc5
+
+extern uint8_t __heap_start;
+extern uint8_t __stack;
+
+void stack_paint(void) __attribute__((naked, used, section(".init1")));
+void stack_paint(void) {
+	__asm__ __volatile__ (
+		"    ldi r30,lo8(__heap_start)\n"
+		"    ldi r31,hi8(__heap_start)\n"
+		"    ldi r24,lo8(0xc5)\n" /* STACK_CANARY */
+		"    ldi r25,hi8(__stack)\n"
+		"    rjmp .Lstackpaint_cmp\n"
+		".Lstackpaint_loop:\n"
+		"    st Z+,r24\n"
+		".Lstackpaint_cmp:\n"
+		"    cpi r30,lo8(__stack)\n"
+		"    cpc r31,r25\n"
+		"    brlo .Lstackpaint_loop\n"
+		"    breq .Lstackpaint_loop\n"
+	);
+}
+
+unsigned int get_stack_free_bytes(void) {
+	const uint8_t *p = &__heap_start;
+	unsigned int count = 0;
+	while (p <= &__stack && *p == STACK_CANARY) {
+		p++;
+		count++;
+	}
+	return count;
+}
+
+void request_immediate_sync(void) {
+	FLAGS |= F_SYNC;
+}
+
+void io_mark(unsigned long int start_ms) {
+	unsigned int dt = (unsigned int)(get_uptime_ms() - start_ms);
+	if (dt > io_max_ms)
+		io_max_ms = dt;
+}
+
+unsigned int io_get_and_reset_max(void) {
+	unsigned int v = io_max_ms;
+	io_max_ms = 0;
+	return v;
+}
 
 void start_bootloader(void) {
 	typedef void (*do_reboot_t)(void);
@@ -51,7 +145,9 @@ ISR(TIMER1_COMPA_vect)
 	unsigned char i;
 	unsigned char k;
 	static unsigned char oldk;
-	
+
+	uptime_ms += 10; /* this ISR fires at 100Hz */
+
 	for(i=0; i<sizeof(System.timers)/sizeof(unsigned int); i++){ // decrement every variable from timers struct unless it's already zero
 		ctimer = ((unsigned int *)&System.timers) + i;
 		if(*ctimer)
@@ -137,8 +233,10 @@ ISR(ADC_vect)
 		}
 	} else {
 		if (bat_volt < VI_LVL) {
-			if (++lvt >= 3)
+			if (++lvt >= 3) {
 				FLAGS |= F_LVD;
+				FLAGS |= F_SYNC; /* flush now, as early as possible before an imminent brownout */
+			}
 		} else {
 			lvt = 0;
 		}
@@ -167,6 +265,12 @@ void beep (UINT len, BYTE cnt)
 	}
 }
 
+/* Number of consecutive epochs (nmea_epoch_complete() pulses) a new
+ * fix-acquired/lost state must hold before it's logged - coalesces the rapid
+ * flapping seen on marginal reception (e.g. 2D/3D/no-fix alternating for
+ * about a minute) into at most one or two log lines instead of one per flip. */
+#define FIX_HOLD_EPOCHS	5
+
 #define LOG_SIZE	300
 
 struct {
@@ -176,10 +280,14 @@ struct {
 
 void log_put(int c){
 	UINT bw;
-	uart1_put(c);
+	if (System.conf.uart_log_mode != UART_LOG_MODE_NMEA)
+		uart1_put(c);
 	logbuf.buf[logbuf.len++] = c;
 	if (logbuf.len >= LOG_SIZE && (FLAGS & F_FILEOPEN)) {
-		if(!f_write(&system_log, logbuf.buf, logbuf.len, &bw))
+		unsigned long int t0 = get_uptime_ms();
+		unsigned char err = f_write(&system_log, logbuf.buf, logbuf.len, &bw);
+		io_mark(t0);
+		if (!err)
 			logbuf.len = 0;
 	}
 	if (logbuf.len > LOG_SIZE) {
@@ -190,8 +298,7 @@ void log_put(int c){
 static
 void ioinit (void)
 {
-	wdt_enable(WDTO_4S);
-    MCUSR = 0;
+	wdt_enable(WDTO_4S); /* MCUSR was already cleared in wdt_disable_early() (.init3) */
 	POWER_ON_DDR |= POWER_ON;
 	PORTA |= POWER_ON;
 	
@@ -235,6 +342,8 @@ void ioinit (void)
 void close_files(unsigned char flush_logs) {
 	UINT bw;
 	if (FLAGS & F_FILEOPEN) {
+		if (flush_logs)
+			log_session_summary(1);
 		if (f_close(&gps_log)) {
 			System.status = STATUS_FILE_CLOSE_ERROR;
 			System.global_error |= ERROR_FILE_CLOSE;
@@ -257,52 +366,40 @@ void close_files(unsigned char flush_logs) {
 	disk_ioctl(0, CTRL_POWER, 0);
 }
 
-static inline void auto_unpause(void) {
-	if (!System.tracking_auto_paused)
-		return;
-	System.tracking_auto_paused = 0;
-	beep(50, 4);
-}
-
-static inline void auto_pause_activate(void) {
-	System.tracking_auto_paused = 1;
-	beep(50, 3);
-}
-
-static inline void auto_pause_process(void) {
-	if (System.tracking_paused || !get_flag(CONFFLAG_AUTO_PAUSE)) { /* remove auto-pause */
-		System.tracking_auto_paused = 0;
-		auto_pause.prev_distance = System.distance;
-		auto_pause.point_counter = 0;
-		return;
-	}
-	if (System.speed >= System.conf.auto_pause_speed) { /* immediately unpause when set speed is exceeded */
-		auto_pause.point_counter = 0;
-		auto_unpause();
-		return;
-	}
-	if (++auto_pause.point_counter < System.conf.auto_pause_time)
-		return;
-	auto_pause.point_counter = 0;
-	if ((System.distance - auto_pause.prev_distance)/100 > System.conf.auto_pause_dist) {
-		if (System.tracking_auto_paused)
-			auto_unpause(); /* unpause when distance exceeded */
-	} else {
-		if (!System.tracking_auto_paused)
-			auto_pause_activate(); /* pause otherwise */
-	}
-	auto_pause.prev_distance = System.distance;
-}
-
 void reset_counters(void) {
 	System.distance = 0;
 	System.elevation_gain = 0;
 	System.elevation_loss = 0;
+	System.elevation_gain_raw = 0;
+	System.elevation_loss_raw = 0;
+	System.alt_max = 0;
+	System.speed_accum_x100 = 0;
+	System.speed_sample_count = 0;
+	System.points_written = 0;
+	System.rejected_count = 0;
+	System.bat_volt_min = 99.0;
 	System.time_start = 0;
 	System.current_pause_start = 0;
 	System.pause_time = 0;
 }
 
+unsigned int get_avg_speed_x100(void) {
+	if (!System.speed_sample_count)
+		return 0;
+	return System.speed_accum_x100 / System.speed_sample_count;
+}
+
+/* Average speed the "old" way (distance / moving time), kept only so it can
+ * be logged alongside get_avg_speed_x100() in the session summary - the two
+ * should track each other, and a growing gap is worth noticing. */
+unsigned int get_dist_avg_speed_x100(void) {
+	unsigned int moving = get_logging_time();
+	if (!moving)
+		return 0;
+	/* (distance_cm * 3.6) / moving_s, kept as an all-integer x100 fixed-point value */
+	return (unsigned int)((unsigned long int)System.distance * 36UL / (10UL * moving));
+}
+
 time_t get_pause_time(void) {
 	time_t res = System.pause_time;
 	if (System.current_pause_start < System.time_start)
@@ -327,11 +424,14 @@ __flash const char __open_msg[] = "Open %s\r\n";
 int main (void)
 {
 	UINT bw, len;
-	static struct tm ct;
 	time_t tmp_utc, localtime;
 	FRESULT res;
 	unsigned char prev_status;
 	unsigned char already_logging = 0;
+	static unsigned char fix_ok_prev = 0;
+	static unsigned char fix_hold_counter = 0;
+	static unsigned char sbas_prev = 0xFF; /* 0xFF: not yet known, suppress the first (non-)transition */
+	static unsigned long int last_summary_uptime = 0;
 
 	ioinit();
 	xdev_out(log_put);
@@ -339,9 +439,12 @@ int main (void)
 	disp_init();
 	display_event(DISPLAY_EVENT_STARTUP);
 	settings_load();
-	
+	reset_counters();
+	log_boot_record();
+	request_immediate_sync(); /* flush the boot/config lines as soon as a log file opens, not after up to IVT_SYNC seconds */
+
 	menu_push(default_menu);
-	
+
 	for (;;) {
 		wdt_reset();
 		if (FLAGS & (F_POWEROFF | F_LVD)) {
@@ -360,7 +463,8 @@ int main (void)
 		}
 
 		display_event(DISPLAY_EVENT_INITIALIZED);
-		xprintf(PSTR("LOOP err=%u\r\n"), (unsigned int)System.status);
+		log_loop_status(System.status);
+		request_immediate_sync(); /* flush the just-logged loop/error status promptly */
 		utc = 0;
 		localtime = 0;
 		prev_status = System.status;
@@ -399,7 +503,9 @@ int main (void)
 		uart0_init();	/* Enable UART */
 		_delay_ms(300);	/* Delay */
 		System.gps_initialized = 0;
-		
+		gps_powered_on();
+
+
 		if (!already_logging && !get_flag(CONFFLAG_LOGGING_AFTER_BOOT)) {
 			tracking_pause(TRACKING_PAUSE_CMD_PAUSE, 0);
 		}
@@ -424,8 +530,6 @@ int main (void)
 			else
 				LEDW_OFF();
 
-			if (!(FLAGS & F_GPSOK))
-				xputs_P(PSTR("Waiting for GPS\r\n"));
 			len = get_line(Line, sizeof Line);	/* Receive a line from GPS receiver */
 			if (!len){
 				if (FLAGS & (F_LVD | F_POWEROFF))
@@ -436,30 +540,58 @@ int main (void)
 			tmp_utc = gps_parse(Line);
 			if (tmp_utc) {
 				localtime = tmp_utc + local_time_diff(tmp_utc) * 3600L;	/* Local time */
-				ct = *gmtime(&localtime);
-				if (timer_expired(system_log)) {
-					set_timer(system_log, 5000);
-					xprintf(PSTR("Time: %u.%02u.%04u %u:%02u:%02u\r\n"), ct.tm_mday, ct.tm_mon + 1, ct.tm_year+1900, ct.tm_hour, ct.tm_min, ct.tm_sec);
-					xprintf(PSTR("Bat volt: %.3f\r\n"), System.bat_volt);
-					if (System.temperature_ok)
-						xprintf(PSTR("Temp: %.2f\r\n"), System.temperature);
-					else
-						xputs_P(PSTR("Temperature unknown\r\n"));
-					if (System.sbas)
-						xputs_P(PSTR("SBAS (DGPS) active\r\n"));
-					else
-						xputs_P(PSTR("SBAS inactive\r\n"));
-					xputs_P(PSTR("Using GNSS: "));
-					xputs_P(gnss_names[System.conf.gnss_mode]);
-					xputs_P(PSTR("\r\n"));
-				}
 				LEDG_ON();
 				_delay_ms(2);
 				LEDG_OFF();
 			}
 
+			/* Battery low-water mark, for the session summary */
+			if (System.bat_volt > 0 && System.bat_volt < System.bat_volt_min)
+				System.bat_volt_min = System.bat_volt;
+
+			/* Fix acquired/lost events (a "fix" requires both a valid GPS status and enough
+			 * satellites). Gated on nmea_epoch_complete() so sats=/hdop= are never read from a
+			 * stale epoch, and debounced so a single bad epoch on marginal reception doesn't log
+			 * a FIX/FIX LOST pair by itself - the new state must hold for FIX_HOLD_EPOCHS epochs. */
+			if (nmea_epoch_complete()) {
+				unsigned char fix_ok_now = (FLAGS & F_GPSOK) && !System.sat_count_low;
+				if (fix_ok_now == fix_ok_prev) {
+					fix_hold_counter = 0;
+				} else if (++fix_hold_counter >= FIX_HOLD_EPOCHS) {
+					fix_hold_counter = 0;
+					fix_ok_prev = fix_ok_now;
+					if (fix_ok_now) {
+						log_fix_event();
+					} else {
+						log_fix_lost();
+						request_immediate_sync();
+					}
+				}
+			}
+
+			/* SBAS/DGPS on-off transitions */
+			if (System.sbas != sbas_prev) {
+				if (sbas_prev != 0xFF)
+					log_sbas_transition();
+				sbas_prev = System.sbas;
+			}
+
+			if (timer_expired(status_log)) {
+				set_timer(status_log, 60000);
+				log_status_line();
+			}
+
+			if (get_uptime_ms() - last_summary_uptime >= 3600000UL) {
+				last_summary_uptime = get_uptime_ms();
+				log_session_summary(0);
+			}
+
 			if (FLAGS & F_FILEOPEN) {
-				f_write(&gps_log, Line, len-1, &bw);
+				{
+					unsigned long int t0 = get_uptime_ms();
+					f_write(&gps_log, Line, len-1, &bw);
+					io_mark(t0);
+				}
 				if (bw != len-1) {
 					System.status = STATUS_FILE_WRITE_ERROR;
 					System.global_error |= ERROR_FILE_WRITE;
@@ -467,11 +599,24 @@ int main (void)
 				}
 				if (System.location_valid == LOC_VALID_NEW) { /* a new point */
 					gpx_process_point(&location, &gpx_file);
-					auto_pause_process();				
+					auto_pause_process();
 				}
 				wdt_reset();
 				if (FLAGS & F_SYNC) {
-					if (f_sync(&gps_log)) {
+					/* Sync all three log files together, on this same periodic
+					 * timer (IVT_SYNC), rather than after every write: an SD
+					 * f_sync() can stall for 100ms+ and must not happen on
+					 * every line, or it would stall the NMEA UART reception. */
+					unsigned long int t0 = get_uptime_ms();
+					unsigned char sync_err = f_sync(&gps_log);
+					io_mark(t0);
+					t0 = get_uptime_ms();
+					sync_err |= f_sync(&gpx_file);
+					io_mark(t0);
+					t0 = get_uptime_ms();
+					sync_err |= f_sync(&system_log);
+					io_mark(t0);
+					if (sync_err) {
 						System.status = STATUS_FILE_SYNC_ERROR;
 						System.global_error |= ERROR_FILE_SYNC;
 						break;
@@ -535,6 +680,8 @@ int main (void)
 				wdt_enable(WDTO_4S);
 				FLAGS |= F_FILEOPEN;
 				System.status = STATUS_OK;
+				set_timer(status_log, 60000);
+				last_summary_uptime = get_uptime_ms();
 				beep(50, System.tracking_paused?8:2);		/* Two beeps. Start logging. */
 				display_event(DISPLAY_EVENT_FILE_OPEN);
 				continue;

+ 54 - 8
soft/main.h

@@ -167,11 +167,16 @@ struct timers {
 	unsigned int owire;
 	unsigned int beep;
 	unsigned int recv_timeout;
-	unsigned int system_log;
+	unsigned int status_log;
 	unsigned int lcd;
 	unsigned int backlight;
 };
 
+/* System.fix_type values (from GSA) */
+#define FIX_TYPE_UNKNOWN	0
+#define FIX_TYPE_2D			2
+#define FIX_TYPE_3D			3
+
 struct system_s {
 	struct timers timers;
 	struct config_s conf;
@@ -183,12 +188,22 @@ struct system_s {
 	unsigned char keypress;
 	unsigned char working_mode;
 	unsigned long int distance; // cm
-	unsigned long int elevation_gain; // dm (decimeters, 0.1m resolution)
-	unsigned long int elevation_loss; // dm (decimeters, 0.1m resolution)
+	unsigned long int elevation_gain; // dm (decimeters, 0.1m resolution); filtered (dead-band+smoothed)
+	unsigned long int elevation_loss; // dm; filtered (dead-band+smoothed)
+	unsigned long int elevation_gain_raw; // dm; unfiltered, for log comparison only
+	unsigned long int elevation_loss_raw; // dm; unfiltered, for log comparison only
+	unsigned long int alt_max; // dm; highest smoothed altitude seen this session
+	unsigned long int speed_accum_x100; // sum of instantaneous NMEA speed samples (km/h * 100) while not paused
+	unsigned long int speed_sample_count; // number of samples in speed_accum_x100
+	unsigned long int points_written; // count of accepted/written track points this session
+	unsigned long int rejected_count; // count of rejected candidate points this session (unsigned long: a poor-fix session can reject far more than 65535 times)
+	unsigned int hdop_x100; // HDOP * 100, from GSA
+	unsigned char fix_type; // FIX_TYPE_* from GSA
 	unsigned char speed; // km/h
 	time_t time_start;
 	time_t current_pause_start;
 	time_t pause_time;
+	float bat_volt_min; // lowest battery voltage seen this session
 	unsigned temperature_ok:1;
 	unsigned satellites_used:5;
 	unsigned location_valid:2;
@@ -199,6 +214,9 @@ struct system_s {
 	unsigned tracking_auto_paused:1;
 	unsigned open_new_file:1;
 	unsigned sat_count_low:1;
+	unsigned epoch_gga_ok:1; // set once GGA has been parsed for the current epoch (since the last RMC)
+	unsigned epoch_gsa_ok:1; // set once GSA has been parsed for the current epoch (since the last RMC)
+	unsigned epoch_logged:1; // set once nmea_epoch_complete() has fired for the current epoch
 };
 
 struct location_s {
@@ -208,14 +226,13 @@ struct location_s {
 	time_t time;
 };
 
-struct auto_pause_s {
-	unsigned char point_counter;
-	unsigned long int prev_distance;
-};
-
 extern volatile struct system_s System;
 extern struct location_s location;
 extern time_t utc;
+extern volatile unsigned long int uptime_ms;
+extern volatile unsigned char reset_cause;
+
+void beep(unsigned int len, unsigned char cnt);
 
 /* Project includes - headers that depend on struct definitions above */
 #include "gpx.h"
@@ -224,6 +241,8 @@ extern time_t utc;
 #include "timec.h"
 #include "nmea.h"
 #include "menu.h"
+#include "syslog.h"
+#include "autopause.h"
 
 static inline void atomic_set_uint(volatile unsigned int *volatile data, unsigned int value) __attribute__((always_inline));
 static inline void atomic_set_uint(volatile unsigned int *volatile data, unsigned int value){
@@ -239,6 +258,15 @@ static inline unsigned int atomic_get_uint(volatile unsigned int *volatile data)
 	sei();
 	return ret;
 }
+static inline unsigned long int atomic_get_ulong(volatile unsigned long int *volatile data) __attribute__((always_inline));
+static inline unsigned long int atomic_get_ulong(volatile unsigned long int *volatile data){
+	unsigned long int ret;
+	cli();
+	ret = *data;
+	sei();
+	return ret;
+}
+#define get_uptime_ms() atomic_get_ulong(&uptime_ms)
 
 #define set_timer(timer,val) set_timer_counts(timer, ms(val))
 #define set_timer_counts(timer,val) atomic_set_uint(&System.timers.timer, val)
@@ -253,4 +281,22 @@ void sleep(void);
 void reset_counters(void);
 time_t get_pause_time(void);
 unsigned int get_logging_time(void);
+unsigned int get_avg_speed_x100(void);
+unsigned int get_dist_avg_speed_x100(void);
+
+/* SD card I/O timing: longest single f_write()/f_sync() call observed since
+ * the last time the status line reset the counter (io_get_and_reset_max). */
+void io_mark(unsigned long int start_ms);
+unsigned int io_get_and_reset_max(void);
+
+/* Request that the next main-loop pass sync all open log files immediately,
+ * instead of waiting for the periodic IVT_SYNC timer - used around events
+ * (boot, low battery, fix lost, errors, shutdown) whose log lines must not be
+ * lost to a brownout before the next periodic sync. */
+void request_immediate_sync(void);
+
+/* Stack headroom: bytes of the stack-painted region between the end of
+ * .bss/.noinit and RAMEND that are still untouched. Falls only, monotonically,
+ * as the deepest call/interrupt nesting grows over the session. */
+unsigned int get_stack_free_bytes(void);
 

+ 2 - 0
soft/menu.c

@@ -62,6 +62,7 @@ void settings_change_bool(struct menu_pos pos, unsigned char k) {
 		set_flag(index, val);
 		if (pos.changed != NULL)
 			pos.changed();
+		log_config();
 	}
 }
 
@@ -83,6 +84,7 @@ void settings_change_u8(struct menu_pos pos, unsigned char k) {
 		System.conf.conf_u8[index] = val;
 		if (pos.changed != NULL)
 			pos.changed();
+		log_config();
 	}
 }
 

+ 119 - 15
soft/nmea.c

@@ -41,13 +41,14 @@ uint16_t get_line (		/* 0:line incomplete or timed out, >0: Number of bytes rece
 			break;	/* EOL */
 		}
 		buff[i++] = c;
-		uart1_put(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) {
+	if (ret_len > 0 && System.conf.uart_log_mode != UART_LOG_MODE_SYSTEM) {
 		uart1_put('\r');
 		uart1_put('\n');
 	}
@@ -106,9 +107,58 @@ unsigned int gp_val(const char *db, unsigned char count) {
 	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 = System.tracking_paused || System.tracking_auto_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;
+
+	System.epoch_gga_ok = 0;
+	System.epoch_gsa_ok = 0;
+	System.epoch_logged = 0;
 
 	p = gp_col(str, 1);		/* Get h:m:s */
 	if (!p)
@@ -136,7 +186,17 @@ static time_t gp_rmc_parse(const char *str) {
 	}
 
 	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;
 }
 
@@ -146,6 +206,7 @@ static void gp_gga_parse(const char *str) {
 
 	p = gp_col(str, 7); /* satellites used */
 	System.satellites_used = atoi(p);
+	System.epoch_gga_ok = 1;
 
 	if (System.satellites_used >= System.conf.min_sats) {
 		System.sat_count_low = 0;
@@ -199,17 +260,49 @@ static void gp_gga_parse(const char *str) {
 	location.time = utc; /* parsed from RMC */
 }
 
-static void gp_vtg_parse(const char *str) {
+/* A fix is only trusted (for the speed average, and for the auto-pause
+ * speed-based unpause check below) 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,
+ * 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 */
+
+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 speed;
-	
-	p = gp_col(str, 9);
-	if (*p == 'N') /* Not valid */
-		return;
-	
-	p = gp_col(str, 7); /* speed in km/h */
-	xatof(&p, &speed);
-	System.speed = speed+0.5;
+	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);
+	}
+	System.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 (System.epoch_gga_ok && System.epoch_gsa_ok && !System.epoch_logged) {
+		System.epoch_logged = 1;
+		return 1;
+	}
+	return 0;
 }
 
 /*$PMTK355*31<CR><LF>
@@ -288,8 +381,8 @@ time_t gps_parse(const char *str) {	/* Get all required data from NMEA sentences
 		gp_gga_parse(str);
 		return 0;
 	}
-	if (!gp_comp(str, PSTR("GPVTG")) || !gp_comp(str, PSTR("GNVTG"))) {
-		gp_vtg_parse(str);
+	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"))) {
@@ -300,6 +393,14 @@ time_t gps_parse(const char *str) {	/* Get all required data from NMEA sentences
 		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;
 }
 
@@ -312,11 +413,14 @@ void gps_initialize(void) {
 	 * PMTK355: query gnss search mode (will fail if only GPS is supported)
 	 * PMTK353: set gnss search mode (GPS/Galileo/Glonass/Beidou)
 	 * PMTK313: enable SBAS
+	 * 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:

+ 3 - 0
soft/nmea.h

@@ -6,4 +6,7 @@ time_t gps_parse(const char *str);
 UINT get_line(char *buff, UINT sz_buf);
 void gps_initialize(void);
 void check_min_sat_limit(void);
+unsigned char gps_fix_trustworthy(void);
+unsigned char nmea_epoch_complete(void);
+unsigned char nmea_epoch_complete(void);
 

+ 31 - 0
soft/settings.c

@@ -10,6 +10,7 @@ const __flash unsigned char limits_max_u8[] = {
 	[CONF_U8_AUTO_PAUSE_DIST] = 100,
 	[CONF_U8_MIN_SATS] = 12,
 	[CONF_U8_AUTO_PAUSE_SPEED] = 20,
+	[CONF_U8_UART_LOG_MODE] = UART_LOG_MODE_SYSTEM,
 };
 
 const __flash unsigned char limits_min_u8[] = {
@@ -19,6 +20,7 @@ const __flash unsigned char limits_min_u8[] = {
 	[CONF_U8_AUTO_PAUSE_DIST] = 2,
 	[CONF_U8_MIN_SATS] = 4,
 	[CONF_U8_AUTO_PAUSE_SPEED] = 0,
+	[CONF_U8_UART_LOG_MODE] = UART_LOG_MODE_MIXED,
 };
 
 const __flash unsigned char defaults_u8[] = {
@@ -28,6 +30,7 @@ const __flash unsigned char defaults_u8[] = {
 	[CONF_U8_AUTO_PAUSE_DIST] = 10,
 	[CONF_U8_MIN_SATS] = 5,
 	[CONF_U8_AUTO_PAUSE_SPEED] = 3,
+	[CONF_U8_UART_LOG_MODE] = UART_LOG_MODE_MIXED,
 };
 
 unsigned char settings_load(void) { /* 0 - ok, 1 - error */
@@ -99,6 +102,20 @@ void display_current_gnss_mode(void) {
 	display_gnss_mode(System.conf.gnss_mode);
 }
 
+__flash const char uart_log_mode_mixed[] = "NMEA+system";
+__flash const char uart_log_mode_nmea[] = "Tylko NMEA";
+__flash const char uart_log_mode_system[] = "Tylko system";
+
+__flash const char *uart_log_mode_names[] = {
+	uart_log_mode_mixed,
+	uart_log_mode_nmea,
+	uart_log_mode_system,
+};
+
+void display_current_uart_log_mode(void) {
+	strcpy_P(disp.line2, uart_log_mode_names[System.conf.uart_log_mode]);
+}
+
 /* SETTINGS ITEMS */
 
 __flash const char _msg_disable_filters[] = "Nie filtruj";
@@ -114,6 +131,8 @@ __flash const char _msg_auto_pause_dist[] = "Autopauza odleg";
 __flash const char _msg_auto_pause_speed[] = "Autopau. predk.";
 __flash const char _msg_min_sats[] = "Minimum satelit";
 __flash const char _msg_reset_on_new_file[] = "Zeruj dystans";
+__flash const char _msg_uart_log_mode[] = "Log na UART";
+__flash const char _msg_verbose_log[] = "Log szczegolowy";
 
 __flash const struct menu_pos settings_menu_list[] = {
 	{
@@ -186,6 +205,18 @@ __flash const struct menu_pos settings_menu_list[] = {
 		.name = _msg_logging_after_boot,
 		.index = CONFFLAG_LOGGING_AFTER_BOOT,
 	},
+	{
+		.type = MENU_TYPE_SETTING_U8,
+		.display_type = MENU_DISPLAY_TYPE_NAME_FUNCTION,
+		.name = _msg_uart_log_mode,
+		.index = CONF_U8_UART_LOG_MODE,
+		.display = display_current_uart_log_mode,
+	},
+	{
+		.type = MENU_TYPE_SETTING_BOOL,
+		.name = _msg_verbose_log,
+		.index = CONFFLAG_VERBOSE_LOG,
+	},
 };
 
 __flash const struct menu_struct settings_menu = {

+ 12 - 2
soft/settings.h

@@ -9,8 +9,14 @@
 #define CONF_U8_AUTO_PAUSE_DIST	3
 #define CONF_U8_MIN_SATS	4
 #define CONF_U8_AUTO_PAUSE_SPEED	5
+#define CONF_U8_UART_LOG_MODE	6
 
-#define CONF_U8_LAST		5
+#define CONF_U8_LAST		6
+
+/* UART log mode values (CONF_U8_UART_LOG_MODE) */
+#define UART_LOG_MODE_MIXED	0	/* NMEA and system log interleaved (default) */
+#define UART_LOG_MODE_NMEA	1	/* NMEA only */
+#define UART_LOG_MODE_SYSTEM	2	/* system log only */
 
 /* flags list - max 31 */
 #define CONFFLAG_DISABLE_FILTERS	0
@@ -18,8 +24,9 @@
 #define CONFFLAG_LOGGING_AFTER_BOOT	2
 #define CONFFLAG_AUTO_PAUSE			3
 #define CONFFLAG_RESET_ON_NEW_FILE	4
+#define CONFFLAG_VERBOSE_LOG		5
 
-#define CONFFLAG_LAST				4
+#define CONFFLAG_LAST				5
 
 /* GNSS modes */
 #define GNSS_MODE_GPS_GLONASS_GALILEO	0
@@ -39,6 +46,7 @@ struct config_s {
 			unsigned char auto_pause_dist;	// 3
 			unsigned char min_sats;		// 4
 			unsigned char auto_pause_speed;	// 5
+			unsigned char uart_log_mode;	// 6
 		};
 	};
 	unsigned char flags[4];
@@ -47,6 +55,7 @@ struct config_s {
 extern const __flash unsigned char limits_max_u8[];
 extern const __flash unsigned char limits_min_u8[];
 extern __flash const char *gnss_names[];
+extern __flash const char *uart_log_mode_names[];
 extern __flash const struct menu_struct settings_menu;
 
 unsigned char settings_load(void); /* 0 - ok, 1 - error */
@@ -57,4 +66,5 @@ void settings_display_and_modify_u8(unsigned char mindex, unsigned char k);
 unsigned char get_flag(unsigned char index);
 void set_flag(unsigned char index, unsigned char val);
 void settings_bool_disp_default(unsigned char val);
+void display_current_uart_log_mode(void);
 

+ 256 - 0
soft/syslog.c

@@ -0,0 +1,256 @@
+#ifdef PC_BUILD
+#include "../gps-test-tool/main.h"
+#else
+#include "main.h"
+#endif
+
+#ifndef FW_VERSION
+#define FW_VERSION "unknown"
+#endif
+
+__flash const char reject_reason_kalman[] = "kalman";
+__flash const char reject_reason_distdiff[] = "distdiff";
+__flash const char reject_reason_mindist[] = "mindist";
+
+__flash const char *reject_reason_names[] = {
+	[REJECT_REASON_KALMAN] = reject_reason_kalman,
+	[REJECT_REASON_DISTDIFF] = reject_reason_distdiff,
+	[REJECT_REASON_MINDIST] = reject_reason_mindist,
+};
+
+__flash const char status_no_power[] = "no power";
+__flash const char status_no_disk[] = "no card";
+__flash const char status_no_gps[] = "waiting for GPS";
+__flash const char status_ok[] = "ok";
+__flash const char status_disk_error[] = "card mount failed";
+__flash const char status_file_write_error[] = "write failed";
+__flash const char status_file_sync_error[] = "sync failed";
+__flash const char status_file_close_error[] = "close failed";
+__flash const char status_file_open_error[] = "open failed";
+__flash const char status_unknown[] = "unknown";
+
+__flash const char *status_names[] = {
+	status_no_power,
+	status_no_disk,
+	status_no_gps,
+	status_ok,
+	status_disk_error,
+	status_file_write_error,
+	status_file_sync_error,
+	status_file_close_error,
+	status_file_open_error,
+};
+
+static struct {
+	unsigned char reason;
+	unsigned char count;
+	unsigned int hdop_x100;
+	unsigned char sats;
+	unsigned char fix_type;
+} reject_coalesce;
+
+static unsigned char fix_had_first;
+static unsigned long int gps_on_uptime;
+
+void log_prefix(void) {
+	if (utc)
+		xprintf(PSTR("%s up=%lu "), get_iso_time(utc, 0), get_uptime_ms());
+	else
+		xprintf(PSTR("NOTIME up=%lu "), get_uptime_ms());
+}
+
+void log_config(void) {
+	log_prefix();
+	xputs_P(PSTR("CFG filt="));
+	xputs_P(get_flag(CONFFLAG_DISABLE_FILTERS) ? PSTR("off") : PSTR("on"));
+	xprintf(PSTR(" skip=%u gate_sats=%u pause_t=%us pause_d=%um pause_v=%ukmh sbas_search="),
+		(unsigned int)System.conf.skip_points, (unsigned int)System.conf.min_sats,
+		(unsigned int)System.conf.auto_pause_time, (unsigned int)System.conf.auto_pause_dist,
+		(unsigned int)System.conf.auto_pause_speed);
+	xputs_P(get_flag(CONFFLAG_ENABLE_SBAS) ? PSTR("on") : PSTR("off"));
+	xputs_P(PSTR(" auto_pause="));
+	xputs_P(get_flag(CONFFLAG_AUTO_PAUSE) ? PSTR("on") : PSTR("off"));
+	xputs_P(PSTR(" gnss="));
+	xputs_P(gnss_names[System.conf.gnss_mode]);
+	xputs_P(PSTR(" uart_log="));
+	xputs_P(uart_log_mode_names[System.conf.uart_log_mode]);
+	xputs_P(PSTR(" verbose="));
+	xputs_P(get_flag(CONFFLAG_VERBOSE_LOG) ? PSTR("on") : PSTR("off"));
+	xputs_P(PSTR(" reset_new_file="));
+	xputs_P(get_flag(CONFFLAG_RESET_ON_NEW_FILE) ? PSTR("on") : PSTR("off"));
+	xputs_P(PSTR("\r\n"));
+}
+
+void log_boot_record(void) {
+	log_prefix();
+	xputs_P(PSTR("BOOT fw="));
+	xputs_P(PSTR(FW_VERSION));
+	xputs_P(PSTR(" reset="));
+	if (reset_cause & _BV(WDRF))
+		xputs_P(PSTR("WDT"));
+	else if (reset_cause & _BV(BORF))
+		xputs_P(PSTR("BOR"));
+	else if (reset_cause & _BV(EXTRF))
+		xputs_P(PSTR("EXT"));
+	else if (reset_cause & _BV(PORF))
+		xputs_P(PSTR("POR"));
+	else
+		xputs_P(PSTR("UNKNOWN"));
+	xputs_P(PSTR(" rx="));
+	xputs_P(gnss_names[System.conf.gnss_mode]);
+	xputs_P(PSTR("\r\n"));
+	log_config();
+}
+
+void log_loop_status(unsigned char status) {
+	log_prefix();
+	xprintf(PSTR("LOOP err=%u ("), (unsigned int)status);
+	if (status < sizeof(status_names)/sizeof(status_names[0]))
+		xputs_P(status_names[status]);
+	else
+		xputs_P(status_unknown);
+	xputs_P(PSTR(")\r\n"));
+}
+
+void gps_powered_on(void) {
+	gps_on_uptime = get_uptime_ms();
+	fix_had_first = 0;
+}
+
+void log_fix_event(void) {
+	log_prefix();
+	xprintf(PSTR("FIX %uD"), (unsigned int)System.fix_type);
+	xputs_P(System.sbas ? PSTR("/D") : PSTR(""));
+	if (!fix_had_first) {
+		unsigned long int ttff = get_uptime_ms() - gps_on_uptime;
+		xprintf(PSTR(" ttff=%lu.%01lus"), ttff/1000, (ttff%1000)/100);
+		fix_had_first = 1;
+	} else {
+		xputs_P(PSTR(" regained"));
+	}
+	xprintf(PSTR(" sats=%u hdop=%.2f\r\n"), (unsigned int)System.satellites_used, (double)System.hdop_x100/100.0);
+}
+
+void log_fix_lost(void) {
+	log_prefix();
+	xprintf(PSTR("FIX LOST sats=%u\r\n"), (unsigned int)System.satellites_used);
+}
+
+void log_sbas_transition(void) {
+	log_prefix();
+	xputs_P(PSTR("SBAS "));
+	xputs_P(System.sbas ? PSTR("on") : PSTR("off"));
+	xputs_P(PSTR("\r\n"));
+}
+
+void log_pause_event(unsigned char started) {
+	log_prefix();
+	xputs_P(started ? PSTR("PAUSE\r\n") : PSTR("RESUME\r\n"));
+}
+
+void log_reject_flush(void) {
+	if (!reject_coalesce.count)
+		return;
+	log_prefix();
+	xputs_P(PSTR("REJ reason="));
+	xputs_P(reject_reason_names[reject_coalesce.reason]);
+	xprintf(PSTR(" hdop=%.2f sats=%u fix=%uD"), (double)reject_coalesce.hdop_x100/100.0,
+		(unsigned int)reject_coalesce.sats, (unsigned int)reject_coalesce.fix_type);
+	if (reject_coalesce.count > 1)
+		xprintf(PSTR(" x%u"), (unsigned int)reject_coalesce.count);
+	xputs_P(PSTR("\r\n"));
+	reject_coalesce.count = 0;
+}
+
+void log_reject(unsigned char reason) {
+	System.rejected_count++;
+	if (reject_coalesce.count && reject_coalesce.reason == reason && reject_coalesce.count < 255) {
+		reject_coalesce.count++;
+		reject_coalesce.hdop_x100 = System.hdop_x100;
+		reject_coalesce.sats = System.satellites_used;
+		reject_coalesce.fix_type = System.fix_type;
+		return;
+	}
+	log_reject_flush();
+	reject_coalesce.reason = reason;
+	reject_coalesce.count = 1;
+	reject_coalesce.hdop_x100 = System.hdop_x100;
+	reject_coalesce.sats = System.satellites_used;
+	reject_coalesce.fix_type = System.fix_type;
+}
+
+void log_jump(unsigned char is_alt, float meters) {
+	log_prefix();
+	xputs_P(PSTR("JUMP "));
+	xputs_P(is_alt ? PSTR("alt=") : PSTR("pos="));
+	xprintf(PSTR("%.1fm\r\n"), (double)meters);
+}
+
+/* Voltage->percent breakpoints for a rough Li-ion/LiPo discharge curve,
+ * anchored so VI_LVL (the level that forces an immediate shutdown, see
+ * main.h) reads 0% - not just happens to clamp there. Precision isn't the
+ * goal (a straight line between points is a rough approximation), only
+ * getting the empty end right, since that's exactly when this number matters
+ * most. Voltages are millivolts to keep the table integer. */
+__flash const unsigned int batt_lut_mv[] = { 3100, 3400, 3550, 3700, 3850, 4000, 4100, 4200 };
+__flash const unsigned char batt_lut_pct[] = {   0,   10,   25,   45,   65,   80,   90,  100 };
+#define BATT_LUT_LEN	(sizeof(batt_lut_mv)/sizeof(batt_lut_mv[0]))
+
+static unsigned char battery_percent(float bat_volt) {
+	unsigned int mv = (unsigned int)(bat_volt*1000.0 + 0.5);
+	unsigned char i;
+
+	if (mv <= batt_lut_mv[0])
+		return batt_lut_pct[0];
+	if (mv >= batt_lut_mv[BATT_LUT_LEN-1])
+		return batt_lut_pct[BATT_LUT_LEN-1];
+	for (i = 1; i < BATT_LUT_LEN; i++) {
+		if (mv <= batt_lut_mv[i]) {
+			unsigned int span_mv = batt_lut_mv[i] - batt_lut_mv[i-1];
+			unsigned char span_pct = batt_lut_pct[i] - batt_lut_pct[i-1];
+			return batt_lut_pct[i-1] + (unsigned char)((unsigned long int)(mv - batt_lut_mv[i-1]) * span_pct / span_mv);
+		}
+	}
+	return batt_lut_pct[BATT_LUT_LEN-1]; /* unreachable */
+}
+
+void log_status_line(void) {
+	unsigned char batpct = battery_percent(System.bat_volt);
+
+	log_reject_flush();
+	log_prefix();
+	xprintf(PSTR("ST v=%.3f bat%%=%d"), (double)System.bat_volt, batpct);
+	if (System.temperature_ok)
+		xprintf(PSTR(" t=%.1f"), (double)System.temperature);
+	xprintf(PSTR(" sats=%u hdop=%.2f fix=%u"), (unsigned int)System.satellites_used,
+		(double)System.hdop_x100/100.0, (unsigned int)System.fix_type);
+	xputs_P(System.sbas ? PSTR("D") : PSTR(""));
+	xprintf(PSTR(" dist=%.2f gain=%.1f/raw%.1f loss=%.1f/raw%.1f wr=%lu rej=%lu"),
+		(double)System.distance/100.0, (double)System.elevation_gain/10.0, (double)System.elevation_gain_raw/10.0,
+		(double)System.elevation_loss/10.0, (double)System.elevation_loss_raw/10.0,
+		System.points_written, System.rejected_count);
+	xprintf(PSTR(" iomax=%ums stack=%u"), io_get_and_reset_max(), get_stack_free_bytes());
+	if (utc)
+		xprintf(PSTR(" loc=%s"), get_iso_time(utc, 1));
+	xputs_P(PSTR("\r\n"));
+}
+
+void log_session_summary(unsigned char final) {
+	unsigned int moving = get_logging_time();
+	unsigned long int stopped = get_pause_time();
+	unsigned int h, m;
+
+	log_reject_flush();
+	log_prefix();
+	xputs_P(final ? PSTR("SUMMARY final ") : PSTR("SUMMARY "));
+	xprintf(PSTR("dist=%.2f gain=%.1f loss=%.1f moving="), (double)System.distance/100.0,
+		(double)System.elevation_gain/10.0, (double)System.elevation_loss/10.0);
+	h = moving/3600; m = (moving/60)%60;
+	xprintf(PSTR("%uh%02um stopped="), h, m);
+	h = stopped/3600; m = (stopped/60)%60;
+	xprintf(PSTR("%uh%02um altmax=%.1f vmin=%.3f rej=%lu"), h, m,
+		(double)System.alt_max/10.0, (double)System.bat_volt_min, System.rejected_count);
+	/* Both averages should track each other; a growing gap is worth noticing. */
+	xprintf(PSTR(" avg_speed=%.2f avg_dist=%.2f\r\n"),
+		(double)get_avg_speed_x100()/100.0, (double)get_dist_avg_speed_x100()/100.0);
+}

+ 23 - 0
soft/syslog.h

@@ -0,0 +1,23 @@
+#pragma once
+
+/* Reasons a candidate point can be rejected before being written */
+#define REJECT_REASON_KALMAN	0	/* Kalman-filtered position error too large */
+#define REJECT_REASON_DISTDIFF	1	/* distance jump inconsistent with recent history */
+#define REJECT_REASON_MINDIST	2	/* too small a position change since last saved point */
+
+extern __flash const char *reject_reason_names[];
+
+void log_prefix(void);
+void log_boot_record(void);
+void log_config(void);
+void log_fix_event(void);
+void log_fix_lost(void);
+void log_sbas_transition(void);
+void log_pause_event(unsigned char started);
+void log_reject(unsigned char reason);
+void log_reject_flush(void);
+void log_jump(unsigned char is_alt, float meters);
+void log_status_line(void);
+void log_session_summary(unsigned char final);
+void gps_powered_on(void);
+void log_loop_status(unsigned char status);

+ 6 - 0
soft/working_modes.c

@@ -114,6 +114,12 @@ __flash const struct menu_pos default_menu_list[] = {
 		.display = disp_func_ele_sat,
 		.func = enter_main_menu,
 	},
+	{
+		.type = MENU_TYPE_FUNCTION,
+		.display_type = MENU_DISPLAY_TYPE_FUNCTION,
+		.display = disp_func_elevation_gain,
+		.func = enter_main_menu,
+	},
 	{
 		.type = MENU_TYPE_FUNCTION,
 		.display_type = MENU_DISPLAY_TYPE_FUNCTION,