Skip to content

Fix FIT epoch timezone bug and preserve sensor data for GPS-less activities - #4

Merged
trasch merged 1 commit into
mainfrom
fix/fit-epoch-and-indoor-activities
Aug 9, 2026
Merged

trasch merged 1 commit into
mainfrom
fix/fit-epoch-and-indoor-activities

Conversation

@trasch

@trasch trasch commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Two correctness bugs surfaced while auditing real-world FIT files.

1. FIT epoch was timezone-dependent (garmin epoch issue)

FIT.epoch was built from DateComponents via a Calendar(identifier: .gregorian) with no time zone pinned, so it resolved to midnight in the user's local zone instead of UTC. On a PST machine the epoch became 1989-12-31 08:00:00Z, eight hours ahead of the spec, shifting every decoded FIT timestamp by the local UTC offset.

Fixed by pinning FIT.epoch = Date(timeIntervalSince1970: 631065600) (exactly 1989-12-31T00:00:00Z per the Garmin FIT profile). FITEpochTests locks this down and verifies dateFromTimestamp against an independently computed UTC date.

2. Indoor/trainer activities lost all sensor data

Activities whose record messages carry heart rate, cadence, power, speed, altitude and timestamps but no GPS positions (e.g. indoor bike trainer workouts) produced an empty FeatureCollection. FITCoder.convertToGeoJSON dropped every record lacking position_lat/position_long, discarding all per-point sensor data.

FITCoder now keeps every record's sensor arrays indexed across all records, and emits an empty MultiLineString as the track geometry when no record has GPS. The encoder was updated to write sensor-only records (with at least one lap) so indoor activities round-trip cleanly. IndoorActivityTests verifies a GPS-less FIT preserves HR/cadence/power/timestamps through read → write → read.

…vities

- FITTypes: pin FIT.epoch to timeIntervalSince1970(631065600). The epoch used
  to be built from DateComponents via a Calendar with no pinned time zone,
  so it resolved to midnight in the user's local zone instead of UTC. Under
  PST the epoch became 1989-12-31 08:00:00Z, shifting every decoded timestamp
  by the local UTC offset and producing bogus activity dates.
- FITCoder: preserve per-record sensor data (heart rate, cadence, power,
  speed, altitude, timestamps) for indoor/trainer activities that contain no
  GPS positions. Records without position_lat/position_long used to be
  dropped entirely, yielding an empty FeatureCollection. The track is now
  emitted as an empty MultiLineString with the sensor arrays intact, and
  the encoder writes such sensor-only records with at least one lap so they
  round-trip cleanly.
- FITDecoder: wrap array-element Data slice in Data(...) so decodeSingle
  receives a standalone Data value.
- Tests: add FITEpochTests (epoch is exactly 1989-12-31T00:00:00Z) and
  IndoorActivityTests (GPS-less FIT round-trips with HR/cadence/power/
  timestamps preserved).
@trasch trasch self-assigned this Aug 9, 2026
@trasch trasch added the bug Something isn't working label Aug 9, 2026
@trasch
trasch merged commit 72570fe into main Aug 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant