45 Commits 132898def8 ... 9242c6c9eb

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

+ 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,