
What is inside a Garmin swim FIT file
I decoded 1,234 of my own Garmin FIT files to see what a swim dashboard can show. Pool files carry every length, open water carries GPS and water temperature, and two big undocumented messages turned out not to be raw IMU data.
We are building a swim dashboard for Vimo Swim, and before drawing a single chart I wanted to know what the data can actually carry. Garmin's web API sends a summary and a per-second trace. The FIT file the watch writes has much more, so I took my local archive of 1,234 FIT files and decoded every message and field with fitdecode.
The archive holds 785 pool swims and 179 open-water swims from 2022 to 2026, plus runs, rides and hikes.
Pool and open water are different files
A pool swim has no GPS, so the one-second records carry heart rate and nothing else. The real data sits in the length messages, about 32 per swim: time, average speed, stroke count, stroke rate, stroke type and whether the length was active or rest.
An open-water swim is the other way round. It has no lengths, but every record has a GPS position, distance, speed, stroke rate and the water temperature.
flowchart LR
FIT[FIT file] --> POOL[Pool swim]
FIT --> OW[Open-water swim]
FIT --> BOTH["session, lap, time_in_zone<br/>training effect, feel and RPE"]
POOL --> LEN["length x ~32<br/>time, speed, strokes,<br/>stroke rate, stroke type, active/idle"]
POOL --> REC1["record x ~1100<br/>heart rate only"]
OW --> REC2["record x ~1750<br/>GPS, distance, speed,<br/>stroke rate, temperature, HR"]
Across all my active pool lengths, the stroke types look like this:
| Stroke | Lengths | Share |
|---|---|---|
| Freestyle | 21,818 | 95.4% |
| Breaststroke | 751 | 3.3% |
| Drill | 154 | 0.7% |
| Backstroke | 103 | 0.5% |
| Butterfly | 42 | 0.2% |
The watch also logged 1,961 rest lengths, which is enough to show rest time per swim.
The file already knows your heart-rate zones
I expected to compute HR zones myself. I did not have to. Every swim has a time_in_zone message with the zone limits the swimmer set on the watch and the seconds spent in each zone, for the session and for every lap. So our zones use the same limits as the watch, instead of a guess from age.
To check the whole idea, I rebuilt one pool swim from the file alone: 1,000 m, 1:35 per 100 m, best length 1:19, 355 strokes, SWOLF 33, 23 strokes per minute, time in each zone, training effect 2.9 aerobic and 1.8 anaerobic, plus the feel and effort I entered on the watch. That covers everything the Overview, Stats and Charts tabs of Garmin Connect show for a swim.
Garmin writes avg_stroke_distance as 0 for pool swims. Distance per stroke is still easy: pool length divided by strokes per length. On that swim it was 2.84 m.
Things Garmin Connect does not show
Once the lengths are there, the extras are plain maths, no AI needed. Here is what the same swim gives, plus two more from open water:
First half 1:39 per 100 m, second half 1:30. Garmin shows the average, not the shape.
Length times varied by 7.1%. One number that tells a coach how even the set was.
15:45 of swimming inside 35:56 on the clock. That says more about the session than the pace does.
Open water only. Every record has a GPS position and a speed, so the map can show where you slowed down.
Open water only. Skip the first minutes: the sensor starts warm from the wrist and needs time to settle.
Two big unknown messages, and no IMU
Two undocumented messages stood out because they are big. unknown_233 is about one 4-byte packet per second. It shows up in every activity recorded on the watch, running, hiking and kayaking included, and it is missing only from road rides recorded on a bike computer. The top bits count up like a sequence number and nothing in it follows heart rate, so it is a watch-wide log, not swim data.
unknown_125 appears only in swims: about 29 rows per minute in the pool and 5 in open water, almost all of them while swimming. Its row count follows length time and stroke count. My guess is internal output of Garmin's stroke and turn detection, which works from the accelerometer, but there is no published format for it.
Raw accelerometer data is not in the file. If we want it, our own Connect IQ data field has to record it.
Clean the data before you trust a record. Eight of my old swims had impossible lengths, one showed 100 m in one second. A simple rule, a length only counts between 0:50 and 6:40 per 100 m, removed all of them.
What it took
One evening, five small Python scripts and an archive I already had. The lesson for me: decode the real files before designing the screens. I had planned to compute HR zones and to chase IMU data. The first was already in the file, the second is not there at all, and per-length data turned out to be the thing that makes the dashboard possible. Next time I will start with the inventory, not end with it.