AI-assisted, human-edited
I used AI as a sparring partner for this post: to explore the topic, check my thinking and draft some sections. I edited everything, and the ideas, opinions and final words are mine. I never publish posts that are fully AI-generated.
Exploring Garmin watch data over USB (MTP)
TL;DR: Over USB, a Garmin watch exposes its storage as folders of FIT files. On a Fenix 6 these hold about 13 months of full activities, summary-only activities going back to 2022, about 24 hours of all-day health data (heart rate, stress, respiration, sleep stages, overnight HRV), 100 nights of sleep scores, a 28-day HRV trend, personal records and lifetime totals. 66 of the 126 message types in these files are not in the public FIT profile.
This post maps the data that a Garmin watch exposes over USB: which folders exist, what’s in them, how far back they go and how to read them. It’s meant as a reference for developers who want to read data straight from the device instead of through an API. In the previous post I compared the activity file on the watch with the copies Garmin hands out. This post covers everything else on the watch.
Scope: one Garmin Fenix 6 Sapphire, firmware 28.02. Other models and firmware versions will differ in the details. The folder layout looks similar on Garmin’s other recent watches, but I haven’t checked that.
Disclaimer: this was a technical exercise to see which data I could read from my own Garmin device over USB. It is not meant as a workaround for the undocumented and potentially proprietary data that Garmin removes from FIT files delivered through the Garmin Connect API.
Access over MTP
Older Garmin devices mount as a USB drive. Recent watches use MTP (Media Transfer Protocol), the same protocol Android phones use. MTP is not a file system: the client asks the device for folder listings and then downloads files one at a time.
- One client at a time. The watch accepts only one MTP session. On macOS, Image Capture (
ptpcamerad), Garmin Express and Android File Transfer claim it when it’s plugged in. On Linux, GNOME’s gvfs and KDE’s kiod do the same. If opening a session times out, another program is usually holding the watch. - Linux needs a udev rule before a normal user can open the device. Match on Garmin’s vendor ID (
091e) only, because the product ID changes while the watch connects. - Windows binds the watch to its own MTP driver (WPD). Native tools work through it, but WebUSB can’t open the watch on Windows. On macOS and Linux, Chrome can read the watch over WebUSB without extra software.
- Listings have no timestamps. At least with mtp-rs, a listing gives names, sizes and parent folders, with no modification times. Activity files are named after their local start time (
2026-10-18-14-02-11.fit), so you can sync them incrementally by name. Health files have encoded names (MAJC4356.FIT), so compare sizes or hashes for those instead. - Read, don’t write. Garmin documents a few writes, like copying a GPX course into
GARMIN/NewFiles/so the watch imports it. The effect of any other write is unknown outside Garmin, and reading data never requires writing.
Tools: mtp-rs for MTP; pyroparse for parsing, with fitdecode for the files pyroparse can’t read yet.
Folder overview
All data lives under /GARMIN. Besides the folders below, the watch holds maps (about 11 GB), language files and Connect IQ app storage.
| Folder | Contents | Retention |
|---|---|---|
Activity/ | Full activity recordings | ~13 months |
SUMMARY/ | Older activities, summaries only | back to 2022 |
Monitor/ | All-day HR, steps, stress, respiration | ~24 hours |
Sleep/ | Sleep stages and sleep score | last night |
HRVStatus/ | Overnight HRV | last night |
Metrics/, Debug/MetricsBackup/ | VO2max and other daily metrics | ~1–5 days |
FbtBackup/ | Daily Firstbeat history: sleep score, bedtimes, HRV trend | 100 nights |
FbtPtdBackup/ | Firstbeat metrics per activity | last 150 activities |
Totals/, Records/, Goals/ | Lifetime totals, personal records, goals | lifetime |
Settings/, Sports/ | User profile, zones, sport profiles | current |
Courses/, Workouts/, Schedule/ | Routes and planned workouts | — |
Nearly everything is FIT.
Activities (Activity/, SUMMARY/)
Activity/ held 191 files going back 13 months. These files are byte-identical to Garmin Connect’s “Export Original”. Besides the per-second record data (heart rate, power, cadence, GPS, altitude, temperature), they contain:
| Data | Where | Notes |
|---|---|---|
| Beat-to-beat RR intervals | hrv | Only with a chest strap that has HRV logging enabled |
| Connect IQ developer fields | developer data | For example Stryd form power, Concept2 drag factor |
| Wrist (optical) HR | record field 136, undocumented | Recorded even when a chest strap is the HR source |
| Strap HR | record field 144, undocumented | Copy of heart_rate |
| Training effect, recovery time | session; message 140, undocumented | Message 140 stores training effect ×10 |
| Watch battery | message 104, undocumented | Voltage, percentage and temperature during the activity |
The undocumented fields are the ones that Garmin’s API removes (previous post).
SUMMARY/ held 100 older activities going back to February 2022, for which the full files are gone. They have the session, laps and time in zone, but no time series. pyroparse fails on most of these files (yes, I will work on supporting them). fitdecode reads all of them with CRC checks disabled and errors ignored.
Health data (Monitor/, Sleep/, HRVStatus/, Metrics/)
The watch keeps only about 24 hours of detailed health data. The files appear to be removed once the watch has synced with Garmin Connect, so building a history from USB requires a sync at least once a day.
| Metric | File → message | Resolution |
|---|---|---|
| All-day heart rate | Monitor/ → monitoring.heart_rate | ~1/min |
| Steps, distance, calories | Monitor/ → monitoring | cumulative per day |
| Stress | Monitor/ → stress_level | 1/min |
| Respiration | Monitor/ → respiration_rate | 1/min |
| Resting HR (today, 7-day average) | Monitor/ → monitoring_hr_data | per day |
| Sleep stages | Sleep/ → sleep_level | per stage change |
| Sleep score and sub-scores | Sleep/ → sleep_assessment | per night |
| Overnight HRV | HRVStatus/ → hrv_value, hrv_status_summary | 5 min, per night |
| VO2max | Debug/MetricsBackup/ → max_met_data | per update |
All of these are documented FIT messages. Parsing notes:
- Timestamps: most
monitoringmessages only carry a 16-bittimestamp_16. Rebuild the full time from the most recent full timestamp before it. - Missing values: negative stress and respiration values mean “not measurable”. A heart rate of 0 means no reading.
- Sleep stages are change events: a stage lasts until the next timestamp.
- Resting HR has no history on the watch, only today’s value and a 7-day average. It is the lowest 30-minute average of the 1-minute heart rate within a calendar day, which reproduces the watch’s value exactly from
Monitor/data. To keep a history, either store the value written at midnight (it belongs to the day that just ended) or compute it from a full day ofMonitor/data.
Firstbeat backups (FbtBackup/, FbtPtdBackup/)
“Fbt” refers to Firstbeat, the company behind Garmin’s training status, load and recovery metrics. FbtBackup/lha_bkup.fit contains only undocumented messages. Comparing them with the documented Sleep/ and HRVStatus/ files identifies these fields:
| Message, field | Contents | History |
|---|---|---|
| 384, field 2 | Sleep score | 100 nights |
| 384, fields 9 / 8 | Bedtime (UTC) / wake time (local) | 100 nights |
| 401, fields 2 / 3 / 4 | HRV 7-day average, baseline lower / upper, per day | 28 days |
The sleep scores match sleep_assessment and the HRV values match hrv_status_summary exactly. This makes FbtBackup/ the only multi-week health history on the watch. The watch rewrites this file in place, so keep every downloaded version.
FbtPtdBackup/ptd_bkup.fit holds Firstbeat’s per-activity metrics (message 140, as in the activity files) for the last 150 activities.
Lifetime data (Totals/, Records/, Goals/)
| File | Contents | Documented |
|---|---|---|
Totals/Totals.fit | Sessions, time, distance and calories per sport | yes |
Records/Records.fit | Personal records in message 114: field 1 = sport, field 2 = distance (cm), field 5 = time (ms) | no |
Goals/Goals.fit | Step, active minutes and ascent goals | yes |
Record distances are exact (100,000 = 1 km, 4,219,500 = a marathon), which makes them easy to identify.
Undocumented messages
Of the 126 message types across all files, 66 are not in the public FIT profile. Parsers show them as numbers (384) with fields named unknown_field_N. The ones identified so far:
| Where | Contents | Confidence |
|---|---|---|
record field 136 | Wrist HR | checked |
record field 144 | Strap HR (copy of heart_rate) | checked |
record field 90 | Performance condition | likely |
| Message 140 | Per-activity Firstbeat metrics: training effect ×10 (fields 4, 20), recovery time (field 9) | checked / likely |
| Message 104 | Watch battery during activity (mV, %, °C) | likely |
| Message 79 | User metrics snapshot: height, weight, max HR, LTHR | likely |
| Message 384 | Nightly summary: sleep score, bedtime, wake time | checked |
| Message 401 | 28-day HRV trend | checked |
| Message 114 | Personal records | checked |
Checked means the values match a documented field exactly. Likely means the identification rests on the range and shape of the values.
Not located: Body Battery, SpO2 and training status. They are probably in one of the undocumented messages in Monitor/ or Metrics/.
Pitfalls
- File extensions are mixed case:
SUMMARY/contains both.fitand.FIT. - Multisport activities contain several sessions in one file. Some parsers skip them.
- Undocumented messages often store a time twice: in UTC, and in local time encoded as if it were UTC.
- Assign a night to its wake date, and a value written at midnight to the previous day.
Monitor/and the Firstbeat backups change between syncs. Activity files don’t.
Unstrap
This exploration was part of building Unstrap, a tool that copies every file off a Garmin watch, from the browser or the command line, and keeps every version. It’s in beta. Everything above also works with the open source tools listed earlier and a USB cable.
If you’ve read data from other Garmin models and found differences, or decoded one of the remaining messages, let me know and I’ll update this post.