autopause.c 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. /*
  2. * Auto-pause detection: shared between the firmware and gps-test-tool (see
  3. * autopause_wrapper.c), since it directly gates what ends up in
  4. * System.distance/elevation_gain/elevation_loss (excluded while paused) and
  5. * is a major part of what those tools' output actually looks like.
  6. */
  7. #ifdef PC_BUILD
  8. #include "../gps-test-tool/main.h"
  9. #else
  10. #include "main.h"
  11. #endif
  12. struct auto_pause_s auto_pause;
  13. /* Resuming from a period of auto-pause because of apparent distance needs
  14. * more apparent movement than triggering the pause did in the first place:
  15. * position noise while genuinely stationary (multipath, poor sky view) can
  16. * still occasionally clear the plain auto_pause_dist threshold over enough
  17. * auto_pause_time windows, and unlike a false pause (which just costs one
  18. * quiet window before the speed-based check or the next window catches the
  19. * resume), a false resume pollutes the moving/stopped split and the
  20. * odometer for as long as it isn't caught. */
  21. #define AUTO_PAUSE_RESUME_MIN_DIST_M 15 /* resume distance-from-anchor floor, regardless of HDOP */
  22. #define AUTO_PAUSE_RESUME_HDOP_MULT 5 /* ...and scale up from there by this many meters per HDOP unit -
  23. * a standard rule-of-thumb for GPS position error (HDOP x a typical
  24. * few-meter UERE), so a poor fix needs to move further to count */
  25. #define AUTO_PAUSE_RESUME_HISTORY_LEN 7 /* the resume condition must hold for most (see MIN_COUNT below) of
  26. * the last this-many samples, not just instantaneously - the same
  27. * persistence idea used for the speed cap, so a few seconds of
  28. * genuine drift-free movement are needed, not one lucky sample */
  29. #define AUTO_PAUSE_RESUME_HISTORY_MASK ((1U << AUTO_PAUSE_RESUME_HISTORY_LEN) - 1)
  30. #define AUTO_PAUSE_RESUME_MIN_COUNT 5
  31. /* Raw NMEA-reported ground speed (System.speed_x10) is single-epoch Doppler
  32. * output with no smoothing - real recorded hikes show it read a few km/h for
  33. * several consecutive seconds even while the receiver is stationary and
  34. * HDOP/sat count are excellent (multipath/receiver noise, not position
  35. * error, and correlated over several seconds rather than independent from
  36. * sample to sample). A strict "N consecutive samples over threshold"
  37. * debounce is exactly the shape most exposed to this - and even a plain
  38. * majority-of-a-window vote on the raw value isn't enough on its own, since
  39. * this noise is persistent enough to occasionally dominate a short window
  40. * outright. Smooth the speed first (exponential smoothing, same idea as
  41. * elevation_process()'s altitude smoothing) to knock down the bulk of the
  42. * noise, *then* still require the smoothed value to clear the threshold for
  43. * most of a window before resuming - so a single lingering noisy patch
  44. * cannot itself resume a genuinely stationary logger, but sustained real
  45. * movement (which pushes the smoothed value well past the threshold, not
  46. * just brushing it) still resumes within several seconds. */
  47. #define SPEED_SMOOTH_ALPHA 0.25 /* exponential smoothing factor per sample (~1s apart) */
  48. #define SPEED_RESUME_HISTORY_LEN 7 /* same window length as the other resume-history checks */
  49. #define SPEED_RESUME_HISTORY_MASK ((1U << SPEED_RESUME_HISTORY_LEN) - 1)
  50. #define SPEED_RESUME_MIN_COUNT 6 /* stricter majority than distance/heading: speed alone has no
  51. * corroborating signal (no distance-from-anchor or heading check
  52. * backs it up), so it needs to be almost the whole window */
  53. /* Distance-from-anchor alone can't tell a real departure from a smooth
  54. * position drift with no real movement behind it: a satellite geometry
  55. * change (a few satellites dropping out/back in) can walk the computed fix
  56. * steadily in one direction for tens of seconds while the receiver doesn't
  57. * move at all - the exact "persist over several samples" shape the resume
  58. * check above is trying to accept. Real walking keeps a fairly stable
  59. * step-to-step bearing; a geometry-driven drift has no reason to, so require
  60. * the bearing between consecutive paused samples to also stay consistent
  61. * before counting a sample as a real resume signal. Skipped below
  62. * HEADING_STEP_MIN_DIST_M: bearing is meaningless noise over a sub-meter step
  63. * (the two fixes are within GPS jitter of each other), and would otherwise
  64. * make genuinely stationary jitter look "inconsistent" for the wrong reason. */
  65. #define HEADING_STEP_MIN_DIST_M 3 /* ignore bearing between two samples closer than this */
  66. #define HEADING_CONSISTENT_MAX_DEG 60 /* max angle from the previous step's bearing to still agree */
  67. #define HEADING_HISTORY_LEN 7 /* same window length as the distance resume history above */
  68. #define HEADING_HISTORY_MASK ((1U << HEADING_HISTORY_LEN) - 1)
  69. #define HEADING_MIN_COUNT 5 /* same majority-of-window requirement as the distance resume history */
  70. static void auto_unpause(void) {
  71. if (!System.tracking_auto_paused)
  72. return;
  73. System.tracking_auto_paused = 0;
  74. if (!System.tracking_paused)
  75. pause_time_end();
  76. log_pause_event(0);
  77. beep(50, 4);
  78. }
  79. static void auto_pause_activate(void) {
  80. System.tracking_auto_paused = 1;
  81. pause_time_start();
  82. /* Otherwise, speed-history progress from just before this decision could
  83. * carry over and complete on the very next few samples, instantly
  84. * flip-flopping straight back out of the pause this just activated. */
  85. auto_pause.speed_high_history = 0;
  86. auto_pause.smoothed_speed_valid = 0;
  87. /* Anchor the resume-by-distance check on where the pause actually
  88. * started, then let it drift towards the running average of positions
  89. * seen while paused (steadier than any single fix - see
  90. * auto_pause_process()). */
  91. auto_pause.pause_anchor = location;
  92. auto_pause.anchor_sample_count = 1;
  93. auto_pause.resume_high_history = 0;
  94. auto_pause.prev_location_valid = 0;
  95. auto_pause.prev_bearing_valid = 0;
  96. auto_pause.heading_consistent_history = 0;
  97. log_pause_event(1);
  98. beep(50, 3);
  99. }
  100. /* Call once, when tracking_pause() (working_modes.c) transitions
  101. * tracking_paused from set to clear (a manual resume): auto_pause_process()
  102. * only clears tracking_auto_paused while tracking_paused is *true* (to force
  103. * an active auto-pause off the instant a manual pause starts), so without
  104. * this, a manual resume while auto-paused would leave tracking_auto_paused
  105. * set - tracking_is_paused() (tracking_paused || tracking_auto_paused) would
  106. * still report paused, silently keeping the logger paused until the
  107. * speed/distance-based auto-resume condition happened to also be met, well
  108. * after the user asked to resume. No log/beep here (unlike auto_unpause()) -
  109. * the manual resume already has its own distinct feedback. */
  110. void auto_pause_clear_manual_resume(void) {
  111. System.tracking_auto_paused = 0;
  112. auto_pause.prev_distance = System.distance;
  113. auto_pause.point_counter = 0;
  114. auto_pause.speed_high_history = 0;
  115. auto_pause.smoothed_speed_valid = 0;
  116. }
  117. /* Call once, whenever fix_gap_detected() reports a gap on the point about to
  118. * be passed to auto_pause_process(). Without this, point_counter keeps
  119. * counting straight through the gap with no idea how long it was or whether
  120. * the device kept moving during it, so reacquiring a fix that was lost while
  121. * genuinely moving could satisfy the stationary-time threshold within
  122. * moments, using mostly pre-gap progress - the opposite of what the
  123. * threshold is meant to require (persistent time actually stationary). */
  124. void auto_pause_reset_gap(void) {
  125. auto_pause.point_counter = 0;
  126. auto_pause.speed_high_history = 0;
  127. auto_pause.smoothed_speed_valid = 0;
  128. auto_pause.prev_distance = System.distance;
  129. if (System.tracking_auto_paused) {
  130. /* Same reasoning as auto_pause_activate()'s anchor init: re-anchor on
  131. * the fix regained after the gap, not wherever the device was when
  132. * the fix was lost - it may have moved (or been carried) an unknown
  133. * distance in between, which resume-by-distance must not read as an
  134. * instant departure from a now-stale anchor. */
  135. auto_pause.pause_anchor = location;
  136. auto_pause.anchor_sample_count = 1;
  137. auto_pause.resume_high_history = 0;
  138. auto_pause.prev_location_valid = 0;
  139. auto_pause.prev_bearing_valid = 0;
  140. auto_pause.heading_consistent_history = 0;
  141. }
  142. }
  143. void auto_pause_process(void) {
  144. if (System.tracking_paused || !get_flag(CONFFLAG_AUTO_PAUSE)) { /* remove auto-pause */
  145. System.tracking_auto_paused = 0;
  146. auto_pause.prev_distance = System.distance;
  147. auto_pause.point_counter = 0;
  148. auto_pause.speed_high_history = 0;
  149. auto_pause.smoothed_speed_valid = 0;
  150. return;
  151. }
  152. /* A poor/invalid fix must not be read as "moving": a single noisy sample
  153. * (e.g. a low-satellite, high-HDOP fix briefly reporting tens of km/h,
  154. * as seen on a real hike where reception dropped for a few seconds)
  155. * could otherwise satisfy the persistence check below on its own and
  156. * resume a logger that is actually sitting still. Such a sample is
  157. * simply ignored - it shifts nothing into the history below - so it
  158. * can't itself trigger a resume, but also can't erase real progress
  159. * made by genuine movement. */
  160. if (gps_fix_trustworthy()) {
  161. unsigned char speed_high;
  162. if (!auto_pause.smoothed_speed_valid) {
  163. auto_pause.smoothed_speed_x10 = (float)System.speed_x10;
  164. auto_pause.smoothed_speed_valid = 1;
  165. } else {
  166. auto_pause.smoothed_speed_x10 += SPEED_SMOOTH_ALPHA * ((float)System.speed_x10 - auto_pause.smoothed_speed_x10);
  167. }
  168. speed_high = auto_pause.smoothed_speed_x10 >= (float)((unsigned int)System.conf.auto_pause_speed*10);
  169. auto_pause.speed_high_history = (auto_pause.speed_high_history << 1) | speed_high;
  170. if (__builtin_popcount(auto_pause.speed_high_history & SPEED_RESUME_HISTORY_MASK) >= SPEED_RESUME_MIN_COUNT) {
  171. auto_pause.point_counter = 0;
  172. auto_pause.speed_high_history = 0;
  173. auto_unpause();
  174. return;
  175. }
  176. }
  177. /* Resume-by-distance: compared against an anchor updated to the running
  178. * average position seen while paused (steadier than any single fix),
  179. * scaled by HDOP (a poor fix needs to move further to count), and
  180. * required to persist for most of a short window rather than firing on
  181. * one sample - stationary GPS jitter (well under a meter per second) can
  182. * otherwise accumulate across enough net-movement windows to look like a
  183. * real departure. Evaluated every accepted point, not gated on the
  184. * auto_pause_time window below (that window is for the pause trigger
  185. * only), so the persistence history has one sample per point. Also
  186. * requires a consistent step-to-step bearing (see HEADING_* above) - a
  187. * satellite geometry change can drift the fix in a way that also
  188. * persists across this many samples, but has no reason to keep the same
  189. * heading step to step the way real walking does. */
  190. if (System.tracking_auto_paused && gps_fix_trustworthy()) {
  191. float resume_threshold_m = AUTO_PAUSE_RESUME_MIN_DIST_M;
  192. float hdop_threshold_m = (System.hdop_x100/100.0f) * AUTO_PAUSE_RESUME_HDOP_MULT;
  193. float dist_from_anchor_m;
  194. unsigned char resumed_this_sample;
  195. if (hdop_threshold_m > resume_threshold_m)
  196. resume_threshold_m = hdop_threshold_m;
  197. /* Capped well below the 16-bit wraparound point: once the running
  198. * average has this many samples behind it, one more changes it
  199. * negligibly anyway, and letting the count wrap to 0 would make the
  200. * very next sample overwrite the whole average with itself. */
  201. if (auto_pause.anchor_sample_count < 60000)
  202. auto_pause.anchor_sample_count++;
  203. auto_pause.pause_anchor.lat += (location.lat - auto_pause.pause_anchor.lat) / auto_pause.anchor_sample_count;
  204. auto_pause.pause_anchor.lon += (location.lon - auto_pause.pause_anchor.lon) / auto_pause.anchor_sample_count;
  205. dist_from_anchor_m = distance(&location, &auto_pause.pause_anchor);
  206. resumed_this_sample = dist_from_anchor_m > resume_threshold_m;
  207. auto_pause.resume_high_history = (auto_pause.resume_high_history << 1) | resumed_this_sample;
  208. if (auto_pause.prev_location_valid) {
  209. float step_dist_m = distance(&auto_pause.prev_location, &location);
  210. if (step_dist_m >= HEADING_STEP_MIN_DIST_M) {
  211. float this_bearing = bearing(&auto_pause.prev_location, &location);
  212. if (auto_pause.prev_bearing_valid) {
  213. float diff = fabs(this_bearing - auto_pause.prev_bearing);
  214. unsigned char heading_ok;
  215. if (diff > 180.0)
  216. diff = 360.0 - diff;
  217. heading_ok = diff <= HEADING_CONSISTENT_MAX_DEG;
  218. auto_pause.heading_consistent_history =
  219. (auto_pause.heading_consistent_history << 1) | heading_ok;
  220. }
  221. auto_pause.prev_bearing = this_bearing;
  222. auto_pause.prev_bearing_valid = 1;
  223. auto_pause.prev_location = location;
  224. }
  225. /* else: too small a step to trust its bearing - leave prev_bearing
  226. * and heading_consistent_history untouched, neither confirming nor
  227. * breaking the run of consistent headings. */
  228. }
  229. if (!auto_pause.prev_location_valid) {
  230. auto_pause.prev_location = location;
  231. auto_pause.prev_location_valid = 1;
  232. }
  233. if (__builtin_popcount(auto_pause.resume_high_history & AUTO_PAUSE_RESUME_HISTORY_MASK) >= AUTO_PAUSE_RESUME_MIN_COUNT
  234. && __builtin_popcount(auto_pause.heading_consistent_history & HEADING_HISTORY_MASK) >= HEADING_MIN_COUNT) {
  235. auto_pause.point_counter = 0;
  236. auto_unpause();
  237. return;
  238. }
  239. }
  240. if (auto_pause.point_counter == System.conf.auto_pause_time - System.conf.auto_pause_time/4)
  241. auto_pause.quarter_distance = System.distance;
  242. if (++auto_pause.point_counter < System.conf.auto_pause_time)
  243. return;
  244. auto_pause.point_counter = 0;
  245. if (!System.tracking_auto_paused && gps_fix_trustworthy()) {
  246. /* Net displacement over this window can be small even while genuinely
  247. * moving throughout it - a winding path folding back on itself, or a
  248. * short out-and-back - and pausing on that alone, only to have the
  249. * very next sample or two immediately satisfy the speed-based resume
  250. * above, is itself a spurious (if brief) pause. If the logger is
  251. * already moving fast enough to resume on its own right now, don't
  252. * pause it in the first place.
  253. *
  254. * Gated on gps_fix_trustworthy(), matching the resume checks above:
  255. * System.distance itself must stay ungated (it's the odometer), but
  256. * a run of untrustworthy fixes must not be allowed to decide a pause
  257. * transition either way here - it's simply skipped for this window. */
  258. if ((System.distance - auto_pause.prev_distance)/100 <= System.conf.auto_pause_dist
  259. && (System.distance - auto_pause.quarter_distance)/100 <= System.conf.auto_pause_dist/4
  260. && System.speed_x10 < (unsigned int)System.conf.auto_pause_speed*10)
  261. auto_pause_activate(); /* pause: not enough net movement, and not currently moving fast */
  262. }
  263. auto_pause.prev_distance = System.distance;
  264. }